β οΈ Disclaimer: This is an unofficial, community-driven SDK and is not supported or endorsed by GitHub. Use at your own risk.
This guide covers advanced scenarios for extending and customizing your Copilot integration.
- Custom Tools
- System Messages
- File Attachments
- Bring Your Own Key (BYOK)
- Infinite Sessions
- MCP Servers
- Custom Agents
- Skills Configuration
- Custom Configuration Directory
- User Input Handling
- Permission Handling
- Session Hooks
- Manual Server Control
- Session Lifecycle Events
- Foreground Session Control (TUI+Server Mode)
- Error Handling
Let the AI call back into your application to fetch data or perform actions.
// 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()
.setTools(List.of(lookupTool))
).get();See ToolDefinition Javadoc for schema details.
Customize the AI's behavior by adding rules or replacing the default prompt.
Use APPEND mode to add constraints while keeping default guardrails:
var session = client.createSession(
new SessionConfig()
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.APPEND)
.setContent("""
<rules>
- Always check for security vulnerabilities
- Suggest performance improvements
</rules>
"""))
).get();Use REPLACE mode for complete control (removes default guardrails):
var session = client.createSession(
new SessionConfig()
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.REPLACE)
.setContent("You are a helpful coding assistant."))
).get();Include files as context for the AI to analyze.
session.send(new MessageOptions()
.setPrompt("Review this file for bugs")
.setAttachments(List.of(
new Attachment("file", "/path/to/file.java", "MyService.java")
))
).get();Use your own OpenAI or Azure OpenAI API key instead of GitHub Copilot.
var session = client.createSession(
new SessionConfig()
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("https://api.openai.com/v1")
.setApiKey("sk-..."))
).get();Some providers require bearer token authentication instead of API keys:
var session = client.createSession(
new SessionConfig()
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("https://my-custom-endpoint.example.com/v1")
.setBearerToken(System.getenv("MY_BEARER_TOKEN")))
).get();Note: The
bearerTokenoption 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.
When using BYOK, be aware of these 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.
Run long conversations without hitting context limits.
When enabled (default), the session automatically compacts older messages as the context window fills up.
var session = client.createSession(
new SessionConfig()
.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();When compaction occurs, the session emits events that you can listen for:
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.isSuccess()
+ ", tokens removed: " + data.getTokensRemoved());
});For short conversations, disable to avoid overhead:
new InfiniteSessionConfig().setEnabled(false)Extend the AI with external tools via the Model Context Protocol.
Map<String, Object> server = Map.of(
"type", "local",
"command", "npx",
"args", List.of("-y", "@modelcontextprotocol/server-filesystem", "/tmp"),
"tools", List.of("*")
);
var session = client.createSession(
new SessionConfig()
.setMcpServers(Map.of("filesystem", server))
).get();π Full MCP documentation β for local/remote servers and all options.
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.
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()
.setCustomAgents(List.of(reviewer))
).get();
// The user can now mention @code-reviewer in messages
session.send("@code-reviewer Review src/Main.java").get();| 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 |
Register multiple agents for different tasks:
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().setCustomAgents(agents)
).get();See CustomAgentConfig Javadoc for full details.
Load custom skills from directories to extend the AI's capabilities with domain-specific knowledge.
Skills are loaded from SKILL.md files in subdirectories of the specified skill directories:
var session = client.createSession(
new SessionConfig()
.setSkillDirectories(List.of("/path/to/skills"))
).get();Each skill subdirectory should contain a SKILL.md file with YAML frontmatter:
---
name: my-skill
description: A skill that provides domain-specific knowledge
---
# Skill Instructions
Your skill instructions go here...Disable specific skills by name:
var session = client.createSession(
new SessionConfig()
.setSkillDirectories(List.of("/path/to/skills"))
.setDisabledSkills(List.of("my-skill"))
).get();Use a custom configuration directory for session settings:
var session = client.createSession(
new SessionConfig()
.setConfigDir("/path/to/custom/config")
).get();This is useful when you need to isolate session configuration or use different settings for different environments.
Handle user input requests when the AI uses the ask_user tool to gather information from the user.
var session = client.createSession(
new SessionConfig()
.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 askinggetChoices()- Optional list of choices for the user to select from
The UserInputResponse should include:
setAnswer(String)- The user's answersetWasFreeform(boolean)-trueif the answer was freeform text,falseif it was from the provided choices
See UserInputHandler Javadoc for more details.
Approve or deny permission requests from the AI.
var session = client.createSession(
new SessionConfig()
.setOnPermissionRequest((request, invocation) -> {
// Inspect request and approve/deny
var result = new PermissionRequestResult();
result.setKind("user-approved");
return CompletableFuture.completedFuture(result);
})
).get();Intercept tool execution and session lifecycle events using hooks.
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().setHooks(hooks)
).get();π Full Session Hooks documentation β for all 5 hook types, inputs/outputs, and examples.
Control the CLI lifecycle yourself instead of auto-start.
var client = new CopilotClient(
new CopilotClientOptions().setAutoStart(false)
);
client.start().get(); // Start manually
// ... use client ...
client.stop().get(); // Stop manuallySubscribe to lifecycle events to be notified when sessions are created, deleted, updated, or change foreground/background state.
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();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 createdSessionLifecycleEventTypes.DELETED- Session was deletedSessionLifecycleEventTypes.UPDATED- Session was updatedSessionLifecycleEventTypes.FOREGROUND- Session moved to foreground (TUI+server mode)SessionLifecycleEventTypes.BACKGROUND- Session moved to background (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.
var sessionId = client.getForegroundSessionId().get();
if (sessionId != null) {
System.out.println("TUI is displaying session: " + sessionId);
}client.setForegroundSessionId("session-123").get();All SDK methods return CompletableFuture. Errors surface via ExecutionException:
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():
session.send(new MessageOptions().setPrompt("Hello"))
.exceptionally(ex -> {
System.err.println("Failed: " + ex.getMessage());
return null;
});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:
// 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().getContent());
});Errors are always logged at WARNING level regardless of the policy or
whether a custom error handler is set.
Set a custom EventErrorHandler for additional handling beyond the default
logging β such as metrics, alerts, or integration with external
error-reporting systems:
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:
session.setEventErrorHandler(null);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:
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:
// 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:
// 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 and EventErrorHandler Javadoc for details.