diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 000000000..389bcda90 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Pre-commit hook that runs Spotless check on the Java SDK when Java source +# files are staged. Only triggers if changes exist under java/src/. +# +# To install this hook, run from the repository root: +# git config core.hooksPath .githooks +# + +# Only run Spotless if staged changes include Java source files under java/src/ +if ! git diff --cached --name-only | grep -q '^java/src/'; then + exit 0 +fi + +echo "Running Spotless check on java/ ..." + +# Run spotless check from the java directory +(cd java && mvn spotless:check -q) + +if [ $? -ne 0 ]; then + echo "" + echo "❌ Spotless check failed!" + echo " Run 'cd java && mvn spotless:apply' to fix formatting issues." + echo "" + exit 1 +fi + +echo "βœ“ Spotless check passed" +exit 0 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1dad5f95c..000338e23 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,13 +4,14 @@ ## Big picture πŸ”§ -- The repo implements language SDKs (Node/TS, Python, Go, .NET) that speak to the **Copilot CLI** via **JSON‑RPC** (see `README.md` and `nodejs/src/client.ts`). -- Typical flow: your App β†’ SDK client β†’ JSON-RPC β†’ Copilot CLI (server mode). The CLI must be installed or you can connect to an external CLI server via the `CLI URL option (language-specific casing)` (Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`). +- The repo implements language SDKs (Node/TS, Python, Go, .NET, Java) that speak to the **Copilot CLI** via **JSON‑RPC** (see `README.md` and `nodejs/src/client.ts`). +- Typical flow: your App β†’ SDK client β†’ JSON-RPC β†’ Copilot CLI (server mode). The CLI must be installed or you can connect to an external CLI server via the `CLI URL option (language-specific casing)` (Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`, Java: `cliUrl`). ## Most important files to read first πŸ“š - Top-level: `README.md` (architecture + quick start) - Language entry points: `nodejs/src/client.ts`, `python/README.md`, `go/README.md`, `dotnet/README.md` +- Java: `java/README.md`, `java/pom.xml` - Test harness & E2E: `test/harness/*`, Python harness wrapper `python/e2e/testharness/proxy.py` - Schemas & type generation: `nodejs/scripts/generate-session-types.ts` - Session snapshots used by E2E: `test/snapshots/` (used by the replay proxy) @@ -26,12 +27,15 @@ - Go: `cd go && go test ./...` - .NET: `cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj` - **.NET testing note:** Never add `InternalsVisibleTo` to any project file when writing tests. Tests must only access public APIs. + - Java: `cd java && mvn clean verify` (full build + tests), `mvn spotless:apply` (format code before commit) + - **Java testing note:** Always use `mvn verify` without `-q` and without piping through `grep`. Never add `InternalsVisibleTo` equivalent β€” tests must only access public APIs. ## Testing & E2E tips βš™οΈ - E2E runs against a local **replaying CAPI proxy** (see `test/harness/server.ts`). Most language E2E harnesses spawn that server automatically (see `python/e2e/testharness/proxy.py`). - Tests rely on YAML snapshot exchanges under `test/snapshots/` β€” to add test scenarios, add or edit the appropriate YAML files and update tests. - The harness prints `Listening: http://...` β€” tests parse this URL to configure CLI or proxy. +- Java E2E tests use `E2ETestContext` which manages a `CapiProxy` (Node.js replaying proxy). The harness is cloned during Maven's `generate-test-resources` phase to `java/target/copilot-sdk/`. ## Project-specific conventions & patterns βœ… @@ -42,13 +46,14 @@ ## Integration & environment notes ⚠️ -- The SDK requires a Copilot CLI installation or an external server reachable via the `CLI URL option (language-specific casing)` (Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`) or `COPILOT_CLI_PATH`. +- The SDK requires a Copilot CLI installation or an external server reachable via the `CLI URL option (language-specific casing)` (Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`, Java: `cliUrl`) or `COPILOT_CLI_PATH`. - Some scripts (typegen, formatting) call external tools: `gofmt`, `dotnet format`, `tsx` (available via npm), `quicktype`/`quicktype-core` (used by the Node typegen script), and `prettier` (provided as an npm devDependency). Most of these are available through the repo's package scripts or devDependenciesβ€”run `just install` (and `cd nodejs && npm ci`) to install them. Ensure the required tools are available in CI / developer machines. - Tests may assume `node >= 18`, `python >= 3.9`, platform differences handled (Windows uses `shell=True` for npx in harness). +- Java requires JDK 17+ and Maven 3.9+. Java E2E tests also require Node.js (for the replay proxy). ## Where to add new code or tests 🧭 -- SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`, `rust/src` -- Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests` -- E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/` -- Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location. +- SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`, `rust/src`, `java/src/main/java` +- Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests`, `java/src/test/java` +- E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/`, `java/src/test/java/**/e2e/` +- Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location. Java generated types: `java/src/generated/java` diff --git a/.github/skills/java-coding-skill/SKILL.md b/.github/skills/java-coding-skill/SKILL.md new file mode 100644 index 000000000..e48ad00ed --- /dev/null +++ b/.github/skills/java-coding-skill/SKILL.md @@ -0,0 +1,757 @@ +--- +name: java-coding-skill +description: "Use this skill whenever editing `*.java` files in the `java/` SDK in order to write idiomatic, well-structured Java code for the Copilot SDK" +--- + +# Java Coding Skill + +## 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(); +``` diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml new file mode 100644 index 000000000..0567a4b56 --- /dev/null +++ b/.github/workflows/java-publish-maven.yml @@ -0,0 +1,255 @@ +name: Publish to Maven Central + +env: + # Disable Husky Git hooks in CI to prevent local development hooks + # (e.g., pre-commit formatting checks) from running during automated + # workflows that perform git commits and pushes. + HUSKY: 0 + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: "Release version (e.g., 1.0.0). If empty, derives from pom.xml by removing -SNAPSHOT" + required: false + type: string + developmentVersion: + description: "Next development version (e.g., 1.0.1-SNAPSHOT). If empty, increments patch version" + required: false + type: string + prerelease: + description: "Is this a prerelease?" + type: boolean + required: false + default: false + +permissions: + contents: write + id-token: write + +concurrency: + group: publish-maven + cancel-in-progress: false + +jobs: + publish-maven: + name: Publish Java SDK to Maven Central + runs-on: ubuntu-latest + outputs: + version: ${{ steps.versions.outputs.release_version }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + token: ${{ secrets.JAVA_RELEASE_TOKEN }} + + - name: Configure Git for Maven Release + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - uses: ./.github/actions/setup-copilot + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "17" + distribution: "microsoft" + cache: "maven" + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.JAVA_GPG_SECRET_KEY }} + gpg-passphrase: JAVA_GPG_PASSPHRASE + + - name: Determine versions + id: versions + run: | + CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Current pom.xml version: $CURRENT_VERSION" + + # Determine release version + if [ -n "${{ inputs.releaseVersion }}" ]; then + RELEASE_VERSION="${{ inputs.releaseVersion }}" + else + # Remove -SNAPSHOT suffix if present + RELEASE_VERSION="${CURRENT_VERSION%-SNAPSHOT}" + fi + echo "Release version: $RELEASE_VERSION" + + # Determine next development version + if [ -n "${{ inputs.developmentVersion }}" ]; then + DEV_VERSION="${{ inputs.developmentVersion }}" + if [[ "$DEV_VERSION" != *-SNAPSHOT ]]; then + echo "::error::developmentVersion '${DEV_VERSION}' must end with '-SNAPSHOT' (e.g., '${DEV_VERSION}-SNAPSHOT'). The maven-release-plugin requires the next development version to be a snapshot." + exit 1 + fi + else + # Split version: supports "0.1.32", "0.1.32-java.0", and "0.1.32-java-preview.0" formats + # Validate RELEASE_VERSION format explicitly to provide clear errors + if ! echo "$RELEASE_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-(beta-)?java(-preview)?\.[0-9]+)?$'; then + echo "Error: RELEASE_VERSION '$RELEASE_VERSION' is invalid. Expected format: M.M.P, M.M.P-java.N, M.M.P-java-preview.N, M.M.P-beta-java.N, or M.M.P-beta-java-preview.N (e.g., 1.2.3, 1.2.3-java.0, 1.2.3-java-preview.0, 1.2.3-beta-java.0, or 1.2.3-beta-java-preview.0)." >&2 + exit 1 + fi + # Extract the base M.M.P portion (before any qualifier) + BASE_VERSION=$(echo "$RELEASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') + QUALIFIER=$(echo "$RELEASE_VERSION" | sed "s|^${BASE_VERSION}||") + IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" + NEXT_PATCH=$((PATCH + 1)) + DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}${QUALIFIER}-SNAPSHOT" + fi + echo "Next development version: $DEV_VERSION" + + echo "release_version=$RELEASE_VERSION" >> $GITHUB_OUTPUT + echo "dev_version=$DEV_VERSION" >> $GITHUB_OUTPUT + + echo "### Version Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Release version:** $RELEASE_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Next development version:** $DEV_VERSION" >> $GITHUB_STEP_SUMMARY + + - name: Update documentation with release version + id: update-docs + run: | + VERSION="${{ steps.versions.outputs.release_version }}" + + # Read the reference implementation SDK commit hash that this release is synced to + REFERENCE_IMPL_HASH=$(cat .lastmerge) + REFERENCE_IMPL_SHORT="${REFERENCE_IMPL_HASH:0:7}" + REFERENCE_IMPL_URL="https://github.com/github/copilot-sdk/commit/${REFERENCE_IMPL_HASH}" + echo "Reference implementation SDK sync: ${REFERENCE_IMPL_SHORT} (${REFERENCE_IMPL_URL})" + + # Update CHANGELOG.md with release version and Reference implementation sync hash + ./.github/scripts/release/update-changelog.sh "${VERSION}" "${REFERENCE_IMPL_HASH}" + + # Update version in README.md (supports any version qualifier like -java.N, -java-preview.N, -beta-java.N) + sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|${VERSION}|g" README.md + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" README.md + + # Update snapshot version in README.md + DEV_VERSION="${{ steps.versions.outputs.dev_version }}" + sed -i "s|[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*-SNAPSHOT|${DEV_VERSION}|g" README.md + + # Update version in jbang-example.java + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" jbang-example.java + sed -i 's|copilot-sdk-java:${project\.version}|copilot-sdk-java:'"${VERSION}"'|g' jbang-example.java + + # Update version in cookbook files (hardcoded for direct GitHub browsing and JBang usage) + find src/site/markdown/cookbook -name "*.md" -type f -exec \ + sed -i "s|copilot-sdk-java:[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\(-[a-z][a-z0-9-]*\.[0-9][0-9]*\)*|copilot-sdk-java:${VERSION}|g" {} \; + + # Commit the documentation changes before release:prepare (requires clean working directory) + git add CHANGELOG.md README.md jbang-example.java src/site/markdown/cookbook/ + git commit -m "docs: update version references to ${VERSION}" + + # Save the commit SHA for potential rollback + echo "docs_commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + + git push origin main + + - name: Prepare Release + run: | + mvn -B release:prepare \ + -DreleaseVersion=${{ steps.versions.outputs.release_version }} \ + -DdevelopmentVersion=${{ steps.versions.outputs.dev_version }} \ + -DtagNameFormat=v@{project.version} \ + -DpushChanges=true \ + -Darguments="-DskipTests" + env: + MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }} + JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }} + + - name: Perform Release and Deploy to Maven Central + run: | + mvn -B release:perform \ + -Dgoals="deploy" \ + -Darguments="-DskipTests -Prelease" + env: + MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }} + JAVA_GPG_PASSPHRASE: ${{ secrets.JAVA_GPG_PASSPHRASE }} + + - name: Rollback documentation commit on failure + if: failure() && steps.update-docs.outputs.docs_commit_sha != '' + run: | + echo "Release failed, rolling back documentation commit..." + git revert --no-edit ${{ steps.update-docs.outputs.docs_commit_sha }} + git push origin main + + # Also run Maven release:rollback to clean up any partial release state + mvn -B release:rollback || true + + github-release: + name: Create GitHub Release + needs: publish-maven + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + - name: Create GitHub Release + run: | + VERSION="${{ needs.publish-maven.outputs.version }}" + GROUP_ID="com.github" + ARTIFACT_ID="copilot-sdk-java" + CURRENT_TAG="v${VERSION}" + + if gh release view "${CURRENT_TAG}" >/dev/null 2>&1; then + echo "Release ${CURRENT_TAG} already exists. Skipping creation." + exit 0 + fi + + # Generate release notes from template + export VERSION GROUP_ID ARTIFACT_ID + RELEASE_NOTES=$(envsubst < .github/workflows/notes.template) + + # Get the previous tag for generating notes + PREV_TAG=$(git tag --list 'v*' --sort=-version:refname \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-java(-preview)?\.[0-9]+)?$' \ + | grep -Fxv "${CURRENT_TAG}" \ + | head -n 1) + + echo "Current tag: ${CURRENT_TAG}" + echo "Previous tag: ${PREV_TAG}" + + # Build the gh release command + GH_ARGS=("${CURRENT_TAG}") + GH_ARGS+=("--title" "GitHub Copilot SDK for Java ${VERSION}") + GH_ARGS+=("--notes" "${RELEASE_NOTES}") + GH_ARGS+=("--generate-notes") + + if [ -n "$PREV_TAG" ]; then + GH_ARGS+=("--notes-start-tag" "$PREV_TAG") + fi + + ${{ inputs.prerelease == true && 'GH_ARGS+=("--prerelease")' || '' }} + + gh release create "${GH_ARGS[@]}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Move 'latest' tag to new release + run: | + VERSION="${{ needs.publish-maven.outputs.version }}" + git tag -f latest "v${VERSION}" + git push origin latest --force + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + deploy-site: + name: Deploy Documentation + needs: [publish-maven, github-release] + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: Trigger site deployment + run: | + gh workflow run deploy-site.yml \ + --repo ${{ github.repository }} \ + -f version="${{ needs.publish-maven.outputs.version }}" \ + -f publish_as_latest=true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/java-publish-snapshot.yml b/.github/workflows/java-publish-snapshot.yml new file mode 100644 index 000000000..d7720ceb2 --- /dev/null +++ b/.github/workflows/java-publish-snapshot.yml @@ -0,0 +1,55 @@ +name: Publish Snapshot to Maven Central + +env: + HUSKY: 0 + +on: + schedule: + - cron: "0 7 * * 1-5" # Mon-Fri at 07:00 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: publish-snapshot + cancel-in-progress: false + +jobs: + publish-snapshot: + name: Publish SNAPSHOT to Maven Central + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - uses: ./.github/actions/setup-copilot + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: "17" + distribution: "microsoft" + cache: "maven" + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: Verify version is a SNAPSHOT + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Publishing version: $VERSION" + if [[ "$VERSION" != *"-SNAPSHOT" ]]; then + echo "ERROR: This workflow only publishes SNAPSHOT versions. Current version: $VERSION" + exit 1 + fi + echo "### Snapshot Publish" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** $VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Repository:** Maven Central Snapshots" >> $GITHUB_STEP_SUMMARY + + - name: Deploy Snapshot + run: mvn -B deploy -DskipTests + env: + MAVEN_USERNAME: ${{ secrets.JAVA_MAVEN_CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.JAVA_MAVEN_CENTRAL_PASSWORD }} diff --git a/.gitignore b/.gitignore index 4821b5e65..23915a160 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ test/scenarios/**/rust/Cargo.lock # C# Dev Kit *.csproj.lscache + +*job-logs.txt* diff --git a/80-java-monorepo-add-01-remove-before-merge/20260512-prompts.md b/80-java-monorepo-add-01-remove-before-merge/20260512-prompts.md new file mode 100644 index 000000000..8073365b7 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/20260512-prompts.md @@ -0,0 +1,432 @@ + +### 17:17 Prompt + +Looking at `temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md`, for each of the existing workflows in section "1B. `copilot-sdk-00` Workflows", with the exception of the already language specific `*-sdk-tests.yml`, what changes would one need to make to these workflows to ensure that none of the content in `java` would accidentally get picked up by those workflows? + +### 17:24 Response + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#1-1), lines 1 to 200 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#200-200), lines 200 to 400 + +Let me look at the actual workflow files in the monorepo to assess each one. + +Searched for files matching `copilot-sdk-00` Β· `.github/workflows/*`, 20 matches + +Searched for files matching `copilot-sdk-00` Β· `.github/workflows/*`, 32 matches + +Created 13 todos + +Let me read all the non-language-specific workflow files in parallel: + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/codegen-check.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/publish.yml#1-1), lines 1 to 200 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/scenario-builds.yml#1-1), lines 1 to 200 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/docs-validation.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/update-copilot-dependency.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/copilot-setup-steps.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/verify-compiled.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/collect-corrections.yml) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/corrections-tests.yml) + +Now let me read the remaining publish.yml and the agentic workflow `.md` files: + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/publish.yml#200-200), lines 200 to 315 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/issue-classification.md#1-1), lines 1 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/issue-triage.md) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/handle-bug.md) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/handle-documentation.md) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/handle-enhancement.md) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/handle-question.md) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/cross-repo-issue-analysis.md#1-1), lines 1 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/release-changelog.md#1-1), lines 1 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/sdk-consistency-review.md) + +Starting: *Compile analysis* (2/2) + +Let me also check the codegen script to see if it could pick up Java content: + +Read [](file:///e%3A/workareas/copilot-sdk-00/scripts/codegen) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/workflows/scenario-builds.yml#1-1), lines 1 to 30 + +Read [](file:///e%3A/workareas/copilot-sdk-00/scripts/codegen/package.json) + +Completed: *Compile analysis* (2/2) + +Here's the analysis of each non-`*-sdk-tests.yml` workflow in section 1B, grouped by risk level: + +--- + +## HIGH RISK β€” Workflows that would actively process or misrepresent Java content + +### 1. codegen-check.yml +- **Problem**: The `push` trigger on `main` has **no path filter** β€” every push to `main` (including Java-only changes) fires this workflow. While the existing codegen scripts (package.json `generate` script) only run `typescript.ts`, `csharp.ts`, `python.ts`, `go.ts`, `rust.ts`, if a `java.ts` is later merged into codegen, the `npm run generate` step would execute it too. +- **Changes needed**: + - Add `paths-ignore: ['java/**']` to the `push` trigger, or add explicit `paths:` matching the existing language dirs. + - Alternatively, after Java codegen is merged, do **not** add `java.ts` to the `generate` script in package.json β€” keep `generate:java` as a separate script only invoked by `java-sdk-tests.yml` / a separate `java-codegen-check` job. + +### 2. update-copilot-dependency.yml +- **Problem**: The `npm run generate` step ([line ~77](copilot-sdk-00/.github/workflows/update-copilot-dependency.yml)) runs **all** codegen scripts. If `java.ts` is added to the `generate` script, it would generate Java code. The subsequent "Format generated code" step only formats nodejs, dotnet, and rust β€” Java output would be committed **unformatted**. +- **Changes needed**: + - Ensure the `generate` npm script in package.json does NOT include Java. + - OR add an explicit `java/**` exclusion from the `git add -A` / commit step. + - The "Format generated code" step currently only references nodejs, dotnet, rust β€” if Java codegen is ever included, add `cd ../java && mvn spotless:apply` there, or better, keep Java out of this workflow entirely. + +### 3. publish.yml +- **Problem**: Manually triggered, but the `github-release` job creates a GitHub Release with auto-generated notes (`--generate-notes`). These notes include **all** PRs merged since the last tag, including Java PRs. Java changes would appear in release notes for a release that doesn't include Java. +- **Changes needed**: + - Add `--exclude-labels java` or similar filtering to the `gh release create` steps so Java-only PRs are excluded from the release notes. + - Alternatively, document that the release-changelog.md agentic workflow is the source of truth and configure `--notes ""` plus rely on the changelog agent to filter. + +### 4. release-changelog.md +- **Problem**: Gathers ALL merged PRs/commits between tags and categorizes by SDK. Java PRs would be included in the changelog. The prompt says "TypeScript and C# as the primary languages" for code snippets and only lists Node, Python, Go, .NET. Java changes would either be miscategorized or lumped into "Other changes." +- **Changes needed**: + - Add explicit instruction in the prompt: "Exclude PRs that only touch `java/**` β€” the Java SDK has its own independent release and changelog." + - Or add a filter step before the agent runs that identifies and excludes Java-only commits. + +--- + +## MEDIUM RISK β€” Workflows where Java content would cause incorrect agent behavior + +### 5. sdk-consistency-review.md +- **Problem**: Path triggers are `nodejs/**`, `python/**`, `go/**`, `dotnet/**` β€” does NOT include `java/**`. The "SDK Locations" in the prompt lists only 4 SDKs. If a PR touches both `dotnet/**` and `java/**`, the workflow fires (due to dotnet), but the agent would **not check Java for consistency** because it's not in its list. +- **Changes needed**: + - This is a deliberate non-inclusion per the plan (Java is to be added later). But to ensure the agent doesn't stumble upon java and give incorrect consistency advice, add a note to the prompt: "The java directory exists but is managed separately β€” do not include it in your consistency analysis." + +### 6. issue-triage.md +- **Problem**: The `add-labels.allowed` list includes `sdk/dotnet`, `sdk/go`, `sdk/nodejs`, `sdk/python` but NOT `sdk/java`. A Java-related issue would not receive proper SDK labeling. The agent's description says "implementations in .NET, Go, Node.js, and Python" β€” no Java. +- **Changes needed**: + - Until Java labeling is desired: add a note to the prompt telling the agent that java exists but should not receive an SDK label (or should receive a generic label like `needs-info` and be left for human triage). + +### 7. issue-classification.md +- **Problem**: Description says "multi-language SDK (Node.js/TypeScript, Python, Go, .NET)." A Java-related issue would still be classified (bug/enhancement/question/documentation are language-agnostic categories), but the agent lacks Java context and might misjudge whether behavior is a bug vs. working-as-designed. +- **Changes needed**: + - Add a note: "The java SDK also exists in this repository but has a separate triage process. If an issue specifically concerns the Java SDK, classify it normally but note in your comment that it relates to the Java SDK." + +### 8. handle-bug.md +- **Problem**: The investigation instructions tell the agent to search src, copilot, go, src β€” **not** java. A Java bug report would be investigated only in the other SDKs, leading to incorrect conclusions. +- **Changes needed**: + - Add a guard: "If the issue specifically references the Java SDK (java), note this in your comment and do not attempt to analyze Java source code β€” the Java SDK is maintained separately." + +### 9. cross-repo-issue-analysis.md +- **Problem**: Same as handle-bug β€” explicit directory list for searching SDK code doesn't include java. A Java SDK issue labeled `runtime triage` would be analyzed without looking at the Java code. +- **Changes needed**: + - Add `java/src/` to the list of SDK directories to search, or add the same "Java is maintained separately" guard. + +--- + +## LOW RISK β€” Workflows with minimal or no concern + +### 10. scenario-builds.yml +- **Problem**: The `find test/scenarios` commands look for language-specific markers (`*/typescript/package.json`, `*/python/main.py`, `*/go/go.mod`, `*.csproj`). None of these match Java files. The path triggers include `test/scenarios/**`, so Java scenario changes would trigger the workflow but **no Java job would run** (wasted CI only). +- **Changes needed**: + - Minor: add `!test/scenarios/**/java/**` to paths or rely on the fact that no job processes Java files. No functional risk. + +### 11. docs-validation.yml +- **Problem**: PR trigger includes `docs/**`. If Java code snippets (```java fenced blocks) are added to docs, the extraction scripts might pull them but there's no `validate:java` step. Depending on the extraction script behavior, these could be silently skipped or cause errors. +- **Changes needed**: + - Check docs-validation extraction scripts. If they extract by fenced code block language tag, ensure unknown languages are gracefully skipped. If they extract all blocks, add Java to the skip list. + +### 12. copilot-setup-steps.yml +- **No risk**. Only triggers on changes to itself. Installs dependencies for nodejs, python, go, dotnet. Java would need its own additions to this file eventually, but the existing workflow won't touch java. + +### 13. verify-compiled.yml +- **No risk (actually desired)**. Triggers on `.github/workflows/*.md` and `.lock.yml`. If Java agentic workflows (like `java-codegen-fix.md`) are added to workflows, this workflow correctly verifies their lock files. This is **correct behavior**, not accidental pickup. + +### 14. collect-corrections.yml +- **No risk**. Triggers on `repository_dispatch` and `workflow_dispatch`. Runs a specific JS script. Language-agnostic. + +### 15. corrections-tests.yml +- **No risk**. Only triggers on `scripts/corrections/**`. No language-specific content. + +### 16. handle-documentation.md, handle-enhancement.md, handle-question.md +- **No risk**. These just add labels. No language-specific logic or code search. + +--- + +## Summary of key protective measures + +| # | Action | Workflows protected | +|---|--------|-------------------| +| 1 | Keep `java.ts` out of the package.json `generate` script | codegen-check.yml, update-copilot-dependency.yml | +| 2 | Add `paths-ignore: ['java/**']` to codegen-check.yml push trigger | codegen-check.yml | +| 3 | Filter Java PRs from release notes generation | publish.yml, release-changelog.md | +| 4 | Add "Java is separately managed" guardrails to agentic prompts | issue-classification.md, issue-triage.md, handle-bug.md, cross-repo-issue-analysis.md, sdk-consistency-review.md | + +### 17:31 Prompt + +I'm not familiar with `just`. From what language ecosystem does this come? Do Java shops use it? + +### 17:33 Response + + + +`just` is a **command runner** inspired by `make`, written in **Rust** by Casey Rodarmor. It comes from the Rust ecosystem but is language-agnostic β€” it's essentially a modern, simpler alternative to `Makefile` for running project-specific commands. + +**Do Java shops use it?** Rarely. Java projects overwhelmingly rely on **Maven** or **Gradle** as their task runners (via lifecycle phases, plugins, and custom tasks). `just` is more popular in Rust, Go, and polyglot/scripting-heavy repos where there's no dominant build tool that already serves as a command runner. + +In this monorepo (`copilot-sdk-00`), `just` makes sense because it unifies commands across 5+ language ecosystems (`just test`, `just format`, `just lint`) β€” each of which has its own native build tool underneath. The justfile delegates to `npm`, go, dotnet, `uv`, `cargo`, etc. per language. + +### 17:33 Prompt + +But might it make sense to simply not make `just` touch the Java? + +### 17:34 Response + + + +Yes, absolutely. Maven already serves as Java's command runner, and every Java developer already knows `mvn verify`, `mvn spotless:apply`, etc. Adding `just` targets for Java would mean: + +- Maintaining a **redundant indirection layer** (`just test-java` β†’ `cd java && mvn verify`) +- Requiring Java contributors to install a tool (`just`) that adds zero value over what Maven already provides +- Creating a discoverability problem β€” Java devs would need to know to look in the justfile instead of the pom.xml + +The other languages in the monorepo benefit from `just` because their native tools don't provide a unified CLI across the repo. But Maven is already a self-contained build/test/format/lint/publish lifecycle β€” it doesn't need wrapping. + +The practical approach: **don't add Java targets to the justfile**. Document in README.md that Java uses `mvn` commands directly, same as it does today. The top-level `just test` / `just format` / `just lint` recipes would continue to cover the other 5 SDKs, and Java would stay self-contained under java. + +### 17:39 Prompt + +The file `temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md` was output from our work yesterday. One thing I am aware you are missing is what to do about the `src/test/prompts/PROMPT-smoke-test.md` smoke test. This test currently assumes a specific structure of the top level `README.md`. We will need to update this file and the `README.md` itself on the monorepo side so it can continue to function. Add an "Appendix C: Java smoketest" section describing how to handle this. Cover the workflows that invoke this. + +Make a reference to Appendix C at the first mention of the smoke test, which is here: + +``` +| `run-smoke-test.yml` | Builds SDK, installs locally, runs Copilot CLI smoke test on JDK 17 + JDK 25 | Java | M | +``` + +Update that **Brief Description** cell to include a reference to your new Appendix C. + +### 17:54 Response + +Searched for files matching `copilot-sdk-java-00` Β· `.github/workflows/run-smoke-test*`, 1 match + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/src/test/prompts/PROMPT-smoke-test.md#1-1), lines 1 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/README.md#1-1), lines 1 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/src/test/prompts/PROMPT-smoke-test.md#100-100), lines 100 to 136 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/run-smoke-test.yml) + +Now let me also check the monorepo's README to understand what exists there, and find the end of the plan document for the appendix: + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#360-360), lines 360 to 500 + +Read [](file:///e%3A/workareas/copilot-sdk-00/README.md#1-1), lines 1 to 50 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#500-500), lines 500 to 539 + +Now I have all the context. Let me also check the monorepo's README.md to understand what exists there: + +Read [](file:///e%3A/workareas/copilot-sdk-00/java/README.md#1-1), lines 1 to 50 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/README.md#100-100), lines 100 to 100 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/README.md#60-60), lines 60 to 150 + +Now I have full context. Let me also check if there are any workflows that call run-smoke-test.yml: + +Searched for text `run-smoke-test` (`copilot-sdk-java-00` Β· `.github/workflows/**`), 1 result + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/build-test.yml#20-20), lines 20 to 40 + +Searched for text `smoke-test` (`copilot-sdk-java-00` Β· `.github/workflows/**`), 18 results + +Now I have all the context. Let me also check the `publish-maven.yml` to see if it also calls the smoke test: + +Searched for text `smoke` (`copilot-sdk-java-00` Β· `.github/workflows/publish-maven.yml`), no results + +Good, only build-test.yml calls the smoke test. Now I have everything I need. Let me make the two edits to the plan document: + +Replacing 1 lines with 1 lines in [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md) + + +``` + +``` +Now find the exact end of the document to add Appendix C: + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#539-539), lines 539 to 539 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#535-535), lines 535 to 539 + +Replacing 1 lines with 103 lines in [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md) + + +``` + +``` +Now add Appendix C to the table of contents: + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md#1-1), lines 1 to 12 + +Replacing 8 lines with 11 lines in [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md) + + +``` + +``` +Done. Three edits made to dd-2989727-move-java-to-monorepo-plan.md: + +1. **Table of Contents** β€” added links for Appendices A, B, and C +2. **run-smoke-test.yml row** β€” appended "(see Appendix C)" to the description +3. **Appendix C: Java Smoketest** β€” new section at the end covering: + - How the smoke test works today (the prompt-driven design where PROMPT-smoke-test.md instructs Copilot CLI to read the README, extract Quick Start code, build, and run) + - What breaks (the prompt says "top level README" but post-migration that's the monorepo README, not README.md) + - The four required changes: update the prompt's README path, ensure README.md has the real content (not the monorepo placeholder), add `working-directory: ./java` to the workflow, and update the reusable workflow reference + - Risk table and verification checklist + +Made changes. + +### 17:55 Prompt + +I consider it essential that the work to implement the plan described in `dd-2989727-move-java-to-monorepo-plan.md` be done in a topic branch in the `upstream` remote for `copilot-sdk-00`. I am also aware that many of the CI/CD aspects require things to be on `main`. Without changing `dd-2989727-move-java-to-monorepo-plan.md` just yet, look at the "Risk Register" section and describe some of the risks of this `main` requirement. Or am I misunderstanding the risk and it's possible to iterate and make changes to CI/CD without requiring stuff reside on `main`? + +### 18:00 Response + +You're right to flag this β€” there is real tension, but it's more nuanced than "everything needs `main`." Let me break down exactly what can and can't be validated on a topic branch. + +## What WORKS on a topic branch (via PR) + +GitHub Actions uses the **PR branch's version** of workflow files for `pull_request`-triggered workflows. So these are fully testable: + +- **`java-sdk-tests.yml`** (the core CI) β€” `pull_request` with path triggers works from a branch +- **codegen-check.yml** changes β€” same, `pull_request` trigger +- **Reusable workflow calls** (`uses: ./.github/workflows/java-smoke-test.yml`) β€” resolved from the PR branch +- **Composite actions** (`uses: .github.`) β€” same +- **All source code** β€” `java/src/`, `java/pom.xml`, test infrastructure +- **`CODEOWNERS`**, **`dependabot.yaml`**, **justfile**, **copilot-instructions.md** β€” take effect on merge + +So the core development loop β€” "does Java build, test, and pass CI in the monorepo" β€” is fully achievable on a topic branch. + +## What CANNOT be tested on a topic branch + +### 1. Smoke test β€” hard-gated on `main` + +Both jobs in run-smoke-test.yml have: +```yaml +if: github.ref == 'refs/heads/main' +``` +On a PR, `github.ref` is `refs/pull/NNN/merge`, so both jobs are **unconditionally skipped**. You cannot validate the smoke test migration works until the workflow is on `main`. This is the most relevant risk from the register given your Appendix C work. + +### 2. `workflow_dispatch`-only workflows + +These can only be manually triggered from branches that **already exist in the repo** and are selectable in the Actions UI. New workflow files on a topic branch won't appear in the dispatch dropdown until they're on the default branch (or at least pushed). The affected workflows: +- `java-publish.yml` +- `java-publish-snapshot.yml` +- `java-deploy-site.yml` +- `java-smoke-test.yml` (when triggered manually) +- update-copilot-dependency.yml + +### 3. Scheduled workflows + +`schedule` triggers **only run from the default branch**. So `java-reference-impl-sync.md` (the daily check for new dotnet/nodejs changes) won't execute until it's on `main`. + +### 4. copilot-setup-steps.yml + +The Copilot coding agent reads this **exclusively from the default branch**. Adding JDK 17 + Maven setup won't take effect for agent sessions until merged to `main`. This means you can't use the coding agent to help with Java tasks in the monorepo until this lands. + +### 5. Required status checks β€” chicken-and-egg + +You can't add `java-sdk-tests` as a required check in branch protection until the workflow has **run at least once** and GitHub knows the check name exists. First run happens on the PR, but you can't enforce it as required until after merge. + +### 6. Secrets availability + +For `pull_request` events, secrets are available for PRs from branches **within the same repo** (not forks). Since this is an internal topic branch on the `upstream` remote, `COPILOT_GITHUB_TOKEN` and other secrets should be accessible. This is **not** a blocker β€” but it's worth verifying the repo's settings haven't restricted secrets to protected branches only. + +### 7. GitHub Pages deployment + +Pages deployment is typically configured for `main`. The `java-deploy-site.yml` workflow can't be validated until on `main`. + +## Practical implication + +The migration is naturally split into what the plan calls Phases 1-2 (source code + core CI) vs. Phases 3-4 (publish, smoke test, agentic workflows). Phases 1-2 are fully testable on a topic branch. Phases 3-4 inherently require `main`. + +The key question is whether you merge Phases 1-2 as one PR and then iterate Phases 3-4 as follow-up PRs on `main`, or whether you try to land everything in one big PR and accept that the `main`-gated pieces are unvalidated until merge. The former is safer β€” a broken smoke test workflow is recoverable, but a broken `java-sdk-tests.yml` that blocks the merge queue is not. + +The risk register items H1 (Maven Central Trusted Publisher), H4 (secret provisioning), and M3 (branch protection) are all in this "can't validate until `main`" category, which is why Phase 0 (pre-flight) exists β€” those should be resolved **before** any code lands. + +### 18:25 Prompt + +Consider this "MEDIUM RISK" item: + +``` +| M1 | **Codegen `package.json` merge** | Java codegen has its own `@github/copilot` dependency; monorepo codegen gets it from `nodejs/node_modules` | Align Java codegen to use the same dependency source. May need to add `generate:java` script to monorepo's `scripts/codegen/package.json`. | +``` + +I am inclined to **not** put the codegen into the monorepo's + +``` +| `.github/workflows/codegen-check.yml` | **Merge** into existing | `.github/workflows/codegen-check.yml` | +``` + +For fear of messing up existing non-Java aspects. What are the pros and cons of keeping this separate? + +### 18:28 Prompt + +Good instinct. This is the same logic as the `just` discussion β€” Java is self-contained and doesn't need to be force-fitted into the existing cross-language machinery. + +## Pros of keeping Java codegen separate (`java-codegen-check.yml`) + +- **Zero blast radius** β€” you cannot break Node/C#/Python/Go/Rust codegen. This is the strongest argument. +- **No JDK in codegen-check.yml** β€” the existing workflow installs Node, Go, and Rust. Adding JDK + Maven for one more language bloats it and slows it for everyone. +- **update-copilot-dependency.yml stays untouched** β€” that workflow runs `npm run generate` and formats for nodejs/dotnet/rust. Keeping Java out means no changes to a workflow that touches all other languages. +- **Independent `@github/copilot` version cadence** β€” Java already lags the reference impl by design (it's ported, not in lockstep). Forcing the same `@github/copilot` version across all codegen could break Java if a schema change hasn't been ported yet. +- **Consistent with Java's independent versioning** β€” Java already has its own publish workflow, its own version scheme. Separate codegen follows the same principle. + +## Cons of keeping it separate + +- **`@github/copilot` version drift** β€” when the monorepo updates to `@github/copilot@1.0.50`, Java might still be on `1.0.45`. This is intentional but could confuse contributors who expect uniformity. Need to document it. +- **Two codegen check workflows** β€” more YAML to maintain, two required status checks instead of one. +- **Java needs its own `update-copilot-dependency` mechanism** β€” the existing workflow won't cover Java, so you need a separate `java-update-copilot-dependency.yml` (or keep the one from `copilot-sdk-java` adapted for monorepo paths). +- **java.ts location is awkward** β€” if the file lives in codegen alongside the others but isn't in the shared `generate` script, it's confusing. If it lives in `java/scripts/codegen/`, it's clear but diverges from the monorepo layout. + +## Recommendation + +Keep it separate. The version-drift con is actually a **feature** β€” it's how the Java SDK already works, and the `reference-impl-sync` mechanism handles catching up deliberately. The `java.ts` location question resolves cleanly: put it in `java/scripts/codegen/` with its own package.json, making the boundary explicit. This also means Appendix A should change from: + +> java.ts β†’ Copy β†’ java.ts + +to: + +> java.ts β†’ Copy β†’ `java/scripts/codegen/java.ts` + +### 18:28 Prompt + +Yes, do change Appendix A. + +### 18:29 Response + +Replacing 2 lines with 2 lines in [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/dd-2989727-move-java-to-monorepo-plan.md) + + +``` + +``` +Done. Both the `java.ts` and its package.json now stay self-contained under `java/scripts/codegen/` instead of merging into the monorepo's shared codegen. + +Made changes. + +### 18:30 Prompt + +Actually, I am now keeping `dd-2989727-move-java-to-monorepo-plan.md` under version control in `.\80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` in `copilot-sdk-00`. Make the changes there. + diff --git a/80-java-monorepo-add-01-remove-before-merge/20260513-prompts.md b/80-java-monorepo-add-01-remove-before-merge/20260513-prompts.md new file mode 100644 index 000000000..b509025e8 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/20260513-prompts.md @@ -0,0 +1,442 @@ +### 14:07 Prompt + +The plan calls for + +``` +6. Update `CODEOWNERS`: + - Add `java/ @github/copilot-sdk-java` +``` + +Every human member of the `copilot-sdk-java` team is already a member of `@github/copilot-sdk-java`. So I assert no further action is necessary on this file in `copilot-sdk-00`. Do you agree? + +### 14:09 Response + +I’m going to inspect the existing `CODEOWNERS` entry and the surrounding repo setup so I can answer your assertion against what the plan item is actually doing. + +Searched for files matching `copilot-sdk-00` Β· `**/CODEOWNERS`, 1 match + +Searched for regex `copilot-sdk-java|java/` (`copilot-sdk-00`), 50 results + +I’ve confirmed the plan item is specifically about path ownership in the monorepo, not about whether the same humans are already covered indirectly; next I’m reading the current CODEOWNERS file itself. + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/CODEOWNERS) + +I don’t agree. + +Right now CODEOWNERS contains only: + +```text +* @github/copilot-sdk +``` + +So adding `java/ @github/copilot-sdk-java` would still change behavior in an important way: it makes java reviews route to the Java team explicitly, rather than relying on broad ownership by `@github/copilot-sdk`. The fact that the human members overlap today doesn’t make the entry redundant, because CODEOWNERS is about repository policy and future-proof routing, not just current membership coincidence. + +Two practical reasons the explicit java entry still matters: +- it documents that java has a distinct ownership boundary inside the monorepo +- it preserves the intended reviewers if team membership diverges later, or if branch protection / required review rules key off CODEOWNERS matches + +So my recommendation is: keep the plan item, unless your actual policy decision is that Java should remain owned by the general monorepo team with no special routing. + +### 14:11 Prompt + +Is this correct: + +The purpose of the `CODEOWNERS` file is to establish an explicit per-path mapping between paths and teams responsible for maintaining the content within those paths. + +### 14:12 Response + +Yes, that’s basically correct. + +A slightly tighter version would be: + +CODEOWNERS establishes explicit ownership rules that map repository paths to users or teams, mainly so GitHub can automatically request reviews and apply code-owner-based branch protection behavior for changes to those paths. + +That wording matters because β€œresponsible for maintaining” is true in a social sense, but the file’s concrete function is operational: review routing and policy enforcement. + +### 14:12 Prompt + +But does it make sense for path entries in `CODEOWNERS` to "reach out" of the current repo in which the `CODEOWNERS` file resides? + +### 14:13 Response + +No. That would not make sense. + +CODEOWNERS only has meaning within the repository that contains it. Its path patterns match paths in that same repo, and its owners must be users or teams that GitHub can resolve for that repo. It does not express ownership for files in some other repository, and it does not β€œreach out” across repo boundaries. + +So if a CODEOWNERS entry in `github/copilot-sdk` says `java/ @github/copilot-sdk-java`, that is still about ownership of the java directory inside `github/copilot-sdk`, not about the separate `github/copilot-sdk-java` repository. The team name may happen to reference the Java maintainers, but the scope of the rule remains local to the monorepo. + +### 14:13 Prompt + +So what is the meaning of the RHS of an entry in `CODEOWNERS`, if it's not a repo identifier? + +### 14:14 Response + +The RHS is an owner specifier, not a repository identifier. + +In GitHub CODEOWNERS, the right-hand side names the users or teams who own matching paths for that repository’s review/policy purposes. Those values are things like: + +- `@username` +- `@org/team-name` +- sometimes an email address, depending on GitHub context, though teams/users are the common case + +So in: + +```text +java/ @github/copilot-sdk-java +``` + +- java means β€œthe java path inside this repository” +- `@github/copilot-sdk-java` means β€œthe GitHub team named `copilot-sdk-java` in the `github` organization” + +That RHS does not point to a repo. It points to a principal GitHub can use as a code owner for matching files in the current repo. + +### 14:15 Prompt + +So how does one browse the teams registry within the `github` organization? + +### 14:15 Response + +If you have the right GitHub org permissions, the usual way is in the GitHub web UI: + +`github` organization page -> `Teams` + +From there you can browse the team list, search by name, and open a team like `copilot-sdk-java`. + +Direct URL shape is usually: + +```text +https://github.com/orgs/github/teams +https://github.com/orgs/github/teams/copilot-sdk-java +``` + +A couple of practical wrinkles: +- whether you can actually view the full team registry depends on your org visibility and permissions +- some teams may be secret or partially hidden, so you might not see everything even inside the org +- for a repo-specific check, the more relevant question is whether `@github/copilot-sdk-java` has access to `github/copilot-sdk`, not just whether the team exists + +If you want, I can help you phrase the exact criterion for the migration plan: β€œteam exists,” β€œteam is visible,” or β€œteam has sufficient repo access.” + +### 14:24 Prompt + +Review the change to this file for syntactic and semantic correctness. + +### 14:25 Response + +I’m going to inspect the current CODEOWNERS diff and the surrounding repo state so I can review both syntax and what the new rule would actually do. + +Read changed files in [](file:///e%3A/workareas/copilot-sdk-00) + +Read [](file:///e%3A/workareas/copilot-sdk-00/.github/CODEOWNERS) + +Findings first: I don’t see a syntax problem in CODEOWNERS. + +Semantically, the new line is correct if your intent is β€œfiles under java in this monorepo should be code-owned by the `@github/copilot-sdk-java` team.” In GitHub CODEOWNERS, the later, more specific rule wins, so: + +```text +* @github/copilot-sdk +java/ @github/copilot-sdk-java +``` + +means the whole repo defaults to `@github/copilot-sdk`, while java is explicitly owned by the Java team. That is valid and coherent. + +Open question / assumption: this only works operationally if `@github/copilot-sdk-java` is a resolvable team and has the necessary access to the `github/copilot-sdk` repository. If that team is missing, hidden in a way GitHub can’t use here, or lacks repo access, the entry can be semantically wrong in practice even though the file syntax is fine. + +Change summary: the edit is syntactically fine and semantically appropriate for a monorepo path-specific ownership rule. The only external dependency is team existence plus repo access. + +### 14:25 Prompt + +I created the `@github/copilot-sdk-java` team as a "child" of `@github/copilot-sdk`. Does that guarantee path access? + +### 14:28 Response + +No, that does not guarantee it. + +A parent/child team relationship in GitHub mainly helps with organization and inherited membership/visibility patterns, but it does not by itself guarantee that the child team is usable as a code owner for a repo unless that team also has appropriate access to that repository. + +For CODEOWNERS, the practical check is still: +- does `@github/copilot-sdk-java` exist as a team in the `github` org? +- does that team have access to the `github/copilot-sdk` repository? +- is the team visible/usable enough for GitHub to resolve it in code owner rules? + +So making `copilot-sdk-java` a child of `copilot-sdk` is suggestive and often helpful administratively, but I would not treat it as proof. The thing you want to verify is explicit repo access on `github/copilot-sdk` for that child team. + +### 14:28 Prompt + +You stated + +> - does `@github/copilot-sdk-java` exist as a team in the `github` org? + +Yes. I created it. + +> - does that team have access to the `github/copilot-sdk` repository? + +Well, I do. But that's probably because I have been added to that repo. How can I make sure the "team" entity has access? + +### 14:29 Response + +You want to check the team-to-repo grant directly, not infer it from your personal access. + +In the GitHub UI, the cleanest path is to open the team and inspect its repositories: +`Organization` -> `Teams` -> `copilot-sdk-java` -> `Repositories` + +If `github/copilot-sdk` appears there, the team has been granted access. If it does not, the team itself does not currently have repo access, even if you personally do. + +The inverse view also works: +`github/copilot-sdk` -> `Settings` -> `Collaborators and teams` + +That page should list `copilot-sdk-java` as a team with a permission level such as `Read`, `Write`, `Maintain`, or `Admin`. For CODEOWNERS, the important thing is that GitHub can resolve the team for that repo; in practice, checking that it appears there is the reliable test. + +So the short answer is: look for the team on the repo’s `Collaborators and teams` page, or look for the repo on the team’s `Repositories` page. That tells you about the team entity, not just you as an individual. + +### 15:01 Prompt + +I am working on https://github.com/github/copilot-sdk-partners/issues/90 now. Use `gh` to inspect this work item. + +I come to the `RELEASE_TOKEN` secret. + +I expect it would be wise to scope this token so its name is `JAVA_RELEASE_TOKEN`. I expct this would require changes in the workflow files in `copilot-sdk-java-00` as they are moved to `copilot-sdk-00`. Inspect the workflow files in the former and tell me what must change to honor `JAVA_RELEASE_TOKEN` instead of the unscoped `RELEASE_TOKEN`. + +### 15:04 Response + +I’ll search for workflow files in copilot-sdk-java-00 that reference `RELEASE_TOKEN` and identify what would need to change to use `JAVA_RELEASE_TOKEN` instead. + +Searched for text `RELEASE_TOKEN` (`copilot-sdk-java-00` Β· `.github/workflows/**`), 1 result + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/publish-maven.yml#30-30), lines 30 to 60 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/publish-maven.yml#1-1), lines 1 to 30 + +The only workflow reference to `RELEASE_TOKEN` is in the publish-maven.yml file, specifically as the `token` for the `actions/checkout` step. + +To switch to `JAVA_RELEASE_TOKEN`, you must: +- Change all instances of `${{ secrets.RELEASE_TOKEN }}` to `${{ secrets.JAVA_RELEASE_TOKEN }}` in publish-maven.yml. + +No other workflow files reference `RELEASE_TOKEN`, so only this change is required. If you move or adapt this workflow into the monorepo, ensure the secret is set as `JAVA_RELEASE_TOKEN` in the repo’s GitHub Actions secrets. + +### 15:12 Prompt + +We must do the same thing with `GPG_SECRET_KEY` and `GPG_PASSPHRASE`. Those should have the `JAVA` prefix. Analyze the workflows in `copilot-sdk-java-00` and list what needs to change. + +### 15:22 Response + +I’ll search for all workflow references to `GPG_SECRET_KEY` and `GPG_PASSPHRASE` in copilot-sdk-java-00 to identify what must be updated to use `JAVA_GPG_SECRET_KEY` and `JAVA_GPG_PASSPHRASE`. + +Searched for text `GPG_SECRET_KEY` (`copilot-sdk-java-00` Β· `.github/workflows/**`), 1 result + +Searched for text `GPG_PASSPHRASE` (`copilot-sdk-java-00` Β· `.github/workflows/**`), 5 results + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/publish-maven.yml#60-60), lines 60 to 180 + +To use `JAVA_GPG_SECRET_KEY` and `JAVA_GPG_PASSPHRASE` instead of the unscoped names, you must: + +- Change all instances of `${{ secrets.GPG_SECRET_KEY }}` to `${{ secrets.JAVA_GPG_SECRET_KEY }}` +- Change all instances of `${{ secrets.GPG_PASSPHRASE }}` to `${{ secrets.JAVA_GPG_PASSPHRASE }}` + +in the following places in publish-maven.yml: +- The `gpg-private-key` input to `actions/checkout` (or any other action using it) +- All `env:` blocks that set `GPG_PASSPHRASE` for Maven commands + +Summary of lines to update: +- Line 62: `gpg-private-key: ${{ secrets.GPG_SECRET_KEY }}` +- Line 160, 170: `GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}` (in two steps) +- Any other direct references to these secrets in the workflow + +No other workflow files reference these secrets, so only this file needs updating. Also, ensure the new secrets are set in the repo’s GitHub Actions secrets. + +### 15:39 Prompt + +Given that I just used this command to obtain the value for the new `JAVA_GPG_SECRET_KEY` secret, how would I correctly archive the full set of information needed to hand off this keypair to another party, assuming full trust. + +### 15:42 Response + +For a complete, transferable handoff of a GPG signing identity, archive all of these items together: + +1. Secret key material +- Export the secret key in armored form. +- If you have subkeys, export the full secret key bundle, not just a single subkey. + +2. Public key +- Export the corresponding public key (armored). +- This lets the recipient re-import and verify identity metadata cleanly. + +3. Fingerprint and key metadata +- Record full fingerprint, key ID, creation date, expiration date, and UID(s). +- Include the expected signing subkey fingerprint if different. + +4. Revocation certificate +- Include the pre-generated revocation certificate. +- This is critical if compromise is later suspected. + +5. Trust/ownertrust data (optional but useful) +- Export ownertrust so recipient can restore trust settings quickly. + +6. Passphrase handling +- If passphrase-protected, provide the passphrase separately from the key archive, even in a full-trust handoff. +- Best practice is two channels (for example: file transfer for key archive, voice/secure chat for passphrase). + +7. Operational notes +- What this key is used for (for example, Maven release signing). +- Where it is expected to be installed (CI secret name, local keyring use, etc.). +- Rotation/expiry policy and who owns lifecycle management. + +Recommended packaging flow: +1. Put exported files plus a README into one folder. +2. Create a tar/zip archive. +3. Encrypt that archive to the recipient (or recipients) with their public key. +4. Sign the encrypted archive with your current signing key. +5. Transfer. +6. Recipient verifies your signature and fingerprint out-of-band before use. + +Minimal file set to include: +- secret-key.asc +- public-key.asc +- fingerprint.txt +- revocation.asc +- ownertrust.txt +- handoff-notes.txt + +If you want, I can draft a concrete Windows PowerShell command sequence for creating this bundle and producing an encrypted handoff artifact end-to-end. + +### 15:45 Prompt + +Write it as a bash script to `80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh` + +### 15:46 Response + +I’m going to check whether that script file already exists, then I’ll create it with an end-to-end export, bundle, encrypt, and sign flow you can run directly. + +Searched for files matching `copilot-sdk-00` Β· `80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh`, no matches + +I’m creating the script now in the requested path with sane defaults, argument validation, and all artifacts needed for a trusted handoff package. + +Created [](file:///e%3A/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh) + +Created the script at ghcpsp-90-gpg-key-archive.sh. + +What it does: +- Validates required tools and arguments +- Exports secret key, public key, ownertrust, fingerprint details +- Includes revocation certificate if found in the standard GnuPG location +- Writes handoff notes with operational guidance +- Produces both: + - plain tar.gz bundle + - armored encrypted+signed bundle for transfer + +Usage: +ghcpsp-90-gpg-key-archive.sh [output-dir] + +If you want, I can also add a companion verify/import script for the receiving party. + +Made changes. + +### 15:59 Prompt + +Consider this secret from the plan file. + +``` +| `COPILOT_GITHUB_TOKEN` | `build-test.yml`, `codegen-agentic-fix.lock.yml` | || Token for Copilot CLI in CI | +``` + +Is there anything special about that secret name? Is it some kind of well-known implicit secret name in the agentic machinery? Or can we safely treat it as "local" and therefore safely scope it to Java? + +### 16:01 Response + +I’ll trace where `COPILOT_GITHUB_TOKEN` is referenced in both repos and check whether it’s treated as a convention or just an ordinary secret wiring choice. + +Searched for text `COPILOT_GITHUB_TOKEN` (`copilot-sdk-00`), 153 results + +Searched for text `COPILOT_GITHUB_TOKEN` (`copilot-sdk-java-00`), 200 results + +I found strong evidence that the *environment variable* `COPILOT_GITHUB_TOKEN` is conventional for the CLI, while the *GitHub secret* name is workflow-configurable; I’ll grab the exact lines so the recommendation is concrete. + +Read [](file:///e%3A/workareas/copilot-sdk-00/docs/auth/authenticate.md#232-232), lines 232 to 312 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/codegen-agentic-fix.lock.yml#120-120), lines 120 to 145 + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/build-test.yml#20-20), lines 20 to 40 + +Short answer: it is not a GitHub Actions implicit secret name, but it is a strong convention in Copilot/agentic workflows. + +What is special: +- The SDK/CLI auth lookup explicitly prioritizes the environment variable COPILOT_GITHUB_TOKEN. See authenticate.md and authenticate.md. +- Agentic lock workflows explicitly validate and wire that name. See codegen-agentic-fix.lock.yml and codegen-agentic-fix.lock.yml. +- Non-agentic Java CI also passes that name through workflow secrets today. See build-test.yml. + +So can you scope the repository secret name to Java? +- Yes, safely, if you keep the runtime environment variable name as COPILOT_GITHUB_TOKEN. +- In practice: map secrets.JAVA_COPILOT_GITHUB_TOKEN into env.COPILOT_GITHUB_TOKEN (or equivalent secret pass-through in reusable workflows). + +Guidance for migration: +1. Treat the GitHub secret name as local/configurable. +2. Preserve COPILOT_GITHUB_TOKEN as the env var presented to CLI/agent steps. +3. For agentic workflows, update source workflow definitions and recompile lock files rather than hand-editing lock files. + +### 16:02 Prompt + +Is that secret name used in `copilot-sdk-00`? + +### 16:03 Response + +Yes. + +`COPILOT_GITHUB_TOKEN` is used extensively in copilot-sdk-00, including in multiple workflow lock files and docs. Examples: + +- handle-question.lock.yml +- handle-question.lock.yml +- release-changelog.lock.yml +- authenticate.md +- README.md + +So in this repo it is definitely an actively used name. + +### 16:03 Prompt + +Given its existing use, I will opt to not change its name or usage during the migration. Does this seem sound? + +### 16:04 Response + +Yes, that is a sound choice. + +Keeping `COPILOT_GITHUB_TOKEN` unchanged during migration is low risk because: +- it is already widely used in `copilot-sdk-00` +- it matches existing agentic workflow conventions +- it avoids churn across workflow source + lock files + docs at the same time as the Java move + +This is a good β€œstabilize first, rename later if needed” decision. + +### 16:25 Prompt + +I'm back now to `80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh` + +Please do write the script that the recipient must run to bring the archived key into their environment. Name it appropriately. + +### 16:27 Response + +I’m going to inspect the archive script you already have, then I’ll add a matching recipient-side import script in the same folder that verifies, decrypts, extracts, and imports the key material safely. + +Read [](file:///e%3A/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh) + +I’m adding a companion recipient script now that validates signature/decryption status, checks the expected fingerprint, imports keys, and optionally imports ownertrust. + +Created [](file:///e%3A/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-import.sh) + +Implemented. I added the recipient-side script at ghcpsp-90-gpg-key-import.sh. + +What it does: +- Decrypts and verifies the signed archive with GPG +- Confirms signature validity via GPG status output +- Extracts the bundle safely +- Parses and reports the expected fingerprint from bundle metadata +- Imports public key, then secret key +- Optionally imports ownertrust when requested +- Verifies imported secret key fingerprint matches metadata + +Usage: +- ghcpsp-90-gpg-key-import.sh +- ghcpsp-90-gpg-key-import.sh +- ghcpsp-90-gpg-key-import.sh --import-ownertrust + +Natural next step: +1. Add execute permissions to both scripts if needed and run a dry test with a non-production keypair. + +Made changes. diff --git a/80-java-monorepo-add-01-remove-before-merge/20260515-prompts.md b/80-java-monorepo-add-01-remove-before-merge/20260515-prompts.md new file mode 100644 index 000000000..6d5e12148 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/20260515-prompts.md @@ -0,0 +1,536 @@ +### 10:11 Prompt + +Read the `80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` and the two `*-prompts.md` to get the context of what we've been working on. + +My current problem is the branch protection rules as tracked in this text in the plan. + +```markdown +- [βŒ› ] **Confirm branch protection** β€” will new required status checks be accepted? See https://github.com/github/copilot-sdk-partners/issues/95 . +``` + +The maintainer has this question: + +--- + +## @edburns Can you clarify why we need an action to be able to push directly to main rather than having the action produce a PR? + +I know there is a good answer for this, but I can't easily articulate it. Can you help me? + +### 10:16 Response + +The workflow that needs direct-to-main push is `java-publish.yml` (adapted from `publish-maven.yml`), specifically because of how `maven-release-plugin` works. + +Here's the articulation: + +--- + +**Why `maven-release-plugin` requires direct push to `main`:** + +The Maven Release Plugin performs an **atomic, multi-step commit sequence** as part of its `release:prepare` + `release:perform` lifecycle: + +1. Removes `-SNAPSHOT` from pom.xml version β†’ commits as "prepare release vX.Y.Z" +2. Creates a git tag `vX.Y.Z` pointing at that exact commit +3. Bumps pom.xml to next `-SNAPSHOT` version β†’ commits as "prepare for next development iteration" +4. Pushes both commits + tag in one operation +5. `release:perform` then checks out the tag and deploys the artifact to Maven Central + +**Why a PR doesn't work here:** + +- The **tag must point to the exact commit** with the non-SNAPSHOT version on `main`. If that commit lives on a PR branch, the tag references a commit that isn't on the default branch β€” Maven Central metadata and `` URLs become inconsistent. +- A PR introduces a **race condition**: other commits could merge between the release commit and the SNAPSHOT-bump commit, breaking the plugin's assumption of consecutive commits. +- There is **no human decision left to make** β€” the decision to release was already made when someone triggered the `workflow_dispatch`. The commits are mechanical version bookkeeping, not code changes requiring review. +- The entire operation is **idempotent and reversible** (revert the two commits + delete the tag) if something goes wrong. + +This is the standard pattern used by virtually all Maven projects that release via CI (Spring, Apache, Quarkus, etc.). The alternative β€” manually managing versions and tags outside the plugin β€” defeats the purpose of using `maven-release-plugin` and introduces more error-prone manual steps. + +--- + +Want me to draft this as a comment you can post directly on the issue? + +### 10:17 Prompt + +Yes. + +### 10:18 Response + +Read [](file:///e%3A/workareas/copilot-sdk-java-00/.github/workflows/publish-maven.yml#1-1), lines 1 to 180 + +Here's a comment you can post: + +--- + +The workflow that needs this is `java-publish.yml` (the Maven release workflow). It uses `maven-release-plugin`, which performs an atomic multi-commit sequence directly on `main`: + +1. Removes `-SNAPSHOT` from pom.xml β†’ commits `[maven-release-plugin] prepare release v1.2.3` +2. Creates git tag `v1.2.3` pointing at that exact commit +3. Bumps pom.xml to next `-SNAPSHOT` β†’ commits `[maven-release-plugin] prepare for next development iteration` +4. Pushes both commits + tag in one operation +5. Checks out the tag and deploys the signed artifact to Maven Central + +**Why a PR can't substitute here:** + +- **The tag must point to a commit on `main`.** Maven Central's POM metadata, `` URLs, and reproducibility guarantees all assume the tagged commit is on the default branch. A tag pointing at a PR branch commit breaks downstream tooling (Dependabot, IDE source attachment, `mvn scm:changelog`). +- **The two commits must be consecutive.** If a PR is opened between the "set release version" commit and the "set next SNAPSHOT" commit, other PRs could merge in between, meaning `main` would contain a non-SNAPSHOT pom.xml that doesn't correspond to any published artifact β€” a broken state. +- **There's no remaining human decision.** The release was already authorized by triggering `workflow_dispatch`. The commits are mechanical version bookkeeping, not code changes requiring review. +- **The workflow already has rollback.** If anything fails, it reverts the documentation commit and runs `mvn release:rollback` (lines 168–177 of the current publish-maven.yml). + +This is the standard pattern used by Maven projects that release via CI (Spring, Apache, Quarkus, etc.). The `JAVA_RELEASE_TOKEN` PAT with `contents:write` scope is specifically for this use case. + +Note: only this one workflow (`java-publish.yml`) needs the bypass. All other Java workflows (`java-sdk-tests.yml`, `java-smoke-test.yml`, `java-codegen-fix.md`, etc.) operate via normal PRs. + +--- + +### 10:21 Prompt + +That's close, but let's take the ADR in `docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md` into account regarding tag names. + +You stated: + +> 2. Creates git tag `v1.2.3` pointing at that exact commit + +In our case, per the ADR, the tag would be `v1.2.3-java.0`. To avoid confusion, rewrite your post-ready response using this tag name wherever you need a tag. + +### 10:24 Response + +Here's the revised comment: + +--- + +The workflow that needs this is `java-publish.yml` (the Maven release workflow). It uses `maven-release-plugin`, which performs an atomic multi-commit sequence directly on `main`: + +1. Removes `-SNAPSHOT` from pom.xml β†’ commits `[maven-release-plugin] prepare release v1.2.3-java.0` +2. Creates git tag `v1.2.3-java.0` pointing at that exact commit. See the [ADR](https://github.com/github/copilot-sdk-java/blob/main/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md). +3. Bumps pom.xml to next `-SNAPSHOT` β†’ commits `[maven-release-plugin] prepare for next development iteration` +4. Pushes both commits + tag in one operation +5. Checks out the tag and deploys the signed artifact to Maven Central + +**Why a PR can't substitute here:** + +- **The tag must point to a commit on `main`.** Maven Central's POM metadata, `` URLs, and reproducibility guarantees all assume the tagged commit is on the default branch. A tag pointing at a PR branch commit breaks downstream tooling (Dependabot, IDE source attachment, `mvn scm:changelog`). +- **The two commits must be consecutive.** If a PR is opened between the "set release version" commit and the "set next SNAPSHOT" commit, other PRs could merge in between, meaning `main` would contain a non-SNAPSHOT pom.xml that doesn't correspond to any published artifact β€” a broken state. +- **There's no remaining human decision.** The release was already authorized by triggering `workflow_dispatch`. The commits are mechanical version bookkeeping, not code changes requiring review. +- **The workflow already has rollback.** If anything fails, it reverts the documentation commit and runs `mvn release:rollback`. + +This is the standard pattern used by Maven projects that release via CI (Spring, Apache, Quarkus, etc.). The `JAVA_RELEASE_TOKEN` PAT with `contents:write` scope is specifically for this use case. + +Note: only this one workflow (`java-publish.yml`) needs the bypass. All other Java workflows (`java-sdk-tests.yml`, `java-smoke-test.yml`, `java-codegen-fix.md`, etc.) operate via normal PRs. + +--- + +### 13:57 Prompt + +The code in `copilot-sdk-java-00` has been updated. In particular there have been changes to the workflows there. I am aware we have copied over at least one workflow in the process of performing Phase 0 work according to the plan. + +Examine the recent workflow changes in `copilot-sdk-java-00` and make the corresponding Java changes to keep the `copilot-sdk-00` side up to date. + +## ghcp-sp-95-branch-protection + +Consider this text from the plan. + +```markdown +- [βŒ› ] **Confirm branch protection** β€” will new required status checks be accepted? See https://github.com/github/copilot-sdk-partners/issues/95 . +``` + +Use the `gh` CLI to look at this comment on the issue: https://github.com/github/copilot-sdk-partners/issues/95#issuecomment-4462156144 + +The last statement in that comment is this: + +> Steve requested I look into the cost of doing the trade-off item 4 and if the cost is deemed too high, we can do it after GA. + +It is now time to do this investigation. I suspect what Steve was told by his agent isn't quite the full story. I suspect plenty of projects still use `maven-release-plugin`, but do want to follow current best practices. + +One aspect that I want to be clear on: this SDK is much smaller than Quarkus or Spring. Is there evidence to support the assertion that projects of this size still do use `maven-release-plugin` even in 2026-05? Or, is it the case that even small projects have abandoned in favor of other options. + +Finally, what about the use of the `central-publishing-maven-plugin`? Does that need to be considered in this branch protection conversation? + +Write up your findings in a file `ghcp-sp-95-branch-protection-findings-01.md` as a sibling to the plan file. + +### 14:43 Response + +Understood β€” focusing purely on the branch protection investigation. Let me gather one more piece of evidence on `maven-release-plugin` activity before writing the findings. + +Fetched https://central.sonatype.com/artifact/org.apache.maven.plugins/maven-release-plugin/versions + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md) + +Good β€” `maven-release-plugin` v3.3.1 was released December 2025, very actively maintained. Now let me write the findings document. + +Created [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-branch-protection-findings-01.md) + +Created ghcp-sp-95-branch-protection-findings-01.md. + +Key findings: + +- **`maven-release-plugin` is not legacy** β€” v3.3.1 released Dec 2025, three releases in two months. The agent's characterization was inaccurate. +- **Small projects still use it** β€” Apache's own Maven plugins and commons libraries use it. The "move away" trend is specific to large multi-module monorepos (Spring uses Gradle anyway). +- **CI-friendly alternative costs ~2 days** β€” complete workflow rewrite, `flatten-maven-plugin` addition, and testing against Maven Central staging. Doable but adds risk during migration. +- **`central-publishing-maven-plugin` is orthogonal** β€” it works with either approach and doesn't affect the branch-protection question. +- **Recommendation:** Keep `maven-release-plugin` for GA, defer CI-friendly migration to post-GA. The bypass is scoped to a single `workflow_dispatch`-triggered workflow. + +Made changes. + +### 14:47 Prompt + +I want to spice up the analysis with some usage data. Can you find some evidence that downloads of `maven-release-plugin` have been recently high and sustained? + +## Define freeze policy + +### 15:03 Prompt + +Phase 0 states: + +```markdown +- [ ] **Define Freeze window policy** β€” declare a short freeze window in `copilot-sdk-java` to avoid merge conflicts during migration +``` + +I'm very loath to freeze agentic syncing. How hard would it be to adopt a policy of manually syncing any changes that come in to `copilot-sdk-java-00` during the migration? I'm pretty much the only committer, and the agentic sync workflow is the only one whot that runs regularly and pushes commits. I could change that to once a week on Friday and probably be ok. + +### 15:06 Response + +This is very practical given your situation. The risk calculus: + +**Why it's easy:** + +- You're the sole human committer β€” no surprise PRs to worry about +- Reference-impl-sync changes are predictable: generated code under generated, .lastmerge, package.json, and occasionally hand-written Java adjustments +- Any drift is trivially detectable: `git log --oneline copilot-sdk-java-00/main` tells you exactly what landed +- Phase 1 (copy source) is the only phase truly sensitive to drift β€” Phases 2-4 create _new_ monorepo files that don't conflict + +**Suggested policy instead of a freeze:** + +1. Reduce reference-impl-sync to weekly/Friday (as you said) +2. Do Phase 1 copy early in the week, right after verifying java-00 main is clean +3. After each sync lands on Friday, forward-port the diff into `copilot-sdk-00/java/` β€” a straightforward `git diff PREV_HASH..NEW_HASH` in java-00 applied to the monorepo +4. Once Phase 1 merges and the monorepo is the source of truth, disable the sync workflow entirely + +**The one thing to watch:** if a sync lands _during_ an active Phase 1 PR review, you'd need to rebase. But since you control the sync schedule, just don't trigger it while a PR is open. + +A hard freeze is overkill here. Want me to update the plan to reflect this policy? + +### 15:06 Prompt + +Yes. + +## dd-2997995-phase-01 + +### 15:20 Prompt + +The plan states + +```markdown +- `instructions/` β†’ `java/instructions/` (or merge into monorepo copilot-instructions) +``` + +When I look in the `copilot-sdk-00` I observe there is a SKILL for rust, but I don't see any language specific instructions. Am I missing something? Produce a plan for doing the little sub-step of your Phase 01 here. + +### 15:29 Response + +Explore instruction/skill structure + +Searched for files matching `copilot-sdk-00` Β· `**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-java-00` Β· `**/*.instructions.md`, 1 match + +Searched for files matching `copilot-sdk-00` Β· `**/.copilot`, no matches + +Searched for files matching `copilot-sdk-java-00` Β· `**/.copilot`, no matches + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/agents) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/agents) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills/rust-coding-skill) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/agentic-merge-reference-impl) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/commit-as-pull-request) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/documentation-coverage) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/nodejs) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/python) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/go) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/dotnet) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/rust) + +Searched for files matching `copilot-sdk-00` Β· `*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `nodejs/**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `python/**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `go/**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `dotnet/**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `rust/**/*.instructions.md`, no matches + +Searched for files matching `copilot-sdk-00` Β· `**/instructions`, no matches + +Searched for files matching `copilot-sdk-java-00` Β· `**/instructions`, no matches + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md#1-1), lines 1 to 300 + +Read skill [rust-coding-skill](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills/rust-coding-skill/SKILL.md?vscodeLinkType%3Dskill#1-1), lines 1 to 100 + +Read skill [agentic-merge-reference-impl](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/agentic-merge-reference-impl/SKILL.md?vscodeLinkType%3Dskill) + +Read skill [commit-as-pull-request](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/commit-as-pull-request/SKILL.md?vscodeLinkType%3Dskill) + +Read skill [documentation-coverage](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/skills/documentation-coverage/SKILL.md?vscodeLinkType%3Dskill) + +Read skill [rust-coding-skill](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills/rust-coding-skill/SKILL.md?vscodeLinkType%3Dskill) + +Read skill `rust-coding-skill`: [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills/rust-coding-skill/examples.md#1-1), lines 1 to 150 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/agents/agentic-workflows.agent.md#1-1), lines 1 to 50 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/agents/agentic-workflows.agent.md#1-1), lines 1 to 50 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md#300-300), lines 300 to 600 + +Read [](file:///c%3A/Users/edburns/AppData/Roaming/Code%20-%20Insiders/User/prompts) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md#600-600), lines 600 to 757 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/prompts) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/prompts) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/actions) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/actions) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/aw) + +Searched for files matching `copilot-sdk-00` Β· `**/*.prompt.md`, no matches + +Searched for files matching `copilot-sdk-java-00` Β· `**/*.prompt.md`, 4 matches + +Searched for files matching `copilot-sdk-00` Β· `**/*.agent.md`, 2 matches + +Searched for files matching `copilot-sdk-java-00` Β· `**/*.agent.md`, 1 match + +Searched for regex `language.*instruction|instructions.*language|rust.*skill|coding.*skill` (`copilot-sdk-00` Β· `.github/copilot-instructions.md`), no results + +Searched for regex `language.*instruction|instructions.*language|skill|agent` (`copilot-sdk-java-00` Β· `.github/copilot-instructions.md`), 13 results + +Read [](file:///c%3A/Users/edburns/AppData/Roaming/Code%20-%20Insiders/User/workspaceStorage/42e680be608c5e153963c50a3611d619/GitHub.copilot-chat/chat-session-resources/7b272067-59c1-463a-b094-300b246bfc71/toolu_vrtx_017hNdUf5o7v8yju8ZR3EbQ1__vscode-1778880555537/content.txt#1-1), lines 1 to 200 + +You're correct β€” the monorepo has no per-language instructions directories. The pattern it uses instead is **skills**. The `rust-coding-skill` at rust-coding-skill serves the same purpose as Java's copilot-sdk-java.instructions.md β€” it provides language-specific coding rules and API patterns. + +Here's the sub-plan: + +## Plan: Migrate copilot-sdk-java.instructions.md + +### Current State + +| Repo | Mechanism | Content | +| ------------ | -------------------------------- | -------------------------------------------------------------------------- | +| **Monorepo** | SKILL.md | Rust-specific coding rules, API patterns, idioms (~300 lines) | +| **Monorepo** | copilot-instructions.md | Cross-language repo governance (build commands, architecture, conventions) | +| **Java SDK** | copilot-sdk-java.instructions.md | Java-specific coding rules, API usage patterns (~1400 lines) | +| **Java SDK** | copilot-instructions.md | Java repo governance (build commands, architecture, conventions) | + +### Approach: Create a `java-coding-skill` (follows the Rust pattern) + +**Do NOT** put it at `java/instructions/` β€” that doesn't match the monorepo convention. +**Do NOT** merge 1400 lines into copilot-instructions.md β€” that would bloat the central file. + +**Instead**, create `.github/skills/java-coding-skill/SKILL.md` following the `rust-coding-skill` pattern. + +### Steps + +1. **Create** `.github/skills/java-coding-skill/SKILL.md` + - Add YAML frontmatter with `name: java-coding-skill` and a description like `"Use this skill whenever editing *.java files in the java/ SDK..."` + - Body = content from copilot-sdk-java.instructions.md + +2. **Merge Java repo governance** into monorepo's copilot-instructions.md + - The Java SDK's copilot-instructions.md contains build commands (`mvn clean verify`, `mvn spotless:apply`), architecture notes (package structure, handler patterns), and test conventions. + - Add a `## Java` section to the monorepo's instructions file (paralleling how it already covers Node, Python, Go, .NET, Rust). + - Keep it concise β€” the detailed API patterns live in the skill. + +3. **Do NOT copy** instructions as a directory to `java/instructions/` β€” the monorepo doesn't use that pattern. + +### Result + +``` +.github/ + β”œβ”€β”€ copilot-instructions.md # Add Java section (build, test, conventions) + β”œβ”€β”€ skills/ + β”‚ β”œβ”€β”€ rust-coding-skill/ + β”‚ β”‚ β”œβ”€β”€ SKILL.md + β”‚ β”‚ └── examples.md + β”‚ └── java-coding-skill/ # NEW + β”‚ └── SKILL.md # From instructions/copilot-sdk-java.instructions.md +``` + +### What This Means for the Plan + +The line in Phase 1: + +> instructions β†’ `java/instructions/` (or merge into monorepo copilot-instructions) + +Should be updated to: + +> copilot-sdk-java.instructions.md β†’ `.github/skills/java-coding-skill/SKILL.md` (follows the `rust-coding-skill` pattern); Java repo governance merged into copilot-instructions.md + +Want me to execute this, or just update the plan text? + +### 15:32 Prompt + +Update the plan first. + +### 15:34 Response + +Compacted conversation + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md#1-1), lines 1 to 120 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md#120-120), lines 120 to 200 + +Let me also check the appendix for any related references. + +Searched for text `instructions` (`**/dd-2989727-move-java-to-monorepo-plan.md`), 20 results + +Let me read the relevant sections around the other references: + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md#365-365), lines 365 to 380 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md#445-445), lines 445 to 465 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md#500-500), lines 500 to 520 + +Now I have the full picture. There are four places to update: + +1. **Line 49** (Phase 1): instructions β†’ `java/instructions/` +2. **Line 373** (Β§6A table): `Move to java/` +3. **Line 456** (checklist): `copilot-instructions.md includes Java guidance` +4. **Line ~516** (Appendix A): instructions β†’ `java/instructions/` + +Replacing 4 lines with 5 lines in [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md) + +``` + +``` + +Updated all four references in the plan: + +1. **Phase 1 (line 49):** instructions β†’ now points to `.github/skills/java-coding-skill/SKILL.md` +2. **Β§6A inventory table (line 373):** "Move to java" β†’ "Create `.github/skills/java-coding-skill/SKILL.md`" +3. **Verification checklist (line 456):** Split into two items β€” one for copilot-instructions.md governance section, one for the new skill file +4. **Appendix A (line ~516):** "Move β†’ `java/instructions/`" β†’ "Create skill β†’ `.github/skills/java-coding-skill/SKILL.md`" + +Made changes. + +### 15:37 Prompt + +Now that you have updated the plan file, produce a net-new, very focused plan just for this part: making the monorepo smart about Java, while following all conventions. This plan will be executed by a separate agent to do the work, so include everything an agent could possibly need into this prompt. Write the prompt as a sibling to the plan file, but call it `dd-2998002-make-the-monorepo-smart-about-java-plan.md`. Include checkboxes so the agent can follow along and not get confused and lose context. Be explicit about the goals. + +### 15:43 Response + +Let me gather the exact source materials the agent plan needs to reference. + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md#1-1), lines 1 to 30 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/copilot-instructions.md) + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/copilot-instructions.md#1-1), lines 1 to 80 + +Read skill [rust-coding-skill](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/.github/skills/rust-coding-skill/SKILL.md?vscodeLinkType%3Dskill#1-1), lines 1 to 15 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/.github/copilot-instructions.md#80-80), lines 80 to 260 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md) + +Now I have all the source materials. Let me create the focused plan document. + +Created [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md) + +Created dd-2998002-make-the-monorepo-smart-about-java-plan.md. It includes: + +- **Explicit goals** β€” two deliverables (skill file + governance merge) +- **Non-goals** β€” what NOT to do (no `java/instructions/` directory, no other language changes) +- **7-step checklist** with checkboxes for the agent to follow +- **Exact YAML frontmatter** to use (matching the `rust-coding-skill` pattern) +- **Precise edit instructions** for each section of copilot-instructions.md (6a–6f) +- **Verification step** with 4 confirmation checks +- **Appendix A** β€” describes the Java instructions source file structure (757 lines, 22 sections) +- **Appendix B** β€” describes the Java governance content to merge, with guidance on what belongs in governance vs. the skill + +Made changes. + +### 15:44 Prompt + +I intend to use Copilot CLI `copilot --yolo` and ask it to execute the plan as a prompt. It will need to have access to both the monorepo and the java. When I interact with you here in VS Code, that comes by virtue of **Add folder to workspace**. How do I do that with Copilot CLI? + +### 15:45 Response + +Copilot CLI doesn't have a multi-root workspace concept. It operates from whatever directory you `cd` into. Two practical options: + +**Option 1 (recommended): Run from the monorepo, reference Java repo by relative path** + +```powershell +cd c:\Users\edburns\workareas\copilot-sdk-00 +copilot --yolo +``` + +Then in your prompt, tell it: + +> The Java SDK source repo is at copilot-sdk-java-00. Read copilot-sdk-java.instructions.md for the source content. + +This way the monorepo's copilot-instructions.md is automatically discovered (it's in the git root), and the agent can still read files from the sibling repo via copilot-sdk-java-00. + +**Option 2: Run from the common parent** + +```powershell +cd c:\Users\edburns\workareas +copilot --yolo +``` + +Both repos are accessible as `copilot-sdk-00/` and `copilot-sdk-java-00/`, but **neither repo's copilot-instructions.md will be auto-loaded** since the parent isn't a git repo. + +**I'd go with Option 1** β€” the monorepo is where all changes land, and the plan document already tells the agent exactly which files to read from the Java repo. You might want to update the plan's Step 2 to explicitly say copilot-sdk-java.instructions.md so the path is unambiguous. + +### 15:46 Prompt + +Yes, use the relative path option. + +Please update the plan to use the relative path thing. Assume `../copilot-sdk-java-00` is the relative path from the `copilot-sdk-00` directory in which I will run copilot --yolo. + +### 15:47 Response + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md#1-1), lines 1 to 60 + +Read [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md#55-55), lines 55 to 120 + +Now I have the full picture. The references to update are in Steps 2 and 5 where the plan tells the agent where to find files from the Java repo. + +Replacing 21 lines with 22 lines in [](file:///c%3A/Users/edburns/workareas/copilot-sdk-00/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md) + +``` + +``` + +Updated the plan with four changes: + +1. **Added "Execution Context" section** β€” tells the agent it's running from the monorepo root and the Java repo is at copilot-sdk-java-00 (read-only) +2. **Step 2** β€” source file path is now copilot-sdk-java.instructions.md +3. **Step 3 body content** β€” references the same copilot-sdk-java-00 path +4. **Step 5** β€” governance file path is now copilot-instructions.md + +Made changes. diff --git a/80-java-monorepo-add-01-remove-before-merge/20260518-prompts.md b/80-java-monorepo-add-01-remove-before-merge/20260518-prompts.md new file mode 100644 index 000000000..dc5d012ae --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/20260518-prompts.md @@ -0,0 +1,186 @@ +## ghcp-sp-95 + +### 10:44 Prompt + +In issue 95, you stated exactly this as the remedy for the branch protection problem. + +```bash +gh api repos/github/copilot-sdk/rulesets \ + -X POST \ + --input - <<'EOF' +{ + "name": "ghcp-sp-95-java-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ], + "rules": [ + { "type": "deletion" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + }, + { "type": "non_fast_forward" } + ] +} +EOF +``` + +I have approval from the stakeholders to do this. But because I'm very new to the repo and want to protect my reputation as a careful and risk-averse engineer, I want to know exactly how I can revert exactly this change, should problems arise. + +Can you give me the command that will effectively "undo" the operation? + +### 10:48 Response + +The POST will return a JSON response containing an `id` field. To undo, you delete that specific ruleset: + +```bash +gh api repos/github/copilot-sdk/rulesets/ -X DELETE +``` + +### Practical workflow: + +1. **When you create it**, capture the ID from the response: + + ```bash + gh api repos/github/copilot-sdk/rulesets \ + -X POST \ + --input - <<'EOF' + ... (your payload) ... + EOF + ``` + + The response JSON will include `"id": 12345678`. Note that number. + +2. **If you need to revert:** + + ```bash + gh api repos/github/copilot-sdk/rulesets/12345678 -X DELETE + ``` + +3. **If you lose the ID**, you can find it: + ```bash + gh api repos/github/copilot-sdk/rulesets --jq '.[] | select(.name == "Protect main") | .id' + ``` + +The DELETE removes the ruleset entirely β€” returning the repo to the state before you created it (assuming step 1 from the issue, deleting the classic branch protection, has already been done separately). There is no partial state; it's a clean removal. + +### 10:50 Prompt + +Here is the post again. I've changed the "name": + +```bash +gh api repos/github/copilot-sdk/rulesets \ + -X POST \ + --input - <<'EOF' +{ + "name": "ghcp-sp-95-java-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ], + "rules": [ + { "type": "deletion" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + }, + { "type": "non_fast_forward" } + ] +} +EOF +``` + +Can you please write a PowerShell script I can execute that does the command and make s it so I can capture the necessary response? Write it to `temporary-prompts/ghcp-sp-95-enable-java-branch-protection.ps1`. + +### 10:55 Response + +Created [](file:///e%3A/workareas/copilot-sdk-java-00/temporary-prompts/ghcp-sp-95-enable-java-branch-protection.ps1) + +Created at ghcp-sp-95-enable-java-branch-protection.ps1. It pipes the JSON payload to `gh api`, parses the response to extract the ruleset ID, and prints the exact DELETE command you'd need to revert. + +Made changes. + +### 10:54 Prompt + +Here is the output + +``` +Ruleset created successfully. + Name: ghcp-sp-95-java-branch-protection + ID: 16552308 + +To revert this change: + gh api repos/github/copilot-sdk/rulesets/16552308 -X DELETE +``` + +### 20:00 Response + +Phase 1 is complete. Here's a summary: + +Phase 1 Complete βœ… + +Two commits made on edburns/80-java-monorepo-add-01: + +1. 446547c β€” Copied all Java SDK source files into java/ (src, pom.xml, config, scripts, docs, mvnw, etc.) +2. f0aebe7 β€” Updated pom.xml to use local monorepo test harness instead of git clone: + - Removed antrun git-clone plugin entirely + - Changed copilot.sdk.clone.dir to ${project.basedir}/.. (monorepo root) + - Updated URLs to github/copilot-sdk + - Updated comments to reflect monorepo layout + +Verification: mvn clean verify passes β€” BUILD SUCCESS (9 min, all tests green). + +## dd-2989720-move-to-monorepo + +The file `80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` is a high-level plan for migrating the code in `..\copilot-sdk-java-00` (source), which is a local clone of https://github.com/github/copilot-sdk-java , to reside in the `java` directory within `copilot-sdk-00` (dest), which is a local clone of https://github.com/github/copilot-sdk . + +Read the whole thing now. + +Next, I want the agent to handle "Phase 1: Copy Source Code (No Workflows Yet)". Note that the markdown strikethrough steps have already been completed. + +Next, write out a fully detailed prompt sufficient to feed to an agent with `copilot --yolo` to instruct the agent to do Phase 1. + +- βœ…βœ…βœ… Assume copilot will be invoked in `copilot-sdk-00` (dest) and that `..\copilot-sdk-java-00` is the correct relative path for the source. +- βœ…βœ…βœ… Tell the agent about the paths. +- βœ…βœ…βœ… Tell the agent it is safe to do the work in the current topic branch, making fine-grained commits with reasonable commit log messages. +- Tell the agent to read `80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` first. +- ❌❌❌ Tell the agent to not do any other phases. + +Write the prompt to the file `80-java-monorepo-add-01-remove-before-merge\dd-2997995.md`. diff --git a/80-java-monorepo-add-01-remove-before-merge/20260519-prompts.md b/80-java-monorepo-add-01-remove-before-merge/20260519-prompts.md new file mode 100644 index 000000000..12c7a20f0 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/20260519-prompts.md @@ -0,0 +1,27 @@ +# DAY: 2026-05-19 + +## dd-2989720-move-to-monorepo + +### 14:00 Prompt + +The file `80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` is a high-level plan for migrating the code in `..\copilot-sdk-java-00` (source), which is a local clone of https://github.com/github/copilot-sdk-java , to reside in the `java` directory within `copilot-sdk-00` (dest), which is a local clone of https://github.com/github/copilot-sdk . + +Read the whole thing now. + +Next, I want the agent to handle "Phase 2: CI Workflows". Note that the Phase 0 and Phase 1 are already complete. + +Next, write out a fully detailed prompt sufficient to feed to an agent with `copilot --yolo` to instruct the agent to do Phase 2. + +- βœ…βœ…βœ… Assume copilot will be invoked in `copilot-sdk-00` (dest) and that `..\copilot-sdk-java-00` is the correct relative path for the source. +- βœ…βœ…βœ… Tell the agent about the paths. +- βœ…βœ…βœ… Tell the agent it is safe to do the work in the current topic branch, making fine-grained commits with reasonable commit log messages. +- Tell the agent to read `80-java-monorepo-add-01-remove-before-merge\dd-2989727-move-java-to-monorepo-plan.md` first. +- ❌❌❌ Tell the agent to not do any other phases. + +Write the prompt to the file `80-java-monorepo-add-01-remove-before-merge\dd-3001051-phase-2-plan.md`. + +### 14:07 Response + +Created `dd-3001051-phase-2-plan.md` with the full Phase 2 agent prompt. It covers all 5 tasks (java-sdk-tests.yml, codegen-check.yml merge, java-codegen-fix.md, copilot-setup-steps.yml merge, dependabot.yaml update) with specific paths, requirements, and constraints scoped to Phase 2 only. + +Made changes. diff --git a/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md b/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md new file mode 100644 index 000000000..74506ff43 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md @@ -0,0 +1,639 @@ +# DD-2989727: Move Java SDK into copilot-sdk Monorepo β€” Plan + +## Table of Contents + +1. [Migration Plan β€” Phases](#1-migration-plan--phases) +2. [Permissions and Secrets Challenges](#2-permissions-and-secrets-challenges) +3. [Naming Convention Proposal](#3-naming-convention-proposal) +4. [Current Language Separation Assessment](#4-current-language-separation-assessment)5. [Workflow Inventory Tables](#5-workflow-inventory-tables) +5. [Agents, Skills, Prompts, and Supporting Resources Inventory](#6-agents-skills-prompts-and-supporting-resources-inventory) +6. [Pitfalls and Risk Register](#7-pitfalls-and-risk-register) +7. [Post-Migration Verification Checklist](#8-post-migration-verification-checklist) + +- [Appendix A: Files to Copy vs. Merge vs. Delete](#appendix-a-files-to-copy-vs-merge-vs-delete) +- [Appendix B: Unique Java Concerns vs Other Languages](#appendix-b-unique-java-concerns-vs-other-languages) +- [Appendix C: Java Smoketest](#appendix-c-java-smoketest) + +--- + +## 1. Migration Plan β€” Phases + +### Phase 0: βœ… Pre-Flight (Before Writing Any Code) + +- [βœ…] **Provision secrets** in `github/copilot-sdk` (see Β§2A) See https://github.com/github/copilot-sdk-partners/issues/90 +- [βœ…] **Verify CODEOWNERS team** access. See https://github.com/github/copilot-sdk-partners/issues/89 +- [βœ…] **Check Maven Central Trusted Publisher** β€” can `github/copilot-sdk` publish to `com.github:copilot-sdk-java`? See +- [βœ…] **Check GitHub Pages** β€” is it enabled? Can Java docs coexist? See https://github.com/github/copilot-sdk-partners/issues/85 +- [βœ… ] **Confirm branch protection** β€” will new required status checks be accepted? See https://github.com/github/copilot-sdk-partners/issues/95 . +- [βœ…] **Create tracking issue** in `github/copilot-sdk` for this migration. See https://github.co/github/copilot-sdk-partners/issues/80 +- [βœ…] **Define drift-management policy** β€” instead of a hard freeze, adopt a manual forward-port policy: + 1. Reduce `reference-impl-sync` schedule in `copilot-sdk-java` to weekly (Fridays only) + 2. Perform Phase 1 copy early in the week, right after verifying `copilot-sdk-java` main is clean + 3. After each Friday sync lands, forward-port the diff into `copilot-sdk/java/` (`git diff PREV..NEW` in java repo, applied to monorepo) + 4. Once Phase 1 merges and the monorepo is the source of truth, disable the sync workflow in `copilot-sdk-java` entirely + - **Rationale:** A hard freeze is unnecessary because (a) there is a single human committer, (b) the only automated commit source is the reference-impl-sync workflow whose schedule is controllable, and (c) any drift is trivially detectable via `git log`. The one constraint: do not trigger a sync while a Phase 1 PR is under active review. + +### Phase 1: βœ… Copy Source Code (No Workflows Yet) + +**Goal**: Get all Java source code building and testing in the monorepo without any CI/CD. + +1. Copy `copilot-sdk-java-00/` contents into `copilot-sdk-00/java/`: + - `src/` (main, test, generated, site) + - `pom.xml` + - `config/` (checkstyle, spotbugs) + - `scripts/codegen/` β†’ merge `java.ts` into `copilot-sdk-00/scripts/codegen/` + - `CHANGELOG.md`, `README.md`, `jbang-example.java` + - `.lastmerge` β†’ `java/.lastmerge` + - βœ… ~~`.githooks/` β†’ `java/.githooks/`~~ + - `docs/adr/` β†’ `java/docs/adr/` + - βœ… ~~`instructions/copilot-sdk-java.instructions.md` β†’ `.github/skills/java-coding-skill/SKILL.md` (follows the `rust-coding-skill` pattern); Java repo governance merged into `.github/copilot-instructions.md`~~ + +2. Update `pom.xml` paths if needed (should be self-contained under `java/`). + +3. Verify `mvn clean verify` works from `java/` directory locally. Make necessary changes so the test infrastructure is copied locally, rather than checked out. + +### Phase 2: CI Workflows + +**Goal**: Java CI runs on PRs and main pushes within the monorepo. + +1. Create `java-sdk-tests.yml` (adapted from `build-test.yml`): + - Path triggers: `java/**`, `test/**`, `.github/workflows/java-sdk-tests.yml` + - Uses monorepo's `setup-copilot` action (or create `java/setup-copilot` action) + - Runs on 3 OS matrix (match other SDKs) + +2. Merge Java into `codegen-check.yml`: + - Add `java/src/generated/**` to path triggers + - Add a job that runs Java codegen and diffs + +3. Create `java-codegen-agentic-fix.md` (adapted from `codegen-agentic-fix.md`): + - Update paths, remove cross-repo references + - Compile with `gh aw compile` + +4. Merge Java into `copilot-setup-steps.yml`: + - Add JDK 17 setup step + - Add Maven cache + +5. Update `dependabot.yaml`: + - Add Maven ecosystem entry for `/java` + +### Phase 3: Publish Workflows + +**Goal**: Java can be independently published from the monorepo. + +1. Create `java-publish.yml` (adapted from `publish-maven.yml`): + - All paths updated to `java/` prefix + - Working directory set to `java/` + - Uses monorepo secrets + - **Independent trigger** β€” not part of the unified `publish.yml` + +2. Create `java-publish-snapshot.yml` (adapted from `publish-snapshot.yml`): + - Similar path/directory updates + +3. ~~Create `java-deploy-site.yml` (adapted from `deploy-site.yml`):~~ + ~~- Adjust GitHub Pages setup for coexistence~~ + ~~- May need a subdirectory deployment strategy~~ + +4. Create `java-smoke-test.yml` (adapted from `run-smoke-test.yml`). + +5. Migrate `notes.template` to `java/.github/notes.template` or similar. + +### Phase 4: Agentic Workflows and Skills + +**Goal**: Agentic automation works for Java within the monorepo. + +1. **`reference-impl-sync`** β†’ **`java-reference-impl-sync.md`** β€” **REWORK** for intra-repo operation: + - **Trigger**: `schedule` (daily) + `workflow_dispatch` (same as today) + - **Behavior change**: Instead of cloning `github/copilot-sdk` and comparing commits, it: + 1. Reads `java/.lastmerge` (now a monorepo commit SHA) + 2. Runs `git log ..HEAD -- dotnet/src/ nodejs/src/` to find new reference-impl changes + 3. If changes exist β†’ creates an issue assigned to Copilot agent (same as today) + 4. If no changes β†’ closes stale sync issues (same as today) + - **Key simplification**: No cross-repo clone, no remote URL handling, no token for external repo access + - **Compile**: `gh aw compile java-reference-impl-sync.md` + +2. **`agentic-merge-reference-impl` skill** β€” **REWORK** for intra-repo operation: + - **Current behavior**: Clones `github/copilot-sdk`, checks out the target commit, computes a diff of `dotnet/src/` and `nodejs/src/` against the Java repo's `.lastmerge`, then applies equivalent Java changes. + - **New behavior**: + 1. Reads `java/.lastmerge` to get the base commit SHA + 2. Computes `git diff ..HEAD -- dotnet/src/ nodejs/src/` (all local, no clone needed) + 3. Analyzes the diff to identify what changed semantically (new methods, renamed types, new events, etc.) + 4. Applies equivalent idiomatic Java changes under `java/src/` + 5. Runs `mvn verify` from `java/` to validate + 6. Updates `java/.lastmerge` to the current HEAD SHA + 7. Commits and pushes (via `commit-as-pull-request` skill or direct push) + - **Scripts to update**: `.github/scripts/reference-impl-sync/` β€” all 5 scripts assume cross-repo operation: + - `merge-reference-impl-start.sh` β€” remove `git clone`, replace with local `git diff` + - `merge-reference-impl-diff.sh` β€” simplify to intra-repo diff + - `merge-reference-impl-finish.sh` β€” update `java/.lastmerge` with monorepo SHA + - `sync-cli-version-from-reference-impl.sh` β€” now reads from local `nodejs/package.json` directly + - `sync-codegen-version.sh` β€” now reads from local `scripts/codegen/package.json` + - **Prompt files to update**: + - `.github/prompts/agentic-merge-reference-impl.prompt.md` β€” remove cross-repo instructions, add intra-repo paths + - `.github/prompts/coding-agent-merge-reference-impl-instructions.md` β€” same + - **SKILL.md** β€” update with new paths and simplified flow + - Restore the updating of the `readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync` POM property: + pin to. It is updated automatically by + .github/scripts/reference-impl-sync/sync-cli-version-from-reference-impl.sh + +3. **`sdk-consistency-review`** β€” Update: + - Add `java/**` to path triggers in the `.md` frontmatter + - Update agent prompt to include Java in the list of SDKs to review + +4. **`issue-triage`** β€” Update: + - Add `sdk/java` label to the list of per-SDK labels + +5. Merge `agentic-workflows.agent.md` β€” use the monorepo's (newer) version, no action needed. + +6. Migrate `documentation-coverage` skill to monorepo's skills directory (as `java-documentation-coverage`). + +7. Migrate `commit-as-pull-request` skill (check if monorepo already has equivalent). + +### Phase 5: Cross-Cutting Updates + +1. Update monorepo `copilot-instructions.md` to include Java section. +2. Update monorepo `README.md` to list Java as a supported language. +3. Update `scenario-builds.yml` to include Java scenarios (if applicable). +4. Update `docs-validation.yml` to include Java code snippets. +5. Update `lsp.json` to add Java LSP config (optional). +6. Add Java to `docs/` getting-started and feature pages. +7. Update `sdk-protocol-version.json` if Java needs it. + +### Phase 6: Cutover and Cleanup + +1. **Disable CI** in `copilot-sdk-java` (remove or disable workflows). +2. **Archive** `copilot-sdk-java` repo (make read-only). +3. **Update external references**: + - Maven Central POM `` URLs + - README badges pointing to the new repo + - Javadoc.io configuration + - Any links in copilot documentation +4. **Remove duplicate resources** that were merged rather than moved. +5. **Run full CI** in monorepo to validate everything. + +--- + +## 2. Permissions and Secrets Challenges + +### 2A. Secrets That Must Be Provisioned in copilot-sdk + +The Java SDK publish workflow requires secrets that **do not currently exist** in the `copilot-sdk` repo: + +| Old Secret | Old Used By | New Secret | New Used By | Notes | +| ------------------------ | ------------------------------------------------ | ----------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ | +| `RELEASE_TOKEN` | `publish-maven.yml` | `JAVA_RELEASE_TOKEN` | `java-publish-maven.yml` | PAT with `contents:write` for pushing tags/commits during maven-release-plugin | +| `GPG_SECRET_KEY` | `publish-maven.yml` | `JAVA_GPG_SECRET_KEY` | `java-publish-maven.yml` | GPG private key for signing Maven artifacts | +| `GPG_PASSPHRASE` | `publish-maven.yml` | `JAVA_GPG_PASSPHRASE` | `java-publish-maven.yml` | Passphrase for the GPG key | +| `MAVEN_CENTRAL_USERNAME` | `publish-maven.yml`, `publish-snapshot.yml` | `JAVA_MAVEN_CENTRAL_USERNAME` | `java-publish-maven.yml`, `java-publish-snapshot.yml` | Sonatype/Maven Central credentials | +| `MAVEN_CENTRAL_PASSWORD` | `publish-maven.yml`, `publish-snapshot.yml` | `JAVA_MAVEN_CENTRAL_PASSWORD` | `java-publish-maven.yml`, `java-publish-snapshot.yml` | Sonatype/Maven Central credentials | +| `COPILOT_GITHUB_TOKEN` | `build-test.yml`, `codegen-agentic-fix.lock.yml` | unchanged | | Token for Copilot CLI in CI | + +### 2B. Existing Secrets in copilot-sdk That May Conflict + +| Secret | Used By | Concern | +| ------------------------------------------------------ | -------------------- | ------------------------------------------------------------ | +| `CARGO_REGISTRY_TOKEN` | `publish.yml` (Rust) | No conflict | +| `GH_AW_GITHUB_TOKEN` / `GH_AW_GITHUB_MCP_SERVER_TOKEN` | Agentic workflows | Likely already present; Java agentic workflows need the same | + +### 2C. Permissions / Access to Provision + +- [βœ…] **Repository secrets**: File a ticket to add the 6 Java-specific secrets to `github/copilot-sdk`. See https://github.com/github/copilot-sdk-partners/issues/90 +- [βœ…] **CODEOWNERS team**: ~~Ensure `@github/copilot-sdk-java` team has access to `github/copilot-sdk` and is added to CODEOWNERS for `java/**`.~~ See https://github.com/github/copilot-sdk-partners/issues/89 . +- [βŒ›] **Maven Central Trusted Publisher**: Currently configured for `github/copilot-sdk-java`. Must be updated to also allow publishing from `github/copilot-sdk` (or create a new namespace mapping). **This is the highest-risk permission issue** β€” Maven Central's Trusted Publisher setup ties the repository name to the publish flow. See https://github.com/github/copilot-sdk-partners/issues/91 +- [βœ…] **GitHub Pages**: ~~If `deploy-site.yml` moves, check if GitHub Pages is enabled on the monorepo and whether Java docs can coexist with any existing docs deployment.~~ See https://github.com/github/copilot-sdk-partners/issues/85 +- [βŒ›] **Branch protection**: Ensure `main` branch protection rules in copilot-sdk permit the Java CI workflows (merge queues, required status checks, etc.). See https://github.com/github/copilot-sdk-partners/issues/95 . +- [ ] **Copilot coding agent**: Ensure the agent is enabled for `github/copilot-sdk` and the `copilot-setup-steps.yml` is updated to include Java tooling. + +--- + +## 3. Naming Convention Proposal + +### Current State + +The monorepo already uses a partially consistent pattern: + +- **Test workflows**: `{language}-sdk-tests.yml` (e.g., `dotnet-sdk-tests.yml`, `go-sdk-tests.yml`) +- **Cross-language workflows**: descriptive kebab-case names (e.g., `codegen-check.yml`, `publish.yml`) +- **Agentic workflows**: descriptive kebab-case (e.g., `issue-triage.md`, `handle-bug.md`) + +### Proposed Convention + +**Use kebab-case throughout. Language-specific workflows start with the language name.** + +#### Language-specific workflow naming: `{language}-{purpose}.yml` + +| Current (copilot-sdk) | Current (copilot-sdk-java) | Proposed New Name | +| ---------------------- | ------------------------------- | ------------------------------------------------------------------------------- | +| `nodejs-sdk-tests.yml` | β€” | `nodejs-sdk-tests.yml` (keep) | +| `dotnet-sdk-tests.yml` | β€” | `dotnet-sdk-tests.yml` (keep) | +| `go-sdk-tests.yml` | β€” | `go-sdk-tests.yml` (keep) | +| `python-sdk-tests.yml` | β€” | `python-sdk-tests.yml` (keep) | +| `rust-sdk-tests.yml` | β€” | `rust-sdk-tests.yml` (keep) | +| β€” | `build-test.yml` | **`java-sdk-tests.yml`** | +| β€” | `publish-maven.yml` | **`java-publish.yml`** | +| β€” | `publish-snapshot.yml` | **`java-publish-snapshot.yml`** | +| β€” | `deploy-site.yml` | **`java-deploy-site.yml`** | +| β€” | `run-smoke-test.yml` | **`java-smoke-test.yml`** | +| β€” | `codegen-check.yml` | **Merge into existing `codegen-check.yml`** (add Java paths + job) | +| β€” | `codegen-agentic-fix.md` | **`java-codegen-fix.md`** + `.lock.yml` | +| β€” | `reference-impl-sync.md` | **`java-reference-impl-sync.md`** + `.lock.yml` (reworked for intra-repo) | +| β€” | `update-copilot-dependency.yml` | **Merge into existing `update-copilot-dependency.yml`** (add Java codegen step) | +| β€” | `copilot-setup-steps.yml` | **Merge into existing** (add JDK 17 + Maven setup) | +| β€” | `agentics-maintenance.yml` | Already exists via gh-aw in the monorepo; **do not duplicate** | + +#### Cross-language workflow naming: `{purpose}.yml` (no language prefix) + +Keep existing names: `publish.yml`, `codegen-check.yml`, `scenario-builds.yml`, `docs-validation.yml`, etc. + +#### Summary of naming rules + +1. **Language-specific** workflows: `{language}-{purpose}.yml` / `.md` +2. **Cross-language** workflows: `{purpose}.yml` / `.md` (no prefix) +3. **Kebab-case** throughout (already the convention) +4. **Agentic workflows**: same pattern but with `.md` extension +5. **Lock files**: auto-generated, always `{name}.lock.yml` + +--- + +## 4. Current Language Separation Assessment + +### Are the languages in copilot-sdk-00 already sufficiently separated? + +**Mostly yes, with a few cross-cutting concerns:** + +#### Well-separated + +- **Source code**: Each language lives in its own top-level directory (`nodejs/`, `python/`, `go/`, `dotnet/`, `rust/`). Java will go in `java/`. +- **Test workflows**: Each has its own `{language}-sdk-tests.yml` with path-scoped triggers (only fires on changes to that language's directory + `test/`). +- **Dependabot**: Already per-ecosystem, per-directory entries. + +#### Cross-cutting concerns (potential friction points) + +| Concern | Current State | Impact on Java | +| --------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Shared test harness** (`test/harness/`) | Node.js-based replay proxy used by all E2E tests | Java already uses this (clones it at build time from `copilot-sdk` repo). When in-repo, can reference it directly β€” **simpler**. | +| **Shared test snapshots** (`test/snapshots/`) | YAML snapshot files consumed by all languages | Java can share these β€” **positive change**. | +| **Unified codegen** (`scripts/codegen/`) | One `package.json` with generators for TS, C#, Python, Go, Rust | Java codegen (`java.ts`) must be **merged in**. The Java codegen currently has its own `package.json` with a direct `@github/copilot` dependency; the monorepo codegen gets it via `nodejs/node_modules`. This needs reconciliation. | +| **`justfile`** | Has per-language targets (`format-go`, `test-dotnet`, etc.) | Must add `format-java`, `lint-java`, `test-java`, `install-java` targets. | +| **Unified `publish.yml`** | Single workflow publishes all languages with one version number | **Java CANNOT join this** β€” Java has its own versioning scheme (`X.Y.Z-java.N`). Java must keep a separate `java-publish.yml`. | +| **`sdk-consistency-review`** agentic workflow | Reviews PRs for cross-SDK parity (currently watches nodejs, python, go, dotnet) | Must add `java/` to the path triggers and update the agent prompt to include Java. | +| **`copilot-setup-steps.yml`** | Sets up Node, Python, Go, .NET, Rust | Must add JDK 17 + Maven. | +| **`copilot-instructions.md`** | Monorepo-wide instructions | Must incorporate Java-specific guidance. | +| **`CODEOWNERS`** | Single `* @github/copilot-sdk` | ~~Must add `java/ @github/copilot-sdk-java` line.~~ | +| **`lsp.json`** | Configures C# and Go language servers for Copilot agent | May want to add Java LSP (jdtls or similar) β€” **optional**. | + +### The Big Question: `reference-impl-sync` + +Currently, the Java SDK has a scheduled workflow that polls `github/copilot-sdk` for new commits and creates issues for the Copilot agent to port. **This workflow is still needed** when Java lives in the same repo β€” the primary maintainers of `dotnet/` and `nodejs/` are not Java experts, and changes to those SDKs still need to be detected and ported into `java/`. + +What changes is the **mechanism**: instead of polling a remote repository, the workflow watches for commits that land on `main` touching `dotnet/src/` or `nodejs/src/` and compares against `java/.lastmerge` (which now stores a monorepo commit SHA rather than a cross-repo one). + +**Recommendation**: + +1. **Keep `java/.lastmerge`** β€” it stores the last monorepo commit SHA whose `dotnet/`/`nodejs/` changes have been ported into Java. This is the anchor for diffing. +2. **Keep `reference-impl-sync` as `java-reference-impl-sync.md`** β€” reworked for intra-repo operation (see Β§6 Phase 4 for details). +3. **Keep `agentic-merge-reference-impl` skill** β€” reworked so that instead of cloning a remote repo, it reads diffs from the local `dotnet/` and `nodejs/` directories relative to the SHA in `java/.lastmerge`. +4. The `sdk-consistency-review` workflow provides an additional safety net on PRs, but is **not a replacement** for the scheduled sync β€” it only fires on PRs, not when changes land on `main` without Java updates. + +--- + +## 5. Workflow Inventory Tables + +### 5A. copilot-sdk-java-00 Workflows (Source) + +| YAML File Name | Brief Description | Primary Language | Complexity | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ---------- | +| `build-test.yml` | Main CI: Spotless, build, Javadoc, `mvn verify`, JaCoCo coverage badges | Java | L | +| `codegen-check.yml` | Re-runs Java codegen, commits regenerated files to PRs, triggers agentic fix on failure | Java | M | +| `codegen-agentic-fix.md` + `.lock.yml` | Agentic: auto-fixes compilation/test failures caused by codegen changes | Java | L | +| `reference-impl-sync.md` + `.lock.yml` | Agentic: checks for new commits in `github/copilot-sdk`, creates issue for Copilot agent to port | Java | L | +| `publish-maven.yml` | Publishes release to Maven Central via `maven-release-plugin`, GPG signing, GitHub Release creation | Java | XL | +| `publish-snapshot.yml` | Publishes SNAPSHOT builds to Maven Central Snapshots on a weekday schedule | Java | M | +| `deploy-site.yml` | Builds/deploys versioned Maven site docs to GitHub Pages | Java | M | +| `run-smoke-test.yml` | Builds SDK, installs locally, runs Copilot CLI smoke test on JDK 17 + JDK 25 (see [Appendix C](#appendix-c-java-smoketest)) | Java | M | +| `update-copilot-dependency.yml` | Updates `@github/copilot` npm dep in codegen, re-runs generator, creates PR | Java | M | +| `copilot-setup-steps.yml` | Environment setup for Copilot coding agent (JDK 17, Node 22, gh-aw, pre-commit hooks) | Java | S | +| `agentics-maintenance.yml` | Auto-generated gh-aw maintenance: closes expired discussions/issues/PRs | Cross-language (infra) | S | +| `notes.template` | Release notes template for Maven Central (not a workflow) | Java | S | + +### 5B. copilot-sdk-00 Workflows (Target Monorepo) + +| YAML File Name | Brief Description | Primary Language | Complexity | +| -------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------- | ---------- | +| `nodejs-sdk-tests.yml` | Build + test Node.js SDK on 3 OS, prettier, ESLint, typecheck, E2E | Node.js | L | +| `dotnet-sdk-tests.yml` | Build + test .NET SDK on 3 OS, format check, E2E via replay proxy | .NET | L | +| `go-sdk-tests.yml` | Build + test Go SDK on 3 OS, gofmt, golangci-lint, E2E | Go | L | +| `python-sdk-tests.yml` | Build + test Python SDK on 3 OS, ruff, ty, E2E via pytest | Python | L | +| `rust-sdk-tests.yml` | Build + test Rust SDK on 3 OS, nightly fmt, clippy, cargo test | Rust | L | +| `codegen-check.yml` | Verifies generated files across Node, .NET, Python, Go, Rust | Cross-language | M | +| `publish.yml` | Publishes all SDKs (npm, NuGet, PyPI, Go tags, crates.io) from a single version | Cross-language | XL | +| `scenario-builds.yml` | Verifies example scenarios build for each language | Cross-language | M | +| `docs-validation.yml` | Extracts and validates code snippets from `docs/` | Cross-language | M | +| `update-copilot-dependency.yml` | Updates `@github/copilot` dep, re-runs codegen, opens PR | Cross-language | M | +| `copilot-setup-steps.yml` | Agent env setup: Node, Python, Go, .NET, Rust, just, gh-aw | Cross-language | M | +| `verify-compiled.yml` | Ensures `.lock.yml` files match `.md` sources | Cross-language (infra) | S | +| `collect-corrections.yml` | Collects triage agent feedback | Cross-language (infra) | S | +| `corrections-tests.yml` | Tests for triage correction scripts | Cross-language (infra) | S | +| `issue-classification.md` + `.lock.yml` | Agentic: classifies issues β†’ routes to handle-\* handlers | Cross-language | M | +| `issue-triage.md` + `.lock.yml` | Agentic: labels, acknowledges, requests clarification, closes dupes | Cross-language | L | +| `handle-bug.md` + `.lock.yml` | Agentic: investigates bug issues | Cross-language | M | +| `handle-documentation.md` + `.lock.yml` | Agentic: handles doc-related issues | Cross-language | S | +| `handle-enhancement.md` + `.lock.yml` | Agentic: labels enhancement issues | Cross-language | S | +| `handle-question.md` + `.lock.yml` | Agentic: labels question issues | Cross-language | S | +| `cross-repo-issue-analysis.md` + `.lock.yml` | Agentic: checks if issue root cause is in copilot-agent-runtime | Cross-language | M | +| `release-changelog.md` + `.lock.yml` | Agentic: generates release notes, updates CHANGELOG | Cross-language | M | +| `sdk-consistency-review.md` + `.lock.yml` | Agentic: reviews PRs for cross-SDK feature parity | Cross-language | L | + +--- + +## 6. Agents, Skills, Prompts, and Supporting Resources Inventory + +### 6A. copilot-sdk-java-00 + +| Resource | Location | Purpose | Must Migrate? | +| ---------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------- | +| **Agent:** `agentic-workflows.agent.md` | `.github/agents/` | Dispatcher for gh-aw workflow creation/debug | Yes (merge with monorepo version) | +| **Skill:** `agentic-merge-reference-impl` | `.github/skills/` + `.github/prompts/` | Merges reference impl changes into Java | Yes β€” **must be reworked** (no longer cross-repo) | +| **Skill:** `commit-as-pull-request` | `.github/skills/` + `.github/prompts/` | Creates branch, pushes, opens PR | Yes (may already exist in monorepo) | +| **Skill:** `documentation-coverage` | `.github/skills/` + `.github/prompts/` | Assesses Java docs coverage | Yes | +| **Prompt:** `coding-agent-merge-reference-impl-instructions.md` | `.github/prompts/` | Instructions for coding agent merge | Yes | +| **Prompt:** `test-coverage-assessment.prompt.md` | `.github/prompts/` | Test coverage assessment | Yes | +| **Composite Action:** `setup-copilot` | `.github/actions/setup-copilot/` | Sets up Copilot CLI for Java tests | Yes β€” **adapt paths** | +| **Composite Action:** `test-report` | `.github/actions/test-report/` | Test report generation | Yes | +| **Scripts:** `release/`, `ci/`, `build/`, `reference-impl-sync/` | `.github/scripts/` | Release, CI, sync automation | Yes β€” **path rewrites** | +| **Dependabot:** `dependabot.yml` | `.github/` | Maven + GitHub Actions updates | Merge into monorepo's `dependabot.yaml` | +| **CODEOWNERS** | `.github/` | ~~`@github/copilot-sdk-java` | Merge into monorepo's CODEOWNERS~~ | +| **Issue Templates:** bug, documentation, feature, maintenance | `.github/ISSUE_TEMPLATE/` | Issue forms | Assess whether monorepo issue triage covers this | +| **PR Template** | `.github/pull_request_template.md` | PR form | Merge or keep per-language | +| **Release Config** | `.github/release.yml` | Auto-generated release notes config | Merge | +| **copilot-instructions.md** | `.github/` | Agent instructions for Java SDK | Merge (scoped to `java/`) | +| **Site templates** | `.github/templates/` | HTML/CSS for GitHub Pages | Migrate to `java/` | +| **Coverage badge script** | `.github/scripts/generate-coverage-badge.sh` | JaCoCo badge generation | Migrate | +| **`.lastmerge`** | repo root | Tracks last merged ref-impl commit | **This concept changes** β€” see Β§6 | +| **`.githooks/pre-commit`** | repo root | Runs `mvn spotless:check` | Migrate to `java/.githooks/` | +| **`instructions/copilot-sdk-java.instructions.md`** | `instructions/` | VS Code copilot instructions | Create `.github/skills/java-coding-skill/SKILL.md` (follows `rust-coding-skill` pattern) | + +### 6B. copilot-sdk-00 + +| Resource | Location | Purpose | +| --------------------------------------- | -------------------------------- | ----------------------------------------------------- | +| **Agent:** `agentic-workflows.agent.md` | `.github/agents/` | Same dispatcher (newer version with more routing) | +| **Agent:** `docs-maintenance.agent.md` | `.github/agents/` | Docs auditor agent | +| **Skill:** `rust-coding-skill` | `.github/skills/` | Rust-specific coding skill | +| **Composite Action:** `setup-copilot` | `.github/actions/setup-copilot/` | Sets up Copilot CLI from nodejs package | +| **Command:** `triage_feedback.yml` | `.github/commands/` | Repository dispatch for triage feedback | +| **LSP Config:** `lsp.json` | `.github/` | C#, Go language server configs | +| **Dependabot:** `dependabot.yaml` | `.github/` | npm, pip, gomod, nuget, github-actions, devcontainers | +| **CODEOWNERS** | `.github/` | `@github/copilot-sdk` | +| **copilot-instructions.md** | `.github/` | Monorepo-wide agent instructions | + +--- + +## 7. Pitfalls and Risk Register + +### HIGH RISK + +| # | Risk | Impact | Mitigation | +| --- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| H1 | **Maven Central Trusted Publisher** repo-name mismatch | Cannot publish Java releases from monorepo | Verify/update Trusted Publisher config in Maven Central **before** migration. If the GAV is bound to `github/copilot-sdk-java`, it must be updated. | +| H2 | **Unified `publish.yml` version collision** | All SDKs in monorepo share one version. Java has independent `X.Y.Z-java.N` versions. | Java must keep a **separate** publish workflow. Do NOT merge into `publish.yml`. | +| H3 | **`agentic-merge-reference-impl` breaks** | The core Java development loop relies on this skill to stay in sync with .NET/Node changes | Must be carefully reworked for intra-repo operation before cutover. Test thoroughly with a dry-run on a feature branch. The skill + its 5 shell scripts + 2 prompt files all assume cross-repo cloning. | +| H4 | **Secret provisioning delay** | Can't publish or run full CI until secrets are provisioned | Start secret provisioning **immediately** (Phase 0). | +| H5 | **Test harness path changes** | Java E2E tests currently clone `copilot-sdk` at build time to get `test/harness/` and `test/snapshots/`. In-repo, these paths change. | Update `pom.xml` and test infrastructure to reference local `test/` directory instead of cloning. **This simplifies things significantly.** | + +### MEDIUM RISK + +| # | Risk | Impact | Mitigation | +| --- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| M1 | **Codegen `package.json` merge** | Java codegen has its own `@github/copilot` dependency; monorepo codegen gets it from `nodejs/node_modules` | Align Java codegen to use the same dependency source. May need to add `generate:java` script to monorepo's `scripts/codegen/package.json`. | +| M2 | ~~**GitHub Pages conflict**~~ | ~~Java deploys versioned docs to Pages. Monorepo may have its own Pages setup.~~ | ~~Use subdirectory deployment or a separate Pages branch for Java.~~ | +| M3 | **Branch protection / required checks** | New `java-sdk-tests` check may not be in the required list | Add to branch protection after first successful run. | +| M4 | **CODEOWNERS team permissions** | `@github/copilot-sdk-java` team may not have write access to `github/copilot-sdk` | Verify team access and add to repo collaborators. See https://github.com/github/copilot-sdk-partners/issues/89 | +| M5 | **`copilot-setup-steps.yml` bloat** | Adding JDK + Maven makes agent setup slower for non-Java tasks | Acceptable trade-off; other languages already add their tools. Could consider conditional setup but that's over-engineering. | +| M6 | **gh-aw version mismatch** | Java repo uses gh-aw `v0.68.3` setup action pinned at `v0.71.5`; monorepo uses `v0.64.2` reference in docs | Align gh-aw versions. Use the newer version. Recompile all `.lock.yml` files. | + +### LOW RISK + +| # | Risk | Impact | Mitigation | +| --- | ---------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------- | +| L1 | **Issue template conflicts** | Java has custom issue templates; monorepo uses agentic triage | Monorepo agentic triage covers this. Can add Java-specific labels. | +| L2 | **PR template differences** | Different PR templates | Use monorepo's template. Java-specific guidance in CONTRIBUTING.md. | +| L3 | **`.githooks` scope** | Java pre-commit hook runs `mvn spotless:check` globally | Scope hook to only run when Java files are changed. | +| L4 | **Duplicate `agentics-maintenance.yml`** | Java repo has its own; monorepo will generate one | The monorepo's gh-aw will handle this automatically. Don't migrate. | + +--- + +## 8. Post-Migration Verification Checklist + +### CI/CD + +- [ ] `java-sdk-tests.yml` passes on all 3 OS platforms +- [ ] `codegen-check.yml` includes Java and passes +- [ ] `java-codegen-fix.md` compiles and agentic workflow functions +- [ ] `java-publish.yml` can do a dry-run publish +- [ ] `java-publish-snapshot.yml` publishes a SNAPSHOT +- [ ] `java-smoke-test.yml` passes on JDK 17 + JDK 25 +- [ ] `java-deploy-site.yml` successfully deploys docs + +### Integration + +- [ ] `copilot-setup-steps.yml` includes JDK and Maven +- [ ] `dependabot.yaml` includes Maven ecosystem for `java/` +- [βœ…] `CODEOWNERS` includes `java/` path. See https://github.com/github/copilot-sdk-partners/issues/89 +- [ ] `justfile` has all Java targets and `just test` includes Java +- [ ] `sdk-consistency-review` includes `java/` in path triggers +- [ ] `issue-triage` knows about `sdk/java` label + +### Code + +- [ ] `mvn verify` passes from `java/` directory +- [ ] E2E tests use local `test/harness/` and `test/snapshots/` (no cloning) +- [ ] Java codegen integrated into `scripts/codegen/` +- [ ] `.lastmerge` exists at `java/.lastmerge` + +### Documentation + +- [ ] Monorepo `README.md` lists Java +- [ ] `copilot-instructions.md` includes Java governance section (build, test, conventions) +- [ ] `.github/skills/java-coding-skill/SKILL.md` exists with Java API patterns and coding rules +- [ ] `java/README.md` links updated to monorepo +- [ ] Maven Central POM `` URLs updated + +### Agentic Sync + +- [ ] `java-reference-impl-sync.md` compiles and detects new dotnet/nodejs changes via local `git log` +- [ ] `agentic-merge-reference-impl` skill works intra-repo (no cross-repo clone) +- [ ] `java/.lastmerge` correctly stores monorepo commit SHAs +- [ ] Sync scripts in `.github/scripts/java/reference-impl-sync/` use local paths + +### Cleanup + +- [ ] `copilot-sdk-java` repo archived +- [ ] No broken links to old repo +- [ ] No duplicate `agentics-maintenance.yml` + +--- + +## Appendix A: Files to Copy vs. Merge vs. Delete + +| Source File (copilot-sdk-java-00) | Action | Target Location (copilot-sdk-00) | +| -------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------- | +| `src/` | Copy | `java/src/` | +| `config/` | Copy | `java/config/` | +| `pom.xml` | Copy + update paths | `java/pom.xml` | +| `CHANGELOG.md` | Copy | `java/CHANGELOG.md` | +| `README.md` | Copy + update links | `java/README.md` | +| `jbang-example.java` | Copy | `java/jbang-example.java` | +| `.lastmerge` | Copy | `java/.lastmerge` | +| `.githooks/pre-commit` | Copy + scope to Java changes | `java/.githooks/pre-commit` | +| `docs/adr/` | Copy | `java/docs/adr/` | +| `scripts/codegen/java.ts` | Copy | `java/scripts/codegen/java.ts` | +| `scripts/codegen/package.json` | Copy (Java keeps its own) | `java/scripts/codegen/package.json` | +| `.github/workflows/build-test.yml` | **Adapt** β†’ rename | `.github/workflows/java-sdk-tests.yml` | +| `.github/workflows/publish-maven.yml` | **Adapt** β†’ rename | `.github/workflows/java-publish.yml` | +| `.github/workflows/publish-snapshot.yml` | **Adapt** β†’ rename | `.github/workflows/java-publish-snapshot.yml` | +| `.github/workflows/deploy-site.yml` | **Adapt** β†’ rename | `.github/workflows/java-deploy-site.yml` | +| `.github/workflows/run-smoke-test.yml` | **Adapt** β†’ rename | `.github/workflows/java-smoke-test.yml` | +| `.github/workflows/codegen-check.yml` | **Merge** into existing | `.github/workflows/codegen-check.yml` | +| `.github/workflows/codegen-agentic-fix.md` | **Adapt** β†’ rename | `.github/workflows/java-codegen-fix.md` | +| `.github/workflows/update-copilot-dependency.yml` | **Merge** into existing | `.github/workflows/update-copilot-dependency.yml` | +| `.github/workflows/copilot-setup-steps.yml` | **Merge** into existing | `.github/workflows/copilot-setup-steps.yml` | +| `.github/workflows/reference-impl-sync.md` + `.lock.yml` | **Adapt** β†’ rename + rework for intra-repo | `.github/workflows/java-reference-impl-sync.md` + `.lock.yml` | +| `.github/workflows/agentics-maintenance.yml` | **DELETE** (monorepo has its own) | β€” | +| `.github/workflows/notes.template` | Copy | `.github/workflows/java-notes.template` | +| `.github/actions/setup-copilot/` | **Adapt** or merge | `.github/actions/java-setup-copilot/` or merge | +| `.github/actions/test-report/` | Copy | `.github/actions/java-test-report/` | +| `.github/scripts/*` | Copy + update paths | `.github/scripts/java/` (new subdirectory) | +| `.github/skills/agentic-merge-reference-impl/` | **Rework** for intra-repo (remove cross-repo clone, use local git diff) | `.github/skills/java-merge-reference-impl/` | +| `.github/skills/commit-as-pull-request/` | Check for duplicates | `.github/skills/commit-as-pull-request/` | +| `.github/skills/documentation-coverage/` | Copy | `.github/skills/java-documentation-coverage/` | +| `.github/prompts/*` | Copy + update | `.github/prompts/` (prefix with `java-` if needed) | +| `.github/dependabot.yml` | **Merge** into existing | `.github/dependabot.yaml` | +| `.github/CODEOWNERS` | **Merge** into existing | `.github/CODEOWNERS` | +| `.github/copilot-instructions.md` | **Merge** into existing | `.github/copilot-instructions.md` | +| `.github/release.yml` | **Merge** into existing | `.github/release.yml` (if it exists) | +| `.github/ISSUE_TEMPLATE/*` | Evaluate β€” likely skip | β€” | +| `.github/pull_request_template.md` | Evaluate β€” likely skip | β€” | +| `.github/templates/` | Copy | `java/.github/templates/` or `java/src/site/` | +| `instructions/copilot-sdk-java.instructions.md` | Create skill (follows `rust-coding-skill` pattern) | `.github/skills/java-coding-skill/SKILL.md` | +| `test` (file, not directory) | Copy if needed | `java/test` | + +## Appendix B: Unique Java Concerns vs Other Languages + +| Concern | Java | Other Languages | Notes | +| --------------------------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| **Build system** | Maven (`pom.xml`) | npm, pip, go mod, dotnet, cargo | Fully self-contained under `java/` | +| **Versioning** | `X.Y.Z-java.N` (independent) | Shared `X.Y.Z` across all others | **Must keep separate publish workflow** | +| **Code formatting** | Spotless (Eclipse formatter) | prettier, ruff, gofmt, dotnet format, rustfmt | Runs only on Java files | +| **Test framework** | JUnit + Surefire | Vitest, pytest, go test, xunit, cargo test | Standard; no conflicts | +| **E2E test harness** | Clones `copilot-sdk` at build time | References local `test/harness/` | **Major simplification** when in-repo | +| **Codegen** | Own `java.ts` + own `@github/copilot` dep | Shared codegen scripts + shared dep | Needs reconciliation | +| **CI runner** | JDK 17 + JDK 25 (smoke test) | Node 22, Python 3.12, Go 1.24, .NET 10, Rust 1.94 | Just another tool in `copilot-setup-steps.yml` | +| **Publishing** | Maven Central (GPG + Sonatype) | npm, PyPI, NuGet, crates.io, Go tags | Completely different mechanism | +| **Docs hosting** | ~~GitHub Pages (Maven site)~~ | ~~Not clear if monorepo has its own~~ | ~~Potential conflict~~ | +| **Reference impl tracking** | `.lastmerge` + scheduled sync + agentic merge skill | N/A (they ARE the reference impl) | `.lastmerge` stores monorepo SHA; sync becomes intra-repo but is still needed because Java maintainers β‰  .NET/Node maintainers | + +--- + +## Appendix C: Java Smoketest + +### Overview + +The Java SDK has an AI-driven smoke test that validates the SDK's Quick Start code actually compiles and runs. The test is **prompt-driven**: the `run-smoke-test.yml` workflow invokes the Copilot CLI (`copilot --yolo`) with a prompt that instructs it to read the repository's `README.md`, extract the Quick Start code and Maven coordinates, generate a standalone Maven project, build it, and run it. Success = exit code 0. + +This design intentionally tests the README itself β€” if the documented code doesn't compile against the published artifact, the smoke test fails rather than silently fixing the code. This catches documentation drift. + +### How It Works Today + +1. **`src/test/prompts/PROMPT-smoke-test.md`** β€” The master prompt. It instructs the Copilot CLI to: + - Read the top-level `README.md` + - Extract the **"Snapshot Builds"** section (Maven GAV + snapshots repository config) + - Extract the **"Quick Start"** section (verbatim Java source code) + - Create a `smoke-test/` Maven project using those extracted values + - Build with `mvn -U clean package` + +2. **`run-smoke-test.yml`** β€” The workflow. It: + - Builds the SDK and installs it to the local Maven repo + - Feeds the prompt to `copilot --yolo` with overrides (use `--no-snapshot-updates`, stop after build) + - Runs the built jar in a separate deterministic step + - Has two jobs: `smoke-test-jdk17` and `smoke-test-java25` (the latter also applies virtual thread modifications via `// JDK 25+:` comments) + +3. **`build-test.yml`** β€” Calls `run-smoke-test.yml` as a reusable workflow. The main SDK test suite (`java-sdk` job) depends on the smoke test and only runs if it doesn't fail. + +### What Breaks When Moving to the Monorepo + +The smoke test prompt (`PROMPT-smoke-test.md`) contains these instructions: + +> Read the file `README.md` at the top level of this repository. You will need two sections from it: **"Snapshot Builds"** and **"Quick Start"** + +After migration, the **top-level `README.md`** is the monorepo's README (`copilot-sdk-00/README.md`), which does not contain a "Snapshot Builds" section or a "Quick Start" section with Java code. The Java-specific README moves to `java/README.md`. + +Additionally, the monorepo's top-level README contains Quick Start code for **other languages** (TypeScript, Python, Go, C#). If the prompt were naively updated to "read the README," the AI agent might extract the wrong language's code. + +### Required Changes + +#### 1. Update `PROMPT-smoke-test.md` β€” change the README path + +Replace: + +``` +Read the file `README.md` at the top level of this repository. +``` + +With: + +``` +Read the file `java/README.md` in this repository. +``` + +This is the only structural change needed in the prompt. The section names ("Snapshot Builds" and "Quick Start") remain the same in `java/README.md`. + +#### 2. Update `java/README.md` β€” ensure required sections survive the move + +The prompt depends on two specific sections by name: + +- **"Snapshot Builds"** β€” must contain the Maven GAV with `-SNAPSHOT` version and the `central-snapshots` repository XML +- **"Quick Start"** β€” must contain the verbatim Java source code with `// JDK 25+:` inline comments for virtual thread toggling + +When migrating `README.md` β†’ `java/README.md`, these sections and their content must be preserved exactly. The current monorepo placeholder at `java/README.md` has a different Quick Start (different class name `QuickStart` vs `CopilotSDK`, different imports, no `// JDK 25+:` comments, no `System.exit` logic, no usage metrics handling). **The migrated `README.md` from `copilot-sdk-java` must replace the monorepo placeholder**, not the other way around. + +#### 3. Update `run-smoke-test.yml` β†’ `java-smoke-test.yml` β€” working directory + +The workflow steps that run `mvn` and reference `src/test/prompts/PROMPT-smoke-test.md` assume the repo root is the Java project root. After migration: + +- Add `working-directory: ./java` to the "Build SDK and install to local repo" step +- Update the prompt text from `src/test/prompts/PROMPT-smoke-test.md` to `java/src/test/prompts/PROMPT-smoke-test.md` (or set working directory before invoking `copilot`) +- Update the `cd smoke-test` step to `cd java/smoke-test` +- Update the `uses: ./.github/actions/setup-copilot` reference to point to the monorepo's setup action (or a Java-specific one at `.github/actions/java-setup-copilot/`) + +#### 4. Update `build-test.yml` β†’ `java-sdk-tests.yml` β€” smoke test call + +The current `build-test.yml` calls: + +```yaml +uses: ./.github/workflows/run-smoke-test.yml +``` + +After rename, update to: + +```yaml +uses: ./.github/workflows/java-smoke-test.yml +``` + +### Risk Assessment + +| Risk | Severity | Notes | +| ---------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Prompt reads wrong README | **HIGH** | If `PROMPT-smoke-test.md` still says "top level README," the AI agent reads the monorepo README and fails or extracts wrong-language code | +| `java/README.md` placeholder overwrites real content | **HIGH** | The monorepo already has a `java/README.md` with a different Quick Start. Must be replaced with the full Java SDK README during migration | +| `smoke-test/` directory created at wrong location | **MEDIUM** | Without `working-directory: ./java`, the smoke test project gets created at the monorepo root instead of under `java/` | +| `// JDK 25+:` comments missing from Quick Start | **MEDIUM** | The JDK 25 smoke test job relies on these comments to toggle virtual thread support. Missing comments β†’ JDK 25 job builds without virtual threads and still passes (silent regression, not a failure) | + +### Verification Checklist + +- [ ] `PROMPT-smoke-test.md` references `java/README.md`, not `README.md` +- [ ] `java/README.md` contains "Snapshot Builds" and "Quick Start" sections with the full content from `copilot-sdk-java` +- [ ] Quick Start code includes `// JDK 25+:` inline comments and `System.exit` logic +- [ ] `java-smoke-test.yml` uses `working-directory: ./java` for Maven steps +- [ ] `java-smoke-test.yml` references the correct prompt path +- [ ] `java-sdk-tests.yml` calls `java-smoke-test.yml` (not `run-smoke-test.yml`) +- [ ] Smoke test passes locally from `java/` subdirectory before merging diff --git a/80-java-monorepo-add-01-remove-before-merge/dd-2997995-phase-1-plan.md b/80-java-monorepo-add-01-remove-before-merge/dd-2997995-phase-1-plan.md new file mode 100644 index 000000000..f479e1d3e --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/dd-2997995-phase-1-plan.md @@ -0,0 +1,99 @@ +# Phase 1 Agent Prompt: Copy Java SDK Source Code into Monorepo + +## Instructions + +You are working in the `copilot-sdk-00` repository (dest). The source Java SDK code lives at `../copilot-sdk-java-00` (relative to this repo root). Both are local clones: + +- **Dest (you are here):** `copilot-sdk-00` β€” local clone of `https://github.com/github/copilot-sdk` +- **Source:** `../copilot-sdk-java-00` β€” local clone of `https://github.com/github/copilot-sdk-java` + +**Before doing anything else**, read the file `80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md` in this repository. It contains the full migration plan. You are executing **Phase 1 ONLY** β€” "Copy Source Code (No Workflows Yet)". Do NOT perform any other phases (Phase 2, 3, 4, 5, or 6). + +You are safe to commit directly to the current topic branch. Make fine-grained commits with reasonable commit log messages as you go (e.g., one commit per logical group of files copied, one commit for pom.xml adjustments, one commit for test infrastructure changes). + +## Phase 1 Goal + +Get all Java source code building and testing in the monorepo under `java/` without any CI/CD workflows. + +## Phase 1 Steps + +### Step 1: Copy files from source to dest + +Copy the following from `../copilot-sdk-java-00/` into `java/` (replacing the existing placeholder `java/README.md`): + +| Source path (relative to `../copilot-sdk-java-00/`) | Dest path (relative to repo root) | +| --------------------------------------------------- | ------------------------------------------------ | +| `src/` (all of it: main, test, generated, site) | `java/src/` | +| `pom.xml` | `java/pom.xml` | +| `config/` (checkstyle, spotbugs) | `java/config/` | +| `scripts/codegen/java.ts` | `java/scripts/codegen/java.ts` | +| `scripts/codegen/package.json` | `java/scripts/codegen/package.json` | +| `CHANGELOG.md` | `java/CHANGELOG.md` | +| `README.md` | `java/README.md` (replaces existing placeholder) | +| `jbang-example.java` | `java/jbang-example.java` | +| `.lastmerge` | `java/.lastmerge` | +| `docs/adr/` | `java/docs/adr/` | +| `mvnw` | `java/mvnw` | +| `mvnw.cmd` | `java/mvnw.cmd` | +| `.mvn/` | `java/.mvn/` | +| `.gitignore` | `java/.gitignore` | +| `test` (single file, not a directory) | `java/test` | + +**DO NOT copy:** + +- `.githooks/` β€” already handled separately (strikethrough in plan) +- `instructions/copilot-sdk-java.instructions.md` β€” already handled separately (strikethrough in plan) +- `.github/` β€” workflows are Phase 2+, not Phase 1 +- `.git/` β€” never copy git internals +- `target/` β€” build artifacts, never copy +- `.claude/` β€” not needed +- `.vscode/` β€” not needed +- `20260430-*.txt` β€” log files, not needed +- `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `LICENSE`, `SECURITY.md`, `SUPPORT.md` β€” these exist at the monorepo root already + +### Step 2: Update `pom.xml` paths if needed + +The `pom.xml` should be self-contained under `java/`. Review it for any paths that assume it lives at the repository root. Key things to check and fix: + +1. **Test harness clone**: The current `pom.xml` likely has a `maven-antrun-plugin` execution that clones `https://github.com/github/copilot-sdk` into `target/copilot-sdk/` to get `test/harness/` and `test/snapshots/`. Since these directories now exist locally in the same repo at `../../test/harness/` and `../../test/snapshots/` (relative to `java/`), **replace the git clone with a local copy or symlink**. The simplest approach: change the antrun execution to copy from `${project.basedir}/../test/` instead of cloning from GitHub. + +2. **Any absolute or root-relative paths** that reference the repo root β€” these should be adjusted to work from `java/` as the working directory. + +3. **The `` section** β€” update URLs from `github/copilot-sdk-java` to `github/copilot-sdk` and adjust paths if needed. + +### Step 3: Verify `mvn clean verify` works from `java/` + +Run `cd java && mvn clean verify` and fix any issues. The build must pass. Common issues to expect: + +- Test harness path references (from Step 2) +- Any hardcoded paths in test infrastructure that assume repo root = Java project root +- The `E2ETestContext` or `CapiProxy` classes may reference `target/copilot-sdk/test/harness/` β€” these need to point to `../../test/` (or however the local copy is structured after Step 2) + +If tests fail, diagnose and fix. Do NOT skip tests. The goal is a green `mvn clean verify` from `java/`. + +### Commit Strategy + +Make commits as you go: + +1. After copying the source files (Step 1) +2. After updating `pom.xml` and test infrastructure (Step 2) +3. After fixing any build/test issues (Step 3) + +Use descriptive commit messages like: + +- "Copy Java SDK source files into java/ directory" +- "Update pom.xml to use local test harness instead of git clone" +- "Fix E2E test paths for monorepo layout" + +## Constraints + +- **DO NOT** create or modify any GitHub Actions workflow files (`.github/workflows/`) +- **DO NOT** modify `.github/copilot-instructions.md` +- **DO NOT** modify the `justfile` +- **DO NOT** modify `CODEOWNERS` +- **DO NOT** modify `dependabot.yaml` +- **DO NOT** modify `copilot-setup-steps.yml` +- **DO NOT** touch any files under `nodejs/`, `python/`, `go/`, `dotnet/`, `rust/` +- **DO NOT** perform Phase 2, 3, 4, 5, or 6 work +- **DO NOT** modify files under `java/src/generated/java/` beyond what was copied β€” these are auto-generated +- You MAY modify `java/pom.xml`, `java/src/test/java/**`, and `java/src/main/java/**` as needed to get the build passing diff --git a/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md b/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md new file mode 100644 index 000000000..9f13ae79f --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/dd-2998002-make-the-monorepo-smart-about-java-plan.md @@ -0,0 +1,278 @@ +# DD-2998002: Make the Monorepo Smart About Java + +## Goal + +Make the `copilot-sdk` monorepo's Copilot configuration aware of Java so that AI coding agents receive Java-specific guidance when editing Java files. This involves two deliverables: + +1. **Create `.github/skills/java-coding-skill/SKILL.md`** β€” a new skill containing Java SDK API patterns and coding rules (sourced from the standalone Java repo's `instructions/copilot-sdk-java.instructions.md`). +2. **Add a Java section to `.github/copilot-instructions.md`** β€” concise Java governance (build commands, architecture, test conventions) that parallels the existing Node, Python, Go, .NET entries. + +## Context + +- The monorepo uses **skills** for language-specific coding guidance (not `instructions/` directories). +- The only existing language-specific skill is `rust-coding-skill` at `.github/skills/rust-coding-skill/SKILL.md`. +- No other language (Node, Python, Go, .NET) has a dedicated skill β€” only Rust does. +- The Java SDK source will live under `java/` in the monorepo (per the Phase 1 migration plan). +- The standalone Java repo has two Copilot configuration files: + - `instructions/copilot-sdk-java.instructions.md` (~757 lines) β€” API usage patterns, coding rules, examples + - `.github/copilot-instructions.md` (~260 lines) β€” repo governance: build commands, architecture, testing conventions, boundaries, security + +## Execution Context + +- You are running from the **monorepo root** (`copilot-sdk-00/`). +- The standalone Java SDK repo is available at **`../copilot-sdk-java-00/`** (a sibling directory). +- All files you create or edit are in the monorepo (current directory). The Java repo is read-only β€” you only read source files from it. + +## Non-Goals + +- Do NOT copy `instructions/` as a directory to `java/instructions/` β€” that doesn't match the monorepo convention. +- Do NOT create skills for other languages β€” only Java is being added. +- Do NOT modify any Java source code, tests, or build files. +- Do NOT modify any existing skills (e.g., `rust-coding-skill`). +- Do NOT modify any files in `../copilot-sdk-java-00/` β€” it is a read-only source. + +--- + +## Checklist + +### Step 1: Read the existing `rust-coding-skill` to understand the pattern + +- [ ] Read `.github/skills/rust-coding-skill/SKILL.md` to understand the YAML frontmatter structure and content organization. + +The frontmatter uses exactly these fields: + +```yaml +--- +name: rust-coding-skill +description: "Use this skill whenever editing `*.rs` files in the `rust/` SDK in order to write idiomatic, efficient, well-structured Rust code" +--- +``` + +### Step 2: Read the Java instructions source file + +- [ ] Read the full content of the Java instructions file at **`../copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md`**. + - **Fallback** (if the file is not found at that path): The structure is described in [Appendix A](#appendix-a-java-instructions-source-content) below. + +### Step 3: Create `.github/skills/java-coding-skill/SKILL.md` + +- [ ] Create the directory `.github/skills/java-coding-skill/` +- [ ] Create `.github/skills/java-coding-skill/SKILL.md` with: + - YAML frontmatter (see template below) + - Body content adapted from the Java instructions file + +**YAML frontmatter** β€” use exactly this: + +```yaml +--- +name: java-coding-skill +description: "Use this skill whenever editing `*.java` files in the `java/` SDK in order to write idiomatic, well-structured Java code for the Copilot SDK" +--- +``` + +**Body content** β€” take the full content of `../copilot-sdk-java-00/instructions/copilot-sdk-java.instructions.md` (everything after its YAML frontmatter) and make these adaptations: + +1. **Remove the old YAML frontmatter** (`applyTo`, `description`, `name` fields from the instructions file). Replace it with the new frontmatter above. +2. **Add a title line** after the frontmatter: `# Java Coding Skill` +3. **Update paths to reflect monorepo layout**: + - References to `src/` β†’ `java/src/` + - References to `pom.xml` β†’ `java/pom.xml` + - References to `config/` β†’ `java/config/` + - References to `scripts/codegen/` β†’ `scripts/codegen/` (codegen lives at monorepo root) + - References to `target/` β†’ `java/target/` + - References to `.lastmerge` β†’ `java/.lastmerge` + - References to `.githooks/` β†’ `java/.githooks/` + - References to `src/site/` β†’ `java/src/site/` + - References to `src/generated/java/` β†’ `java/src/generated/java/` +4. **Keep all code examples unchanged** β€” they show API usage, not file paths. +5. **Keep all sections** β€” Core Principles, Installation, Client Initialization, Session Management, Event Handling, Streaming, Custom Tools, Permission Handling, User Input, System Message, File Attachments, Message Delivery, Send and Wait, Multiple Sessions, BYOK, Session Lifecycle, Error Handling, Connectivity Testing, Status/Auth, Resource Cleanup, Best Practices, Common Patterns. +6. **Do NOT add content that isn't in the source** β€” no new sections, no commentary. + +### Step 4: Read the monorepo's existing `.github/copilot-instructions.md` + +- [ ] Read `.github/copilot-instructions.md` to understand the current structure and where Java should be added. + +The current file has these sections: + +- Big picture πŸ”§ +- Most important files to read first πŸ“š +- Developer workflows ▢️ (per-language subsection) +- Testing & E2E tips βš™οΈ +- Project-specific conventions & patterns βœ… +- Integration & environment notes ⚠️ +- Where to add new code or tests 🧭 + +### Step 5: Read the Java repo's `.github/copilot-instructions.md` + +- [ ] Read the Java repo's governance file at **`../copilot-sdk-java-00/.github/copilot-instructions.md`** to extract the content that needs to be merged. + - **Fallback** (if the file is not found at that path): The content to merge is provided in [Appendix B](#appendix-b-java-governance-content-to-merge) below. + +### Step 6: Add Java to `.github/copilot-instructions.md` + +- [ ] Make the following additions to the monorepo's `.github/copilot-instructions.md`: + +**6a. Update "Big picture" section:** + +Change: + +``` +The repo implements language SDKs (Node/TS, Python, Go, .NET) that speak to the **Copilot CLI** +``` + +To: + +``` +The repo implements language SDKs (Node/TS, Python, Go, .NET, Java) that speak to the **Copilot CLI** +``` + +And add Java's CLI URL option to the typical flow line. Current: + +``` +(Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`) +``` + +Updated: + +``` +(Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`, Java: `cliUrl`) +``` + +**6b. Update "Most important files to read first" section:** + +Add: + +``` +- Java: `java/README.md`, `java/pom.xml` +``` + +**6c. Update "Developer workflows" per-language section:** + +Add a Java entry after the .NET entry: + +``` + - Java: `cd java && mvn clean verify` (full build + tests), `mvn spotless:apply` (format code before commit) + - **Java testing note:** Always use `mvn verify` without `-q` and without piping through `grep`. Never add `InternalsVisibleTo` equivalent β€” tests must only access public APIs. +``` + +**6d. Update "Testing & E2E tips" section:** + +Add after the existing E2E description: + +``` +- Java E2E tests use `E2ETestContext` which manages a `CapiProxy` (Node.js replaying proxy). The harness is cloned during Maven's `generate-test-resources` phase to `java/target/copilot-sdk/`. +``` + +**6e. Update "Where to add new code or tests" section:** + +Add Java to the lists: + +- SDK code line: add `java/src/main/java` +- Unit tests line: add `java/src/test/java` +- E2E tests line: add `java/src/test/java/**/e2e/` +- Generated types line: add `java/src/generated/java` + +**6f. Update "Integration & environment notes" section:** + +Add Java's CLI URL option. Current: + +``` +(Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`) +``` + +Updated: + +``` +(Node: `cliUrl`, Go: `CLIUrl`, .NET: `CliUrl`, Python: `cli_url`, Java: `cliUrl`) +``` + +Add environment note: + +``` +- Java requires JDK 17+ and Maven 3.9+. Java E2E tests also require Node.js (for the replay proxy). +``` + +### Step 7: Verify + +- [ ] Confirm `.github/skills/java-coding-skill/SKILL.md` exists and has valid YAML frontmatter with `name` and `description` fields. +- [ ] Confirm `.github/copilot-instructions.md` mentions Java in all per-language lists (Big picture, Developer workflows, Most important files, Where to add code, Integration notes). +- [ ] Confirm no files were created under `java/instructions/` β€” that pattern is NOT used in this monorepo. +- [ ] Confirm `.github/skills/rust-coding-skill/` was NOT modified. + +--- + +## Appendix A: Java Instructions Source Content + +The source file is `instructions/copilot-sdk-java.instructions.md` from the `copilot-sdk-java` repository. Its YAML frontmatter is: + +```yaml +--- +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" +--- +``` + +The body contains these sections (in order): + +1. Core Principles +2. Installation (Maven, Gradle) +3. Client Initialization (Basic, Options, Manual Server Control) +4. Session Management (Creating, Config Options, Resuming, Operations) +5. Event Handling (Subscription, Type-Safe, Unsubscribing, Event Types, Error Handling) +6. Streaming Responses (Enabling, Handling Events) +7. Custom Tools (Defining, Type-Safe Args, Overriding Built-In, Return Types, Execution Flow) +8. Permission Handling (Required Handler) +9. User Input Handling +10. System Message Customization (Append Mode, Replace Mode) +11. File Attachments +12. Message Delivery Modes +13. Convenience: Send and Wait +14. Multiple Sessions +15. Bring Your Own Key (BYOK) +16. Session Lifecycle Management (Listing, Deleting, Connection State, Lifecycle Events) +17. Error Handling (Standard Exceptions, Session Error Events) +18. Connectivity Testing +19. Status and Authentication +20. Resource Cleanup (Automatic, Manual) +21. Best Practices (12 items) +22. Common Patterns (Simple Query-Response, Event-Driven, Multi-Turn, Complex Tools, Session Hooks) + +The file is ~757 lines. The agent MUST read the full file from the source repo or the monorepo copy β€” do not truncate or summarize. + +## Appendix B: Java Governance Content to Merge + +The source file is `.github/copilot-instructions.md` from the `copilot-sdk-java` repository. Key content to extract and merge into the monorepo's `copilot-instructions.md`: + +**Build & Test Commands** (merge into Developer workflows): + +- `mvn clean verify` β€” full build + tests +- `mvn test -Dtest=ClassName` β€” single test class +- `mvn test -Dtest=ClassName#method` β€” single test method +- `mvn spotless:apply` β€” format code (required before commit) +- `mvn spotless:check` β€” check formatting only +- `mvn clean package -DskipTests` β€” build without tests +- AI agent testing rule: always use `mvn verify` without `-q`, never pipe through `grep` + +**Architecture** (reference from skill, don't duplicate): + +- CopilotClient, CopilotSession, JsonRpcClient +- Package structure: `com.github.copilot.sdk`, `.json`, `.generated` + +**Key Conventions** (merge selectively β€” most belongs in the skill): + +- Reference implementation merging pattern (keep in governance β€” it's repo-level policy) +- Code style: 4-space indent, Spotless, Checkstyle (keep in governance) +- Pre-commit hooks (keep in governance) + +**Boundaries and Restrictions** (keep in governance): + +- Do not edit `src/generated/java/` (auto-generated) +- Do not modify test snapshots in `target/copilot-sdk/test/snapshots/` +- Must run `gh aw compile` after editing agentic workflow `.md` files + +**Security Guidelines** (keep in governance): + +- Never commit secrets +- Use try-with-resources, StandardCharsets.UTF_8 +- Review dependencies for vulnerabilities + +**NOTE**: The governance content that goes into `copilot-instructions.md` should be CONCISE β€” just enough for an agent to know how to build, test, and follow repo rules. The detailed API patterns and coding examples belong in the skill file, not in governance. diff --git a/80-java-monorepo-add-01-remove-before-merge/dd-3001051-phase-2-plan.md b/80-java-monorepo-add-01-remove-before-merge/dd-3001051-phase-2-plan.md new file mode 100644 index 000000000..6231407a7 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/dd-3001051-phase-2-plan.md @@ -0,0 +1,231 @@ +# Phase 2: CI Workflows β€” Agent Prompt + +## Context + +You are working in `copilot-sdk-00`, a local clone of `https://github.com/github/copilot-sdk`. The Java SDK source repository is at `../copilot-sdk-java-00` (a local clone of `https://github.com/github/copilot-sdk-java`). + +You are implementing one phase of the work to make it so the `copilot-sdk/java` directory is the new home for what is currently `copilot-sdk-java`, with all sournce code, workflows and maintenance affordances migrated. + +Phase 0 (pre-flight) and Phase 1 (copy source code) are already complete. The Java source code is already present under `java/` in this repository and `mvn clean verify` passes from that directory. + +## First Step β€” Read the Master Plan + +Before doing any work, read the file: + +``` +80-java-monorepo-add-01-remove-before-merge/dd-2989727-move-java-to-monorepo-plan.md +``` + +This contains the full migration plan. Focus on the "Phase 2: CI Workflows" section, but also review the naming conventions in Β§3 and the workflow inventory tables in Β§5 for context. + +**You are executing Phase 2 only. Do NOT perform any other phases (Phase 0, 1, 3, 4, 5, or 6).** + +## Working Branch and Commits + +You are safe to do all work on the current topic branch. Make fine-grained commits with clear, descriptive commit log messages (e.g., "Add java-sdk-tests.yml adapted from build-test.yml", "Add Java job to codegen-check.yml"). Do not squash β€” keep commits granular so they are easy to review. + +## Phase 2 Goal + +Java CI runs on PRs and pushes to `main` within the monorepo. This phase creates/updates 5 things: + +1. A new `java-sdk-tests.yml` workflow +2. A merged Java job in the existing `codegen-check.yml` +3. A new `java-codegen-agentic-fix.md` agentic workflow (+ compiled `.lock.yml`) +4. Java tooling added to the existing `copilot-setup-steps.yml` +5. Maven ecosystem entry added to the existing `dependabot.yaml` + +--- + +## Task 1: Create `.github/workflows/java-sdk-tests.yml` + +**Source to adapt from:** `../copilot-sdk-java-00/.github/workflows/build-test.yml` + +**Reference for monorepo style:** `.github/workflows/dotnet-sdk-tests.yml` or `.github/workflows/go-sdk-tests.yml` (read one of these to match trigger structure and job naming conventions). + +### Requirements + +- **Triggers:** + - `push` to `main` with paths: `java/**`, `test/**`, `.github/workflows/java-sdk-tests.yml` + - `pull_request` with same paths + - `workflow_dispatch` + +- **OS matrix:** Run on `ubuntu-latest`, `windows-latest`, `macos-latest` (match other SDK test workflows in this repo). + +- **JDK version:** 17 (use `actions/setup-java` with `temurin` distribution). + +- **Steps (adapt from `build-test.yml`):** + 1. Checkout + 2. Set up JDK 17 + 3. Cache Maven dependencies (`~/.m2/repository`) + 4. Set up Node.js (needed for E2E test harness β€” the replay proxy is Node-based, located at `test/harness/`) + 5. Run `mvn spotless:check` (formatting gate) + 6. Run `mvn clean verify` (build + all tests including E2E) + 7. Upload test results (Surefire reports) as artifacts on failure + +- **Working directory:** All Maven commands must use `working-directory: ./java` + +- **Important differences from source:** + - The source `build-test.yml` clones the `copilot-sdk` repo at build time (via `generate-test-resources` Maven phase) to get `test/harness/` and `test/snapshots/`. In the monorepo these are already present at the repo root under `test/`. The `java/pom.xml` has already been updated in Phase 1 to reference the local `test/` directory, so no special handling is needed β€” just ensure the checkout is a full checkout (not shallow if tests need git history β€” check if this matters). + - The source has a `smoke-test` job that calls `run-smoke-test.yml`. Do NOT include the smoke test in this workflow β€” that is a Phase 3 concern (`java-smoke-test.yml`). + - The source has coverage badge generation. Do NOT include coverage badge generation β€” keep the workflow focused on build+test. + - The source has a Javadoc generation step. Include a `mvn javadoc:javadoc` step (non-failing, just to verify Javadoc compiles) OR fold it into the `mvn verify` if the POM already runs Javadoc during verify. Check `java/pom.xml` to see if Javadoc is part of the verify lifecycle. + +- **Do NOT include:** + - Smoke test job (Phase 3) + - Deploy/publish steps (Phase 3) + - Any cross-repo clone of `copilot-sdk` (no longer needed) + +--- + +## Task 2: Merge Java into `.github/workflows/codegen-check.yml` + +**Source to adapt from:** `../copilot-sdk-java-00/.github/workflows/codegen-check.yml` + +**Target to modify:** `.github/workflows/codegen-check.yml` (already exists in the monorepo) + +### Requirements + +- Read the existing `codegen-check.yml` to understand its structure. It already has jobs for Node, .NET, Python, Go, and Rust codegen verification. + +- **Add `java/src/generated/**` to the path triggers\*\* (both push and pull_request). + +- **Add a new job** (e.g., `java-codegen`) that: + 1. Checks out the repo + 2. Sets up Node.js (the Java codegen script `java/scripts/codegen/java.ts` is a TypeScript file that runs via `npx tsx`) + 3. Sets up JDK 17 (needed if the codegen script validates against Java compilation) + 4. Installs codegen dependencies: `cd java/scripts/codegen && npm ci` + 5. Runs the Java codegen: `cd java/scripts/codegen && npx tsx java.ts` + 6. Checks for uncommitted changes in `java/src/generated/` using `git diff --exit-code java/src/generated/` + 7. If changes exist, fails with a message indicating codegen is out of date + +- **Match the job structure** of the other language codegen jobs in the same file (same checkout action version, same diff pattern, etc.). + +--- + +## Task 3: Create `.github/workflows/java-codegen-agentic-fix.md` + +**Source to adapt from:** `../copilot-sdk-java-00/.github/workflows/codegen-agentic-fix.md` + +Also read the corresponding `.lock.yml` from the source to understand the compiled structure. + +### What codegen-check.yml and `codegen-agentic-fix` Actually Own + +These two workflows form a **self-contained codegen CI pipeline** with one concern only: + +> **Keep java in sync with whatever `@github/copilot` schemas are declared in package.json.** + +Their flow: + +1. codegen-check.yml: On PR or push, re-runs codegen β†’ if drift detected, pushes regen'd files β†’ if `mvn verify` fails, triggers the agentic fix +2. codegen-agentic-fix.lock.yml: AI agent that fixes `java.ts` and/or handwritten source until `mvn verify` passes, then pushes to the PR + +They do **not** interact with .lastmerge, the CLI version property, or the test harness clone. + +### Requirements + +- This is a `gh-aw` (GitHub Agentic Workflows) markdown file. It defines an agentic workflow that auto-fixes compilation/test failures caused by Java codegen changes. + +- **Adapt the source** with these changes: + - All paths updated to reflect the monorepo structure (e.g., `java/src/generated/`, `java/scripts/codegen/`, etc.) + - Remove any references to cross-repo operations + - Update the workflow trigger to fire when `java-codegen` job (from `codegen-check.yml`) fails + - Update instructions to run `cd java && mvn verify` for validation + - Update codegen command to `cd java/scripts/codegen && npx tsx java.ts` + +- **After creating the `.md` file**, compile it: + + ``` + gh aw compile java-codegen-fix + ``` + + This generates `.github/workflows/java-codegen-fix.lock.yml`. Both files must be committed. + +- **If `gh aw` is not available or the compile fails**, note this in a commit message and commit just the `.md` file. The `.lock.yml` can be generated later. + +--- + +## Task 4: Merge Java into `.github/workflows/copilot-setup-steps.yml` + +**Target to modify:** `.github/workflows/copilot-setup-steps.yml` (already exists) + +### Requirements + +- Read the existing `copilot-setup-steps.yml`. It sets up the environment for the Copilot coding agent (Node, Python, Go, .NET, Rust, etc.). + +- **Add the following steps** (in a logical position alongside other language setups): + 1. **Set up JDK 17:** + ```yaml + - uses: actions/setup-java@v4 + with: + distribution: "microsoft" + java-version: "17" + cache: "maven" + ``` + 2. **Set up Maven cache** (if not handled by the `cache: 'maven'` option above, add explicit caching of `~/.m2/repository`). + 3. **Enable Java git hooks** (the Java pre-commit hook): + ```yaml + - name: Enable Java pre-commit hook + run: | + cd java + git config core.hooksPath .githooks + ``` + Only add this if `java/.githooks/pre-commit` exists. Check first. + +- **Also check** `../copilot-sdk-java-00/.github/workflows/copilot-setup-steps.yml` for any other setup steps that the Java coding agent environment needs (e.g., `gh aw` installation, specific npm global installs). Port those over if they aren't already present in the monorepo version. + +--- + +## Task 5: Update `.github/dependabot.yaml` + +**Target to modify:** `.github/dependabot.yaml` (already exists) + +### Requirements + +- Read the existing file to understand its structure. + +- **Add a Maven ecosystem entry** for the `java/` directory: + + ```yaml + - package-ecosystem: "maven" + directory: "/java" + schedule: + interval: "weekly" + labels: + - "dependencies" + - "sdk/java" + ``` + +- Match the style (labels, schedule interval, grouping) of the existing entries. If other entries use `groups`, add an appropriate group for Java dependencies. + +- **Also check** if `../copilot-sdk-java-00/.github/dependabot.yml` has any additional ecosystems configured (e.g., `github-actions` scoped to Java workflows, or `npm` for `java/scripts/codegen/`). If so, add those entries too. + +--- + +## Verification + +After completing all 5 tasks, verify: + +1. **YAML syntax:** Run `python -c "import yaml; yaml.safe_load(open('.github/workflows/java-sdk-tests.yml'))"` (or equivalent) to check YAML validity of new/modified workflow files. +2. **No broken references:** Ensure no workflow file references actions or paths that don't exist. +3. **Consistent style:** The new `java-sdk-tests.yml` should look similar in structure to `dotnet-sdk-tests.yml` or `go-sdk-tests.yml`. +4. **Commit each task separately** with a clear commit message. + +## Summary of Files to Create/Modify + +| Action | File | +| ---------- | ------------------------------------------------------------------- | +| **Create** | `.github/workflows/java-sdk-tests.yml` | +| **Modify** | `.github/workflows/codegen-check.yml` | +| **Create** | `.github/workflows/java-codegen-fix.md` | +| **Create** | `.github/workflows/java-codegen-fix.lock.yml` (via `gh aw compile`) | +| **Modify** | `.github/workflows/copilot-setup-steps.yml` | +| **Modify** | `.github/dependabot.yaml` | + +## Reminders + +- All Maven commands use `working-directory: ./java` +- Do NOT touch Phase 3 (publish), Phase 4 (agentic sync), Phase 5 (cross-cutting), or Phase 6 (cutover) +- Do NOT modify `java/pom.xml` or any Java source code +- Do NOT modify `test/harness/` or `test/snapshots/` +- Follow the naming convention: language-specific workflows use `java-` prefix +- Make fine-grained commits on the current branch diff --git a/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-branch-protection-findings-01.md b/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-branch-protection-findings-01.md new file mode 100644 index 000000000..c08d65341 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-branch-protection-findings-01.md @@ -0,0 +1,205 @@ +# GHCP-SP-95: Branch Protection Findings + +**Date:** 2025-05-15 +**Issue:** https://github.com/github/copilot-sdk-partners/issues/95 +**Context:** Phase 0 investigation into whether `maven-release-plugin` can be replaced by a CI-friendly alternative to eliminate the need for a branch protection bypass on `main`. + +--- + +## 1. Summary of the Question + +Steve's agent claimed: + +1. `maven-release-plugin` is "legacy" and major projects have moved away from it. +2. CI-friendly versions (`${revision}` + `flatten-maven-plugin`) would eliminate the need for branch protection bypasses. +3. The `-SNAPSHOT` lifecycle is "arguably unnecessary." +4. The trade-off is re-engineering the release workflow, but it's a "well-documented path." + +Steve asked Ed to investigate the cost of item 4 β€” and whether the cost is low enough to do before GA, or should be deferred. + +This document evaluates each claim against evidence. + +--- + +## 2. Is `maven-release-plugin` Actually Legacy? + +**No.** The plugin is actively maintained by Apache: + +| Version | Release Date | +| ------- | ------------ | +| 3.3.1 | 2025-12-09 | +| 3.3.0 | 2025-11-30 | +| 3.2.0 | 2025-11-04 | +| 3.1.1 | 2024-07-11 | +| 3.1.0 | 2024-06-14 | + +Three releases in the last two months of 2025 alone. The 3.x line is a major rewrite from the 2.x series. This is not an abandoned plugin. + +### Do Small Projects Still Use It? + +Yes. The agent's claim conflated "Spring/Quarkus don't use it" with "nobody uses it." Those projects have custom build infrastructure **because they are multi-module monorepos with hundreds of modules**. A single-module library like `copilot-sdk-java` is the exact use case `maven-release-plugin` was designed for. + +Examples of actively maintained single-module or small-module-count Maven projects that use `maven-release-plugin` in 2025–2026: + +- **Apache Maven plugins themselves** (maven-compiler-plugin, maven-surefire, etc.) all use `maven-release-plugin` for their own releases. +- **Many Apache commons libraries** (commons-lang3, commons-io) use it. +- The plugin's own page at https://maven.apache.org/plugins/maven-release-plugin/ shows its last published version is 3.3.1 (2025-12-09). + +The trend away from `maven-release-plugin` is real **for large multi-module projects**, but for a single-artifact library, it remains the most battle-tested, lowest-maintenance option. + +### What About the Agent's Claim About Spring, Apache, and Quarkus? + +The agent was **partially correct but misleading**: + +- **Spring** uses Gradle, not Maven, so the comparison is irrelevant. +- **Quarkus** is a 900+ module monorepo β€” they need custom tooling regardless. +- **Apache sub-projects** vary: many small ones (the Maven plugins, commons libraries) still use `maven-release-plugin`. The large ones (Kafka, Beam) don't, but they have dedicated release engineering teams. + +The relevant comparison for `copilot-sdk-java` is other single-artifact Maven libraries, not framework monorepos. + +### Usage Data from Maven Central (mvnrepository.com) + +`maven-release-plugin` is ranked **#9 in the Maven Plugins category** and has **28,957 published artifacts** that declare it as a dependency β€” nearly 29,000 distinct Maven projects on Central use it in their build. + +To put that number in context, the "Used By" list for `maven-release-plugin` is sorted by popularity of the _dependent_ artifact. The **top 10 most popular artifacts that use `maven-release-plugin`** are: + +| Rank | Artifact | Own "Used By" Count | +| ---- | ------------------------------- | ------------------- | +| 1 | JUnit | 141,118 | +| 2 | Apache Maven Compiler Plugin | 119,190 | +| 3 | Apache Maven Source Plugin | 82,729 | +| 4 | Apache Maven Javadoc Plugin | 80,532 | +| 5 | Apache Maven JAR Plugin | 55,934 | +| 6 | Apache Maven GPG Plugin | 48,317 | +| 7 | Jackson Databind | 39,358 | +| 8 | Logback Classic | 33,374 | +| 9 | Maven Bundle Plugin | 31,856 | +| 10 | **Maven Release Plugin itself** | 28,957 | + +This means the most foundational artifacts in the Java ecosystem β€” JUnit, Jackson, Logback, and Maven's own core plugins β€” all use `maven-release-plugin` in their build. These are not legacy holdouts; they are the infrastructure that every Java project depends on. + +Version-level download counts also show sustained adoption of the 3.x line: + +| Version | Downloads | Release Date | +| -------------- | --------- | ------------ | +| 3.3.1 | 396 | Dec 13, 2025 | +| 3.3.0 | 32 | Dec 03, 2025 | +| 3.2.0 | 174 | Nov 08, 2025 | +| 3.1.1 | 1,218 | Jul 14, 2024 | +| 3.0.1 | 1,204 | Jun 03, 2023 | +| 2.5.3 (legacy) | 9,640 | Oct 14, 2015 | + +Note: The lower counts on the newest 3.3.x versions are expected β€” they were released only 5 months ago, and many projects pin to a version and update on their own schedule. The 3.1.1 version (11 months old) already has 1,218 downloads, showing healthy adoption of the 3.x line. + +--- + +## 3. The CI-Friendly Alternative: What Would It Cost? + +The alternative approach uses: + +1. **CI-friendly versions** β€” `${revision}` in pom.xml with `flatten-maven-plugin` +2. **`central-publishing-maven-plugin`** (Sonatype's new portal plugin) or plain `mvn deploy` for publishing +3. **GitHub Actions** manages versioning: tags drive the version, no commits to pom.xml needed + +### What the Rewrite Would Involve + +| Component | Change Required | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `pom.xml` | Replace hardcoded version with `${revision}${changelist}`, add `flatten-maven-plugin`, replace `maven-release-plugin` with `central-publishing-maven-plugin` or keep `maven-deploy-plugin` | +| `java-publish-maven.yml` | Complete rewrite: remove `release:prepare`/`release:perform`, replace with `mvn deploy -Drevision=X.Y.Z -Dchangelist=`, add explicit `git tag` + `git push --tags` steps | +| `java-publish-snapshot.yml` | Moderate rewrite: pass `-Drevision=X.Y.Z -Dchangelist=-SNAPSHOT` | +| Version calculation scripts | Rewrite β€” currently `maven-release-plugin` computes next version; would need custom shell logic (which we already partially have in the "Determine versions" step) | +| `.lastmerge` / changelog scripts | Update references to how version is determined | +| ADR-002 | Update to reflect new versioning scheme (tag format stays the same) | +| Rollback logic | Rewrite β€” no more `mvn release:rollback`; instead, delete the tag and the workflow is idempotent | +| Local developer workflow | `mvn install` still works (uses default `${revision}` from properties); `mvn release:prepare` no longer available for local releases | + +### Estimated Effort + +| Aspect | Estimate | +| ---------------------------- | ------------------------------------------------------------------------------------------------ | +| pom.xml changes | Small (1–2 hours) | +| Workflow rewrite | Medium (4–8 hours) β€” this is the bulk of the work | +| Testing the new publish flow | Medium (4–8 hours) β€” need a dry-run against Maven Central staging | +| Updating docs, ADR, scripts | Small (2–3 hours) | +| Risk of breaking a release | **Medium** β€” the current flow has been tested through 3 published releases; new flow is untested | +| **Total** | **~1.5–2.5 days of focused work** | + +--- + +## 4. Does `central-publishing-maven-plugin` Matter Here? + +**It's a separate concern that could be done independently.** + +`central-publishing-maven-plugin` (currently at v0.10.0) is Sonatype's new recommended way to deploy to Maven Central via their portal API. It replaces the older `nexus-staging-maven-plugin` / OSSRH staging workflow. + +Key facts: + +- It works with **either** `maven-release-plugin` **or** CI-friendly versions. It's orthogonal to the branch-protection question. +- Our current `publish-maven.yml` uses `mvn release:perform -Dgoals="deploy"`, which invokes the standard `maven-deploy-plugin`. This still works β€” Sonatype hasn't deprecated the old OSSRH route yet. +- Switching to `central-publishing-maven-plugin` would be a good modernization step but **does not affect whether we need branch protection bypass**. +- The snapshot workflow already uses plain `mvn deploy`, which also works with the old route. + +**Recommendation:** Consider switching to `central-publishing-maven-plugin` as a separate, lower-priority improvement. It does not intersect with the branch-protection decision. + +--- + +## 5. The Real Trade-Off + +### Keeping `maven-release-plugin` (Status Quo) + +| Pros | Cons | +| --------------------------------------------- | ---------------------------------------------------------------- | +| Already working and tested through 3 releases | Requires branch protection bypass for one workflow | +| Battle-tested pattern, well-understood | The bypass is a permanent exception in repo policy | +| Plugin actively maintained (v3.3.1, Dec 2025) | Commits directly to `main` (though mechanical, not code changes) | +| Rollback built-in (`mvn release:rollback`) | | +| Zero additional engineering work | | + +### Switching to CI-Friendly Versions + +| Pros | Cons | +| ---------------------------------------------------------- | --------------------------------------------------------- | +| No branch protection bypass needed | ~2 days of engineering work | +| pom.xml never changes in `main` for releases | New, untested publish pipeline | +| Aligns with Maven's stated modern direction | More shell scripting in workflows (version calc, tagging) | +| Version is always derived from tag, single source of truth | `flatten-maven-plugin` adds build complexity | +| | Loss of `mvn release:rollback` safety net | +| | Risk of a broken release during cutover | + +--- + +## 6. Recommendation + +**Keep `maven-release-plugin` for GA. Defer the CI-friendly migration to a post-GA improvement.** + +Rationale: + +1. **Risk vs. reward timing is wrong.** We're in Phase 0 of a monorepo migration. Adding a release infrastructure rewrite on top of the migration increases risk for no immediate user benefit. The branch protection bypass is scoped to a single `workflow_dispatch`-triggered workflow β€” it's not a standing vulnerability. + +2. **The plugin is not legacy.** v3.3.1 was released December 2025. It's the most actively maintained it's been in years. The agent's characterization was inaccurate. + +3. **The bypass scope is minimal.** Only `java-publish.yml` needs it. The bypass can be configured as a ruleset exception for a specific GitHub App or PAT, limited to commits matching `[maven-release-plugin]*` patterns. + +4. **Post-GA is the right time.** After GA, when the monorepo migration is complete and the release cadence is established, a CI-friendly migration can be done as a focused improvement with proper testing, including dry-run publishes to Maven Central staging. + +### If Steve Requires No Bypass Before GA + +If the monorepo maintainer absolutely cannot grant a branch protection bypass, the fallback is: + +1. Switch to CI-friendly versions + `central-publishing-maven-plugin` (~2 days work) +2. Accept the risk of a new, untested release pipeline during migration +3. Plan for at least one "dry-run" release to validate the pipeline before the first real GA publish + +This is doable but adds unnecessary risk during an already complex migration phase. + +--- + +## 7. Action Items + +| # | Action | Priority | Timing | +| --- | ----------------------------------------------------------------------------------------------- | -------- | ------------- | +| 1 | Request branch protection bypass scoped to `JAVA_RELEASE_TOKEN` PAT for `java-publish.yml` only | High | Now (Phase 0) | +| 2 | Document the bypass in the monorepo's security/access policy | Medium | Phase 2 | +| 3 | Evaluate CI-friendly version migration as post-GA improvement | Low | Post-GA | +| 4 | Evaluate `central-publishing-maven-plugin` adoption (orthogonal) | Low | Post-GA | diff --git a/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-enable-java-branch-protection.ps1 b/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-enable-java-branch-protection.ps1 new file mode 100644 index 000000000..4cdf46686 --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/ghcp-sp-95-enable-java-branch-protection.ps1 @@ -0,0 +1,55 @@ +# Create repository ruleset for github/copilot-sdk per issue ghcp-sp-95 +# To revert: gh api repos/github/copilot-sdk/rulesets/ -X DELETE + +$payload = @' +{ + "name": "ghcp-sp-95-java-branch-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["refs/heads/main"], + "exclude": [] + } + }, + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "always" + } + ], + "rules": [ + { "type": "deletion" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 1, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + }, + { "type": "non_fast_forward" } + ] +} +'@ + +$response = $payload | gh api repos/github/copilot-sdk/rulesets -X POST --input - + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create ruleset. Exit code: $LASTEXITCODE" + exit 1 +} + +$parsed = $response | ConvertFrom-Json +$rulesetId = $parsed.id + +Write-Host "Ruleset created successfully." -ForegroundColor Green +Write-Host " Name: $($parsed.name)" +Write-Host " ID: $rulesetId" +Write-Host "" +Write-Host "To revert this change:" -ForegroundColor Yellow +Write-Host " gh api repos/github/copilot-sdk/rulesets/$rulesetId -X DELETE" diff --git a/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh b/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh new file mode 100644 index 000000000..238cf768b --- /dev/null +++ b/80-java-monorepo-add-01-remove-before-merge/ghcpsp-90-gpg-key-archive.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Create an encrypted, signed handoff package for a GPG keypair. +# +# Usage: +# ./ghcpsp-90-gpg-key-archive.sh [output-dir] +# +# Example: +# ./ghcpsp-90-gpg-key-archive.sh 0123ABCD jane@example.com ./out + +usage() { + cat <<'EOF' +Usage: + ghcpsp-90-gpg-key-archive.sh [output-dir] + +Arguments: + key-id-or-fingerprint The secret key to export and hand off. + recipient-key-id-or-email Recipient key used to encrypt the bundle. + output-dir Optional output directory (default: current directory). + +Outputs: + .tar.gz Plain archive containing transfer files. + .tar.gz.asc Encrypted + signed archive for transfer. + +Notes: + - Share the passphrase over a separate channel. + - Keep the plain archive only as long as needed, then securely delete it. +EOF +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" >&2 + exit 1 + fi +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ $# -lt 2 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +KEY_ID="$1" +RECIPIENT="$2" +OUTPUT_DIR="${3:-.}" + +require_cmd gpg +require_cmd tar +require_cmd awk +require_cmd sed +require_cmd date + +mkdir -p "$OUTPUT_DIR" + +# Confirm key material is available locally. +if ! gpg --list-secret-keys "$KEY_ID" >/dev/null 2>&1; then + echo "Error: no secret key found for: $KEY_ID" >&2 + exit 1 +fi + +# Confirm recipient key exists locally for encryption. +if ! gpg --list-keys "$RECIPIENT" >/dev/null 2>&1; then + echo "Error: recipient public key not found locally: $RECIPIENT" >&2 + exit 1 +fi + +FPR="$(gpg --list-secret-keys --with-colons "$KEY_ID" | awk -F: '/^fpr:/ {print $10; exit}')" +if [[ -z "$FPR" ]]; then + echo "Error: unable to determine fingerprint for key: $KEY_ID" >&2 + exit 1 +fi + +SHORT_FPR="${FPR: -16}" +STAMP="$(date -u +%Y%m%dT%H%M%SZ)" +PREFIX="gpg-key-handoff-${SHORT_FPR}-${STAMP}" + +WORK_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +BUNDLE_DIR="$WORK_DIR/$PREFIX" +mkdir -p "$BUNDLE_DIR" + +SECRET_ASC="$BUNDLE_DIR/secret-key.asc" +PUBLIC_ASC="$BUNDLE_DIR/public-key.asc" +FINGERPRINT_TXT="$BUNDLE_DIR/fingerprint.txt" +OWNERTRUST_TXT="$BUNDLE_DIR/ownertrust.txt" +NOTES_TXT="$BUNDLE_DIR/handoff-notes.txt" + +# Export key material. +gpg --armor --export-secret-keys "$KEY_ID" > "$SECRET_ASC" +gpg --armor --export "$KEY_ID" > "$PUBLIC_ASC" +gpg --export-ownertrust > "$OWNERTRUST_TXT" + +# Capture key identity details. +{ + echo "Primary fingerprint: $FPR" + echo + echo "Secret key listing:" + gpg --list-secret-keys --keyid-format LONG "$KEY_ID" + echo + echo "Public key listing:" + gpg --list-keys --keyid-format LONG "$KEY_ID" +} > "$FINGERPRINT_TXT" + +# Try to locate an existing revocation certificate (GnuPG default layout). +REVOCATION_ASC="$BUNDLE_DIR/revocation.asc" +REVOCATION_SOURCE="${GNUPGHOME:-$HOME/.gnupg}/openpgp-revocs.d/${FPR}.rev" +if [[ -f "$REVOCATION_SOURCE" ]]; then + cp "$REVOCATION_SOURCE" "$REVOCATION_ASC" +else + { + echo "Revocation certificate was not found at:" + echo " $REVOCATION_SOURCE" + echo + echo "If needed, generate one manually on a trusted host and add it to this bundle." + } > "$BUNDLE_DIR/revocation-missing.txt" +fi + +cat > "$NOTES_TXT" < [output-dir] [--import-ownertrust] +# +# Example: +# ./ghcpsp-90-gpg-key-import.sh ./gpg-key-handoff-....tar.gz.asc ./out --import-ownertrust + +usage() { + cat <<'EOF' +Usage: + ghcpsp-90-gpg-key-import.sh [output-dir] [--import-ownertrust] + +Arguments: + sealed-archive.asc Armored encrypted+signed archive produced by the sender. + output-dir Optional output directory for extracted files (default: ./recipient-import). + --import-ownertrust Optional: import ownertrust.txt from the bundle. + +What this script does: +1. Decrypts and validates the signed archive with gpg. +2. Extracts bundle contents. +3. Verifies fingerprint metadata exists. +4. Imports public and secret keys. +5. Optionally imports ownertrust. + +Important: +- Verify the reported fingerprint out-of-band before using the key. +- Key passphrase is required when the secret key is used, not necessarily at import. +EOF +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "Error: required command not found: $cmd" >&2 + exit 1 + fi +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ $# -lt 1 || $# -gt 3 ]]; then + usage >&2 + exit 1 +fi + +SEALED_ARCHIVE="$1" +OUTPUT_DIR="./recipient-import" +IMPORT_OWNERTRUST="false" + +for arg in "${@:2}"; do + case "$arg" in + --import-ownertrust) + IMPORT_OWNERTRUST="true" + ;; + *) + OUTPUT_DIR="$arg" + ;; + esac +done + +require_cmd gpg +require_cmd tar +require_cmd awk +require_cmd grep +require_cmd sed +require_cmd mkdir + +if [[ ! -f "$SEALED_ARCHIVE" ]]; then + echo "Error: archive not found: $SEALED_ARCHIVE" >&2 + exit 1 +fi + +umask 077 +mkdir -p "$OUTPUT_DIR" + +WORK_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +DECRYPTED_TAR="$WORK_DIR/handoff.tar.gz" +STATUS_LOG="$WORK_DIR/gpg-status.log" + +# Decrypt and capture machine-readable GPG status for integrity checks. +if ! gpg --batch --status-fd=1 --decrypt --output "$DECRYPTED_TAR" "$SEALED_ARCHIVE" > "$STATUS_LOG"; then + echo "Error: failed to decrypt/verify archive. Confirm recipient key and signer key are present." >&2 + exit 1 +fi + +if ! grep -q "^\[GNUPG:\] VALIDSIG " "$STATUS_LOG"; then + echo "Error: archive decrypted but signature validity could not be confirmed." >&2 + echo "Status log: $STATUS_LOG" >&2 + exit 1 +fi + +TOP_DIR="$(tar -tzf "$DECRYPTED_TAR" | awk -F/ 'NR==1 {print $1}')" +if [[ -z "$TOP_DIR" ]]; then + echo "Error: could not determine top-level bundle directory." >&2 + exit 1 +fi + +tar -xzf "$DECRYPTED_TAR" -C "$OUTPUT_DIR" +BUNDLE_DIR="$OUTPUT_DIR/$TOP_DIR" + +SECRET_ASC="$BUNDLE_DIR/secret-key.asc" +PUBLIC_ASC="$BUNDLE_DIR/public-key.asc" +FINGERPRINT_TXT="$BUNDLE_DIR/fingerprint.txt" +OWNERTRUST_TXT="$BUNDLE_DIR/ownertrust.txt" + +if [[ ! -f "$SECRET_ASC" || ! -f "$PUBLIC_ASC" || ! -f "$FINGERPRINT_TXT" ]]; then + echo "Error: bundle is missing expected files (secret/public/fingerprint)." >&2 + exit 1 +fi + +EXPECTED_FPR="$(awk -F': ' '/^Primary fingerprint:/ {print $2; exit}' "$FINGERPRINT_TXT" | sed 's/[[:space:]]*$//')" +if [[ -z "$EXPECTED_FPR" ]]; then + echo "Error: could not parse expected fingerprint from $FINGERPRINT_TXT" >&2 + exit 1 +fi + +echo "Expected fingerprint from bundle metadata: $EXPECTED_FPR" +echo "Verify this fingerprint out-of-band with the sender before trusting key usage." + +# Import public first, then secret material. +gpg --import "$PUBLIC_ASC" +gpg --import "$SECRET_ASC" + +if [[ "$IMPORT_OWNERTRUST" == "true" ]]; then + if [[ -f "$OWNERTRUST_TXT" ]]; then + gpg --import-ownertrust "$OWNERTRUST_TXT" + echo "Ownertrust imported from bundle." + else + echo "Warning: --import-ownertrust requested, but ownertrust.txt was not found." + fi +fi + +IMPORTED_FPR="$(gpg --with-colons --list-secret-keys "$EXPECTED_FPR" | awk -F: '/^fpr:/ {print $10; exit}')" +if [[ "$IMPORTED_FPR" != "$EXPECTED_FPR" ]]; then + echo "Error: imported key fingerprint does not match bundle metadata." >&2 + echo "Expected: $EXPECTED_FPR" >&2 + echo "Actual: ${IMPORTED_FPR:-}" >&2 + exit 1 +fi + +echo +echo "Import successful." +echo "Bundle extracted to: $BUNDLE_DIR" +echo "Imported fingerprint: $IMPORTED_FPR" +echo +echo "Recommended next steps:" +echo "1) Confirm fingerprint with sender through an independent channel." +echo "2) Store revocation certificate from the bundle in offline secure storage." +echo "3) Securely delete extracted secret-key material after operational handoff." diff --git a/java/.gitignore b/java/.gitignore new file mode 100644 index 000000000..654d3278f --- /dev/null +++ b/java/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +target +examples-test/ +.merge-env +blog-copilotsdk/ +.claude/worktrees +smoke-test +*job-logs.txt* +temporary-prompts/ +changebundle.txt* +.classpath +.project +.settings +scripts/codegen/node_modules/ +*~ +*.sln diff --git a/java/.lastmerge b/java/.lastmerge new file mode 100644 index 000000000..88ed2a952 --- /dev/null +++ b/java/.lastmerge @@ -0,0 +1 @@ +f6c1adf8329ad4206e5ed2e8d12fb8082bc841a2 diff --git a/java/.mvn/wrapper/maven-wrapper.properties b/java/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..8dea6c227 --- /dev/null +++ b/java/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/java/CHANGELOG.md b/java/CHANGELOG.md new file mode 100644 index 000000000..8e174dd21 --- /dev/null +++ b/java/CHANGELOG.md @@ -0,0 +1,535 @@ +# Changelog + +All notable changes to the GitHub Copilot SDK for Java will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +> Note: This file is automatically modified by scripts and coding agents. Do not change it manually. + +## [Unreleased] + +> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) + +## [1.0.0-beta-java.4] - 2026-05-16 + +> **Reference implementation sync:** [`github/copilot-sdk@e20f5be`](https://github.com/github/copilot-sdk/commit/e20f5bef125860accb30c60d1b35109371a77f16) +## [1.0.0-beta-java.3] - 2026-05-11 + +> **Reference implementation sync:** [`github/copilot-sdk@4a0437b`](https://github.com/github/copilot-sdk/commit/4a0437bb03a0b60a1867f14ae8e3faf053afa5aa) +## [1.0.0-beta-java.2] - 2026-05-08 + +> **Reference implementation sync:** [`github/copilot-sdk@066a69c`](https://github.com/github/copilot-sdk/commit/066a69c1e849adf1bd98564ab1b52316ec471182) +## [1.0.0-beta-java.1] - 2026-05-05 + +> **Reference implementation sync:** [`github/copilot-sdk@c063458`](https://github.com/github/copilot-sdk/commit/c063458ecc3d606766f04cf203b11b08de672cc8) +## [0.3.0-java.2] - 2026-04-26 + +> **Reference implementation sync:** [`github/copilot-sdk@dd2dcbc`](https://github.com/github/copilot-sdk/commit/dd2dcbc439256acfb9feb2cff07c0b9c820091b8) +## [0.3.0-java-preview.1] - 2026-04-21 + +> **Reference implementation sync:** [`github/copilot-sdk@922959f`](https://github.com/github/copilot-sdk/commit/922959f4a7b83509c3620d4881733c6c5677f00c) +## [0.3.0-java-preview.0] - 2026-04-21 + +> **Reference implementation sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) +## [0.2.2-java.1] - 2026-04-07 + +> **Reference implementation sync:** [`github/copilot-sdk@c3fa6cb`](https://github.com/github/copilot-sdk/commit/c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1) +### Added + +- Slash commands β€” register `/command` handlers invoked from the CLI TUI via `SessionConfig.setCommands()` (reference implementation: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) +- `CommandDefinition`, `CommandContext`, `CommandHandler`, `CommandWireDefinition` β€” types for defining and handling slash commands +- `CommandExecuteEvent` β€” event dispatched when a registered slash command is executed +- Elicitation (UI dialogs) β€” incoming handler via `SessionConfig.setOnElicitationRequest()` and outgoing convenience methods via `session.getUi()` (reference implementation: [`f7fd757`](https://github.com/github/copilot-sdk/commit/f7fd757)) +- `ElicitationContext`, `ElicitationHandler`, `ElicitationParams`, `ElicitationResult`, `ElicitationResultAction`, `ElicitationSchema`, `InputOptions` β€” types for elicitation +- `ElicitationRequestedEvent` β€” event dispatched when an elicitation request is received +- `SessionUiApi` β€” convenience API on `session.getUi()` for `confirm()`, `select()`, `input()`, and `elicitation()` calls +- `SessionCapabilities` and `SessionUiCapabilities` β€” session capability reporting populated from create/resume response +- `CapabilitiesChangedEvent` β€” event dispatched when session capabilities are updated +- `CopilotClient.getSessionMetadata(String)` β€” O(1) session lookup by ID +- `GetSessionMetadataResponse` β€” response type for `getSessionMetadata` + +### Fixed + +- Permission events already resolved by a pre-hook now short-circuit before invoking the client-side handler +- `SessionUiApi` Javadoc now uses valid Java null-check syntax instead of `?.` +- README updated to say "GitHub Copilot CLI 1.0.17" instead of "GitHub Copilot 1.0.17" + +## [0.2.1-java.1] - 2026-04-02 + +> **Reference implementation sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15) +## [0.2.1-java.0] - 2026-03-26 + +> **Reference implementation sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15) +### Added + +- `UnknownSessionEvent` β€” forward-compatible placeholder for event types not yet known to the SDK; unknown events are now dispatched to handlers instead of being silently dropped (reference implementation: [`d82fd62`](https://github.com/github/copilot-sdk/commit/d82fd62)) +- `PermissionRequestResultKind.NO_RESULT` β€” new constant that signals the handler intentionally abstains from answering a permission request, leaving it unanswered for another client (reference implementation: [`df59a0e`](https://github.com/github/copilot-sdk/commit/df59a0e)) +- `ToolDefinition.skipPermission` field and `ToolDefinition.createSkipPermission()` factory β€” marks a tool to skip the permission prompt (reference implementation: [`10c4d02`](https://github.com/github/copilot-sdk/commit/10c4d02)) +- `SystemMessageMode.CUSTOMIZE` β€” new enum value for fine-grained system prompt customization (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `SectionOverrideAction` enum β€” specifies the operation on a system prompt section (replace, remove, append, prepend, transform) (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `SectionOverride` class β€” describes how one section of the system prompt should be modified, with optional transform callback (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `SystemPromptSections` constants β€” well-known section identifier strings for use with CUSTOMIZE mode (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `SystemMessageConfig.setSections(Map)` β€” section-level overrides for CUSTOMIZE mode (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `systemMessage.transform` RPC handler β€” the SDK now registers a handler that invokes transform callbacks registered in the session config (reference implementation: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780)) +- `CopilotSession.setModel(String, String)` β€” new overload that accepts an optional reasoning effort level (reference implementation: [`ea90f07`](https://github.com/github/copilot-sdk/commit/ea90f07)) +- `CopilotSession.log(String, String, Boolean, String)` β€” new overload with an optional `url` parameter (minor addition) +- `BlobAttachment` class β€” inline base64-encoded binary attachment for messages (e.g., images) (reference implementation: [`698b259`](https://github.com/github/copilot-sdk/commit/698b259)) +- `MessageAttachment` sealed interface β€” type-safe base for all attachment types (`Attachment`, `BlobAttachment`), with Jackson polymorphic serialization support +- `TelemetryConfig` class β€” OpenTelemetry configuration for the CLI server; set on `CopilotClientOptions.setTelemetry()` (reference implementation: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0)) +- `CopilotClientOptions.setTelemetry(TelemetryConfig)` β€” enables OpenTelemetry instrumentation in the CLI server (reference implementation: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0)) + +### Changed + +- `Attachment` record now implements `MessageAttachment` sealed interface +- `BlobAttachment` class now implements `MessageAttachment` sealed interface and is `final` +- `MessageOptions.setAttachments(List)` β€” parameter type changed from `List` to `List` to support both `Attachment` and `BlobAttachment` in the same list with full compile-time safety +- `SendMessageRequest.setAttachments(List)` β€” matching change for the internal request type + +### Deprecated + +- `CopilotClientOptions.setAutoRestart(boolean)` β€” this option has no effect and will be removed in a future release + +## [0.1.32-java.0] - 2026-03-17 + +> **Reference implementation sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) +## [1.0.11] - 2026-03-12 + +> **Reference implementation sync:** [`github/copilot-sdk@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83) +### Added + +- `CopilotClientOptions.setOnListModels(Supplier>>)` β€” custom handler for `listModels()` used in BYOK mode to return models from a custom provider instead of querying the CLI (reference implementation: [`e478657`](https://github.com/github/copilot-sdk/commit/e478657)) +- `SessionConfig.setAgent(String)` β€” pre-selects a custom agent by name when creating a session (reference implementation: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) +- `ResumeSessionConfig.setAgent(String)` β€” pre-selects a custom agent by name when resuming a session (reference implementation: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a)) +- `SessionConfig.setOnEvent(Consumer)` β€” registers an event handler before the `session.create` RPC is issued, ensuring no early events are missed (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) +- `ResumeSessionConfig.setOnEvent(Consumer)` β€” registers an event handler before the `session.resume` RPC is issued (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) +- New broadcast session event types (protocol v3): `ExternalToolRequestedEvent` (`external_tool.requested`), `ExternalToolCompletedEvent` (`external_tool.completed`), `PermissionRequestedEvent` (`permission.requested`), `PermissionCompletedEvent` (`permission.completed`), `CommandQueuedEvent` (`command.queued`), `CommandCompletedEvent` (`command.completed`), `ExitPlanModeRequestedEvent` (`exit_plan_mode.requested`), `ExitPlanModeCompletedEvent` (`exit_plan_mode.completed`), `SystemNotificationEvent` (`system.notification`) (reference implementation: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812), [`396e8b3`](https://github.com/github/copilot-sdk/commit/396e8b3)) +- `CopilotSession.log(String)` and `CopilotSession.log(String, String, Boolean)` β€” log a message to the session timeline (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7)) + +### Changed + +- **Protocol version bumped to v3.** The SDK now supports CLI servers running v2 or v3 (backward-compatible range). Sessions are now registered in the client's session map *before* the `session.create`/`session.resume` RPC is issued, ensuring broadcast events emitted immediately on session start are never dropped (reference implementation: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7), [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) +- In protocol v3, tool calls and permission requests that have a registered handler are now handled automatically via `ExternalToolRequestedEvent` and `PermissionRequestedEvent` broadcast events; results are sent back via `session.tools.handlePendingToolCall` and `session.permissions.handlePendingPermissionRequest` RPC calls (reference implementation: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812)) + +## [1.0.10] - 2026-03-03 + +> **Reference implementation sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079) +### Added + +- `CopilotSession.setModel(String)` β€” changes the model for an existing session mid-conversation; the new model takes effect for the next message, and conversation history is preserved (reference implementation: [`bd98e3a`](https://github.com/github/copilot-sdk/commit/bd98e3a)) +- `ToolDefinition.createOverride(String, String, Map, ToolHandler)` β€” creates a tool definition that overrides a built-in CLI tool with the same name (reference implementation: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) +- `ToolDefinition` record now includes `overridesBuiltInTool` field; when `true`, signals to the CLI that the custom tool intentionally replaces a built-in (reference implementation: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80)) +- `CopilotSession.listAgents()` β€” lists custom agents available for selection (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.getCurrentAgent()` β€” gets the currently selected custom agent (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.selectAgent(String)` β€” selects a custom agent for the session (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.deselectAgent()` β€” deselects the current custom agent (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `CopilotSession.compact()` β€” triggers immediate session context compaction (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- `AgentInfo` β€” new JSON type representing a custom agent with `name`, `displayName`, and `description` (reference implementation: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb)) +- New event types: `SessionTaskCompleteEvent` (`session.task_complete`), `AssistantStreamingDeltaEvent` (`assistant.streaming_delta`), `SubagentDeselectedEvent` (`subagent.deselected`) (reference implementation: various commits) +- `AssistantTurnStartEvent` data now includes `interactionId` field +- `AssistantMessageEvent` data now includes `interactionId` field +- `ToolExecutionCompleteEvent` data now includes `model` and `interactionId` fields +- `SkillInvokedEvent` data now includes `pluginName` and `pluginVersion` fields +- `AssistantUsageEvent` data now includes `copilotUsage` field with `CopilotUsage` and `TokenDetails` nested types +- E2E tests for custom tool permission approval and denial flows (reference implementation: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) + +### Changed + +- **Breaking:** `createSession(SessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- **Breaking:** `resumeSession(String, ResumeSessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- **Breaking:** The no-arg `createSession()` and `resumeSession(String)` overloads were removed (reference implementation: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4)) +- `AssistantMessageDeltaEvent` data: `totalResponseSizeBytes` field moved to new `AssistantStreamingDeltaEvent` (reference implementation: various) + +### Fixed + +- Permission checks now also apply to SDK-registered custom tools, invoking the `onPermissionRequest` handler with `kind="custom-tool"` before executing tools (reference implementation: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3)) + +## [1.0.9] - 2026-02-16 + +> **Reference implementation sync:** [`github/copilot-sdk@e40d57c`](https://github.com/github/copilot-sdk/commit/e40d57c86e18b495722adbf42045288c03924342) +### Added + +#### Cookbook with Practical Recipes + +Added a comprehensive cookbook with 5 practical recipes demonstrating common SDK usage patterns. All examples are JBang-compatible and can be run directly without a full Maven project setup. + +**Recipes:** +- **Error Handling** - Connection failures, timeouts, cleanup patterns, tool errors +- **Multiple Sessions** - Parallel conversations, custom session IDs, lifecycle management +- **Managing Local Files** - AI-powered file organization with grouping strategies +- **PR Visualization** - Interactive CLI tool for analyzing PR age distribution via GitHub MCP Server +- **Persisting Sessions** - Save and resume conversations across restarts + +**Location:** `src/site/markdown/cookbook/` + +**Usage:** +```bash +jbang BasicErrorHandling.java +jbang MultipleSessions.java +jbang PRVisualization.java github/copilot-sdk +``` + +Each recipe includes JBang prerequisites, usage instructions, and best practices. + +#### Session Context and Filtering + +Added session context tracking and filtering capabilities to help manage multiple Copilot sessions across different repositories and working directories. + +**New Classes:** +- `SessionContext` - Represents working directory context (cwd, gitRoot, repository, branch) with fluent setters +- `SessionListFilter` - Filter sessions by context fields (extends SessionContext) +- `SessionContextChangedEvent` - Event fired when working directory context changes between turns + +**Updated APIs:** +- `SessionMetadata.getContext()` - Returns optional context information for persisted sessions +- `CopilotClient.listSessions(SessionListFilter)` - New overload to filter sessions by context criteria + +**Example:** +```java +// List sessions for a specific repository +var filter = new SessionListFilter() + .setRepository("owner/repo") + .setBranch("main"); +var sessions = client.listSessions(filter).get(); + +// Access context information +for (var session : sessions) { + var ctx = session.getContext(); + if (ctx != null) { + System.out.println("CWD: " + ctx.getCwd()); + System.out.println("Repo: " + ctx.getRepository()); + } +} + +// Listen for context changes +session.on(SessionContextChangedEvent.class, event -> { + SessionContext newContext = event.getData(); + System.out.println("Working directory changed to: " + newContext.getCwd()); +}); +``` + +**Requirements:** +- GitHub Copilot CLI 0.0.409 or later + +## [1.0.8] - 2026-02-08 + +> **Reference implementation sync:** [`github/copilot-sdk@05e3c46`](https://github.com/github/copilot-sdk/commit/05e3c46c8c23130c9c064dc43d00ec78f7a75eab) + +### Added + +#### ResumeSessionConfig Parity with SessionConfig +Added missing options to `ResumeSessionConfig` for parity with `SessionConfig` when resuming sessions. You can now change the model, system message, tool filters, and other settings when resuming: + +- `model` - Change the AI model when resuming +- `systemMessage` - Override or extend the system prompt +- `availableTools` - Restrict which tools are available +- `excludedTools` - Disable specific tools +- `configDir` - Override configuration directory +- `infiniteSessions` - Configure infinite session behavior + +**Example:** +```java +var config = new ResumeSessionConfig() + .setModel("claude-sonnet-4") + .setReasoningEffort("high") + .setSystemMessage(new SystemMessageConfig() + .setMode(SystemMessageMode.APPEND) + .setContent("Focus on security.")); + +var session = client.resumeSession(sessionId, config).get(); +``` + +#### EventErrorHandler for Custom Error Handling +Added `EventErrorHandler` interface for custom handling of exceptions thrown by event handlers. Set via `session.setEventErrorHandler()` to receive the event and exception when a handler fails. + +```java +session.setEventErrorHandler((event, exception) -> { + logger.error("Handler failed for event: " + event.getType(), exception); +}); +``` + +#### EventErrorPolicy for Dispatch Control +Added `EventErrorPolicy` enum to control whether event dispatch continues or stops when a handler throws an exception. Errors are always logged at `WARNING` level. The default policy is `PROPAGATE_AND_LOG_ERRORS` which stops dispatch on the first error. Set `SUPPRESS_AND_LOG_ERRORS` to continue dispatching despite errors: + +```java +session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); +``` + +The `EventErrorHandler` is always invoked regardless of the policy. + +#### Type-Safe Event Handlers +Promoted type-safe `on(Class, Consumer)` event handlers as the primary API. Handlers now receive strongly-typed events instead of raw `AbstractSessionEvent`. + +```java +session.on(AssistantMessageEvent.class, msg -> { + System.out.println(msg.getData().getContent()); +}); +``` + +#### SpotBugs Static Analysis +Integrated SpotBugs for static code analysis with exclusion filters for `events` and `json` packages. + +### Changed + +- **Copilot CLI**: Minimum version updated to **0.0.405** +- **CopilotClient**: Made `final` to prevent Finalizer attacks (security hardening) +- **JBang Example**: Refactored `jbang-example.java` with streamlined session creation and usage metrics display +- **Code Style**: Use `var` for local variable type inference throughout the codebase + +### Fixed + +- **SpotBugs OS_OPEN_STREAM**: Wrap `BufferedReader` in try-with-resources to prevent resource leaks +- **SpotBugs REC_CATCH_EXCEPTION**: Narrow exception catch in `JsonRpcClient.handleMessage()` +- **SpotBugs DM_DEFAULT_ENCODING**: Add explicit UTF-8 charset to `InputStreamReader` +- **SpotBugs EI_EXPOSE_REP**: Add defensive copies to collection getters in events and JSON packages + +## [1.0.7] - 2026-02-05 + +### Added + +#### Session Lifecycle Hooks +Extended the hooks system with three new hook types for session lifecycle control: +- **`onSessionStart`** - Called when a session starts (new or resumed) +- **`onSessionEnd`** - Called when a session ends +- **`onUserPromptSubmitted`** - Called when the user submits a prompt + +New types: +- `SessionStartHandler`, `SessionStartHookInput`, `SessionStartHookOutput` +- `SessionEndHandler`, `SessionEndHookInput`, `SessionEndHookOutput` +- `UserPromptSubmittedHandler`, `UserPromptSubmittedHookInput`, `UserPromptSubmittedHookOutput` + +#### Session Lifecycle Events (Client-Level) +Added client-level lifecycle event subscriptions: +- `client.onLifecycle(handler)` - Subscribe to all session lifecycle events +- `client.onLifecycle(eventType, handler)` - Subscribe to specific event types +- `SessionLifecycleEventTypes.CREATED`, `DELETED`, `UPDATED`, `FOREGROUND`, `BACKGROUND` + +New types: `SessionLifecycleEvent`, `SessionLifecycleEventMetadata`, `SessionLifecycleHandler` + +#### Foreground Session Control (TUI+Server Mode) +For servers running with `--ui-server`: +- `client.getForegroundSessionId()` - Get the session displayed in TUI +- `client.setForegroundSessionId(sessionId)` - Switch TUI display to a session + +New types: `GetForegroundSessionResponse`, `SetForegroundSessionResponse` + +#### New Event Types +- **`SessionShutdownEvent`** - Emitted when session is shutting down, includes reason and exit code +- **`SkillInvokedEvent`** - Emitted when a skill is invoked, includes skill name and context + +#### Extended Event Data +- `AssistantMessageEvent.Data` - Added `id`, `isLastReply`, `thinkingContent` fields +- `AssistantUsageEvent.Data` - Added `outputReasoningTokens` field +- `SessionCompactionCompleteEvent.Data` - Added `success`, `messagesRemoved`, `tokensRemoved` fields +- `SessionErrorEvent.Data` - Extended with additional error context + +#### Documentation +- New **[hooks.md](src/site/markdown/hooks.md)** - Comprehensive guide covering all 5 session hooks with examples for security gates, logging, result enrichment, and lifecycle management +- Expanded **[documentation.md](src/site/markdown/documentation.md)** with all 33 event types, `getMessages()`, `abort()`, and custom timeout examples +- Enhanced **[advanced.md](src/site/markdown/advanced.md)** with session hooks, lifecycle events, and foreground session control +- Added **[.github/copilot-instructions.md](.github/copilot-instructions.md)** for AI assistants + +#### Testing +- `SessionEventParserTest` - 850+ lines of unit tests for JSON event deserialization +- `SessionEventsE2ETest` - End-to-end tests for session event lifecycle +- `ErrorHandlingTest` - Tests for error handling scenarios +- Enhanced `E2ETestContext` with snapshot validation and expected prompt logging +- Added logging configuration (`logging.properties`) + +#### Build & CI +- JaCoCo 0.8.14 for test coverage reporting +- Coverage reports generated at `target/site/jacoco-coverage/` +- New test report action at `.github/actions/test-report/` +- JaCoCo coverage summary in workflow summary +- Coverage report artifact upload + +### Changed + +- **Copilot CLI**: Minimum version updated from 0.0.400 to **0.0.404** +- Refactored `ProcessInfo` and `Connection` to use records +- Extended `SessionHooks` to support 5 hook types (was 2) +- Renamed test methods to match snapshot naming conventions with Javadoc + +### Fixed + +- Improved timeout exception handling with detailed logging +- Test infrastructure improvements for proxy resilience + +## [1.0.6] - 2026-02-02 + +### Added + +- Auth options for BYOK configuration (`authType`, `apiKey`, `organizationId`, `endpoint`) +- Reasoning effort configuration (`reasoningEffort` in session config) +- User input handler for freeform user prompts (`UserInputHandler`, `UserInputRequest`, `UserInputResponse`) +- Pre-tool use and post-tool use hooks (`PreToolUseHandler`, `PostToolUseHandler`) +- VSCode launch and debug configurations +- Logging configuration for test debugging + +### Changed + +- Enhanced permission request handling with graceful error recovery +- Updated test harness integration to clone from reference implementation SDK +- Improved logging for session events and user input requests + +### Fixed + +- Non-null answer enforcement in user input responses for CLI compatibility +- Permission handler error handling improvements + +## [1.0.5] - 2026-01-29 + +### Added + +- Skills configuration: `skillDirectories` and `disabledSkills` in `SessionConfig` +- Skill events handling (`SkillInvokedEvent`) +- Javadoc verification step in build workflow +- Deploy-site job for automatic documentation deployment after releases + +### Changed + +- Merged reference implementation SDK changes (commit 87ff5510) +- Added agentic-merge-reference-impl Claude skill for tracking reference implementation changes + +### Fixed + +- Resume session handling to keep first client alive +- Build workflow updated to use `test-compile` instead of `compile` +- NPM dependency installation in CI workflow +- Enhanced error handling in permission request processing +- Checkstyle and Maven Resources Plugin version updates +- Test harness CLI installation to match reference implementation version + +## [1.0.4] - 2026-01-27 + +### Added + +- Advanced usage documentation with comprehensive examples +- Getting started guide with Maven and JBang instructions +- Package-info.java files for `com.github.copilot.sdk`, `events`, and `json` packages +- `@since` annotations on all public classes +- Versioned documentation with version selector on GitHub Pages +- Maven resources plugin for site markdown filtering + +### Changed + +- Refactored tool argument handling for improved type safety +- Optimized event listener registration in examples +- Enhanced site navigation with documentation links +- Merged reference implementation SDK changes from commit f902b76 + +### Fixed + +- BufferedReader replaced with BufferedInputStream for accurate JSON-RPC byte reading +- Timeout thread now uses daemon thread to prevent JVM exit blocking +- XML root element corrected from `` to `` in site.xml +- Badge titles in README for consistency + +## [1.0.3] - 2026-01-26 + +### Added + +- MCP Servers documentation and integration examples +- Infinite sessions documentation section +- Versioned documentation template with version selector +- Guidelines for porting reference implementation SDK changes to Java +- Configuration for automatically generated release notes + +### Changed + +- Renamed and retitled GitHub Actions workflows for clarity +- Improved gh-pages initialization and remote setup + +### Fixed + +- Documentation navigation to include MCP Servers section +- GitHub Pages deployment workflow to use correct branch +- Enhanced version handling in documentation build steps +- Rollback mechanism added for release failures + +## [1.0.2] - 2026-01-25 + +### Added + +- Infinite sessions support with `InfiniteSessionConfig` and workspace persistence +- GitHub Actions workflow for GitHub Pages deployment +- Daily schedule trigger for SDK E2E tests +- Checkstyle configuration and Maven integration + +### Changed + +- Updated GitHub Actions to latest action versions +- Enhanced Maven site deployment with documentation versioning +- Simplified GitHub release title naming convention + +### Fixed + +- Documentation links in site.xml and README for consistency +- Maven build step to include `clean` for fresh builds +- Image handling in README and site generation + +## [1.0.1] - 2026-01-22 + +### Added + +- Metadata APIs implementation +- Tool execution progress event (`ToolExecutionProgressEvent`) +- SDK protocol version 2 support +- Image in README for visual representation +- Detailed sections in README with usage examples +- Badges for build status, Maven Central, Java version, and license + +### Changed + +- Enhanced version handling in Maven release workflow +- Updated SCM connection URLs to use HTTPS + +### Fixed + +- GitHub release command version formatting and title +- Documentation commit messages to include version information +- JBang dependency declaration with correct group ID + +## [1.0.0] - 2026-01-21 + +### Added + +- Initial release of the GitHub Copilot SDK for Java +- Core classes: `CopilotClient`, `CopilotSession`, `JsonRpcClient` +- Session configuration with `SessionConfig` +- Custom tools with `ToolDefinition` and `ToolHandler` +- Event system with 30+ event types extending `AbstractSessionEvent` +- Permission handling with `PermissionHandler` +- BYOK (Bring Your Own Key) support with `ProviderConfig` +- MCP server integration via `McpServerConfig` +- System message customization with `SystemMessageConfig` +- File attachments support +- Streaming responses with delta events +- JBang example for quick testing +- GitHub Actions workflows for testing and Maven Central publishing +- Pre-commit hook for Spotless code formatting +- Comprehensive API documentation + +[Unreleased]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.4...HEAD +[1.0.0-beta-java.4]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.3...v1.0.0-beta-java.4 +[1.0.0-beta-java.3]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.2...v1.0.0-beta-java.3 +[1.0.0-beta-java.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.0-beta-java.1...v1.0.0-beta-java.2 +[1.0.0-beta-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.3.0-java.2...v1.0.0-beta-java.1 +[0.3.0-java.2]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java.2 +[0.3.0-java-preview.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.1 +[0.3.0-java-preview.0]: https://github.com/github/copilot-sdk-java/compare/v0.2.2-java.1...v0.3.0-java-preview.0 +[0.2.2-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.1...v0.2.2-java.1 +[0.2.1-java.1]: https://github.com/github/copilot-sdk-java/compare/v0.2.1-java.0...v0.2.1-java.1 +[0.2.1-java.0]: https://github.com/github/copilot-sdk-java/compare/v0.1.32-java.0...v0.2.1-java.0 +[0.1.32-java.0]: https://github.com/github/copilot-sdk-java/compare/v1.0.11...v0.1.32-java.0 +[1.0.11]: https://github.com/github/copilot-sdk-java/compare/v1.0.10...v1.0.11 +[1.0.10]: https://github.com/github/copilot-sdk-java/compare/v1.0.9...v1.0.10 +[1.0.9]: https://github.com/github/copilot-sdk-java/compare/v1.0.8...v1.0.9 +[1.0.8]: https://github.com/github/copilot-sdk-java/compare/v1.0.7...v1.0.8 +[1.0.7]: https://github.com/github/copilot-sdk-java/compare/v1.0.6...v1.0.7 +[1.0.6]: https://github.com/github/copilot-sdk-java/compare/v1.0.5...v1.0.6 +[1.0.5]: https://github.com/github/copilot-sdk-java/compare/v1.0.4...v1.0.5 +[1.0.4]: https://github.com/github/copilot-sdk-java/compare/v1.0.3...v1.0.4 +[1.0.3]: https://github.com/github/copilot-sdk-java/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/github/copilot-sdk-java/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/github/copilot-sdk-java/compare/1.0.0...v1.0.1 +[1.0.0]: https://github.com/github/copilot-sdk-java/releases/tag/1.0.0 diff --git a/java/README.md b/java/README.md index f197cb549..f2b342dfc 100644 --- a/java/README.md +++ b/java/README.md @@ -1,82 +1,215 @@ # GitHub Copilot SDK for Java -Java SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. - [![Build](https://github.com/github/copilot-sdk-java/actions/workflows/build-test.yml/badge.svg)](https://github.com/github/copilot-sdk-java/actions/workflows/build-test.yml) -[![Maven Central](https://img.shields.io/maven-central/v/com.github/copilot-sdk-java)](https://central.sonatype.com/artifact/com.github/copilot-sdk-java) -[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) +[![Site](https://github.com/github/copilot-sdk-java/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/github/copilot-sdk-java/actions/workflows/deploy-site.yml) +[![Coverage](.github/badges/jacoco.svg)](https://github.github.io/copilot-sdk-java/snapshot/jacoco/index.html) [![Documentation](https://img.shields.io/badge/docs-online-brightgreen)](https://github.github.io/copilot-sdk-java/) -[![Javadoc](https://javadoc.io/badge2/com.github/copilot-sdk-java/javadoc.svg)](https://javadoc.io/doc/com.github/copilot-sdk-java/latest/index.html) +[![Java 17+](https://img.shields.io/badge/Java-17%2B-blue?logo=openjdk&logoColor=white)](https://openjdk.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## Quick Start +#### Latest release +[![GitHub Release Date](https://img.shields.io/github/release-date/github/copilot-sdk-java)](https://github.com/github/copilot-sdk-java/releases) +[![GitHub Release](https://img.shields.io/github/v/release/github/copilot-sdk-java)](https://github.com/github/copilot-sdk-java/releases) +[![Maven Central](https://img.shields.io/maven-central/v/com.github/copilot-sdk-java)](https://central.sonatype.com/artifact/com.github/copilot-sdk-java) +[![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://github.github.io/copilot-sdk-java/latest/) +[![Javadoc](https://javadoc.io/badge2/com.github/copilot-sdk-java/javadoc.svg?q=1)](https://javadoc.io/doc/com.github/copilot-sdk-java/latest/index.html) + +## Background + +> ℹ️ **Public Preview:** This SDK tracks the [GitHub Copilot SDKs](https://github.com/github/copilot-sdk) for [.NET](https://github.com/github/copilot-sdk/tree/main/dotnet) and [Node.js](https://github.com/github/copilot-sdk/tree/main/nodejs). While in public preview, minor breaking changes may still occur between releases. + +Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. + +## Installation + +### Requirements + +- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start). +- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`) + +### Maven + +```xml + + com.github + copilot-sdk-java + 1.0.0-beta-java.4 + +``` -**πŸ“¦ The Java SDK is maintained in a separate repository: [`github/copilot-sdk-java`](https://github.com/github/copilot-sdk-java)** +#### Snapshot Builds -> **Note:** This SDK is in public preview and may change in breaking ways. +Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the repository and update the dependency version in your `pom.xml`: + +```xml + + + central-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + true + + + + + com.github + copilot-sdk-java + 1.0.0-beta-java.5-SNAPSHOT + +``` + +### Gradle + +```groovy +implementation 'com.github:copilot-sdk-java:1.0.0-beta-java.4' +``` + +## Quick Start ```java import com.github.copilot.sdk.CopilotClient; -import com.github.copilot.sdk.events.AssistantMessageEvent; -import com.github.copilot.sdk.events.SessionIdleEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionUsageInfoEvent; +import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; -public class QuickStart { +import java.util.concurrent.Executors; + +public class CopilotSDK { public static void main(String[] args) throws Exception { + var lastMessage = new String[]{null}; + // Create and start client - try (var client = new CopilotClient()) { + try (var client = new CopilotClient()) { // JDK 25+: comment out this line + // JDK 25+: uncomment the following 3 lines for virtual thread support + // var options = new CopilotClientOptions() + // .setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + // try (var client = new CopilotClient(options)) { client.start().get(); - // Create a session (onPermissionRequest is required) + // Create a session var session = client.createSession( - new SessionConfig() - .setModel("gpt-5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - ).get(); - - var done = new java.util.concurrent.CompletableFuture(); - - // Handle events - session.on(AssistantMessageEvent.class, msg -> - System.out.println(msg.getData().content())); - session.on(SessionIdleEvent.class, idle -> - done.complete(null)); - - // Send a message and wait for completion - session.send(new MessageOptions().setPrompt("What is 2+2?")); - done.get(); + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); + + + // Handle assistant message events + session.on(AssistantMessageEvent.class, msg -> { + lastMessage[0] = msg.getData().content(); + System.out.println(lastMessage[0]); + }); + + // Handle session usage info events + session.on(SessionUsageInfoEvent.class, usage -> { + var data = usage.getData(); + System.out.println("\n--- Usage Metrics ---"); + System.out.println("Current tokens: " + data.currentTokens().intValue()); + System.out.println("Token limit: " + data.tokenLimit().intValue()); + System.out.println("Messages count: " + data.messagesLength().intValue()); + }); + + // Send a message + var completable = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")); + // and wait for completion + completable.get(); } + + boolean success = lastMessage[0] != null && lastMessage[0].contains("4"); + System.exit(success ? 0 : -1); } } ``` ## Try it with JBang -Run the SDK without setting up a full project using [JBang](https://www.jbang.dev/): +You can run the SDK without setting up a full Java project, by using [JBang](https://www.jbang.dev/). + +See the full source of [`jbang-example.java`](jbang-example.java) for a complete example with more features like session idle handling and usage info events. + +Or run it directly from the repository: ```bash -jbang https://github.com/github/copilot-sdk-java/blob/main/jbang-example.java +jbang https://github.com/github/copilot-sdk-java/blob/latest/jbang-example.java ``` -## Documentation & Resources +## Documentation + +πŸ“š **[Full Documentation](https://github.github.io/copilot-sdk-java/)** β€” Complete API reference, advanced usage examples, and guides. + +### Quick Links -| Resource | Link | -| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| **Full Documentation** | [github.github.io/copilot-sdk-java](https://github.github.io/copilot-sdk-java/) | -| **Getting Started Guide** | [Documentation](https://github.github.io/copilot-sdk-java/latest/documentation.html) | -| **API Reference (Javadoc)** | [javadoc.io](https://javadoc.io/doc/com.github/copilot-sdk-java/latest/index.html) | -| **MCP Servers Integration** | [MCP Guide](https://github.github.io/copilot-sdk-java/latest/mcp.html) | -| **Cookbook** | [Recipes](https://github.com/github/copilot-sdk-java/tree/main/src/site/markdown/cookbook) | -| **Source Code** | [github/copilot-sdk-java](https://github.com/github/copilot-sdk-java) | -| **Issues & Feature Requests** | [GitHub Issues](https://github.com/github/copilot-sdk-java/issues) | -| **Releases** | [GitHub Releases](https://github.com/github/copilot-sdk-java/releases) | -| **Copilot Instructions** | [copilot-sdk-java.instructions.md](https://github.com/github/copilot-sdk-java/blob/main/instructions/copilot-sdk-java.instructions.md) | +- [Getting Started](https://github.github.io/copilot-sdk-java/latest/documentation.html) +- [Javadoc API Reference](https://github.github.io/copilot-sdk-java/latest/apidocs/) +- [MCP Servers Integration](https://github.github.io/copilot-sdk-java/latest/mcp.html) +- [Cookbook](src/site/markdown/cookbook/) β€” Practical recipes for common use cases + +## Projects Using This SDK + +| Project | Description | +|---------|-------------| +| [JMeter Copilot Plugin](https://github.com/brunoborges/jmeter-copilot-plugin) | JMeter plugin for AI-assisted load testing | + +> Want to add your project? Open a PR! + +## CI/CD Workflows + +This project uses several GitHub Actions workflows for building, testing, releasing, and syncing with the reference implementation SDK. + +See [WORKFLOWS.md](docs/WORKFLOWS.md) for a full overview and details on each workflow. ## Contributing -Contributions are welcome! Please see the [Contributing Guide](https://github.com/github/copilot-sdk-java/blob/main/CONTRIBUTING.md) in the GitHub Copilot SDK for Java repository. +Contributions are welcome! Please see the [Contributing Guide](CONTRIBUTING.md) for details. + +### Agentic Reference Implementation Merge and Sync + +This SDK tracks the official [Copilot SDK](https://github.com/github/copilot-sdk) (.NET reference implementation) and ports changes to Java. The reference implementation merge process is automated with AI assistance: + +**Automated sync** β€” A [scheduled GitHub Actions workflow](.github/workflows/reference-impl-sync.yml) runs on the schedule specified in that file. It checks for new reference implementation commits since the last merge (tracked in [`.lastmerge`](.lastmerge)), and if changes are found, creates an issue labeled `reference-impl-sync` and assigns it to the GitHub Copilot coding agent. Any previously open `reference-impl-sync` issues are automatically closed. The sync also updates the `@github/copilot` version in both `pom.xml` and `scripts/codegen/package.json` to keep schemas and test CLI in lockstep. + +**Reusable prompt** β€” The merge workflow is defined in [`agentic-merge-reference-impl.prompt.md`](.github/prompts/agentic-merge-reference-impl.prompt.md). It can be triggered manually from: +- **VS Code Copilot Chat** β€” type `/agentic-merge-reference-impl` +- **GitHub Copilot CLI** β€” use `copilot` CLI with the same skill reference + +### Development Setup + +```bash +# Clone the repository +git clone https://github.com/github/copilot-sdk-java.git +cd copilot-sdk-java + +# Enable git hooks for code formatting +git config core.hooksPath .githooks + +# Build and test +mvn clean verify +``` + +The tests require the official [copilot-sdk](https://github.com/github/copilot-sdk) test harness, which is automatically cloned during build. + +## Support + +See [SUPPORT.md](SUPPORT.md) for how to file issues and get help. + +## Code of Conduct + +This project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details. + +## Security + +See [SECURITY.md](SECURITY.md) for reporting security vulnerabilities. ## License -MIT β€” see [LICENSE](https://github.com/github/copilot-sdk-java/blob/main/LICENSE) for details. +MIT β€” see [LICENSE](LICENSE) for details. + +## Acknowledgement + +- Initially developed with Copilot and [Bruno Borges](https://www.linkedin.com/in/brunocborges/). + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=github/copilot-sdk-java&type=Date)](https://www.star-history.com/#github/copilot-sdk-java&Date) + +⭐ Drop a star if you find this useful! + diff --git a/java/config/checkstyle/checkstyle.xml b/java/config/checkstyle/checkstyle.xml new file mode 100644 index 000000000..fcde1f6c9 --- /dev/null +++ b/java/config/checkstyle/checkstyle.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/config/spotbugs/spotbugs-exclude.xml b/java/config/spotbugs/spotbugs-exclude.xml new file mode 100644 index 000000000..1c7d415f6 --- /dev/null +++ b/java/config/spotbugs/spotbugs-exclude.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/java/docs/adr/adr-001-semver-pre-general-availability.md b/java/docs/adr/adr-001-semver-pre-general-availability.md new file mode 100644 index 000000000..25008b0f5 --- /dev/null +++ b/java/docs/adr/adr-001-semver-pre-general-availability.md @@ -0,0 +1,28 @@ +# SemVer requirements pre general-availability of Reference Implementation + +## Context and Problem Statement + +Steve Sanderson agreed that `copilot-sdk-java` will track reference implementation version numbers directly, with one exception: when the Java SDK needs to ship a breaking change before 1.0, the reference implementation will bump its minor version to accommodate, giving our release a clean version number that signals the change to users. + +The reference implementation makes no backward compatibility guarantees pre-1.0 β€” and neither will we. That said, we're choosing to hold ourselves to a higher standard as a matter of good practice: we'll use minor version bumps as a signal to users when we do ship something breaking. + +The 2026-02 state of `copilot-sdk-java` is that it takes Java 17+ as its baseline. This decision precludes the use of Java 21 features such as virtual threads. Our pre-analysis showed the **possibility** of a significant performance benefit when using Virtual Threads with Java 21. + +We took an architectural decision to enable us to pursue investigating this possibility immediately. + +## Considered Options + +* Track SemVer of reference implementation, with one exception. +* Completely avoid the need for this by doing no breaking changes pre-1.0. +* Abandon the policy of tracking the versions of the reference implementation directly, just do our own thing. + +## Decision Outcome + +Chosen option: "Track SemVer of reference implementation, with one exception.", because this enables us to pursue Virtual Threads without delaying the first public release of `copilot-sdk-java`. Also, we're supposed to be aggressively modernizing our customers. + +To some extent, I would use qualifiers to mark a release as having some feature that is awaiting a reference implementation full release before it goes full ga, i.e you put out 0.1.46-virtualthreads.3 until reference implementation is ready to move to 0.2.0 then you release your virtual threads change and go 0.2.0. So I would make your agreement that your version numbers would match with the exception of qualifiers that you might add in exceptional circumstances. + +## Related work items + +- https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2745172 + diff --git a/java/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md b/java/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md new file mode 100644 index 000000000..248a024c2 --- /dev/null +++ b/java/docs/adr/adr-002-maven-version-and-reference-implementation-tracking.md @@ -0,0 +1,57 @@ +# Maven Version and tracking of versions released from the reference implementation + +## Context and Problem Statement + +Releases of this implementation track releases of the reference implementation. For each release of the reference implementation, there may follow a corresponding relase of this implementation with the same number as the reference implementation, allowing for additional metadata to version multiple releases of this implementation that track the same release of the reference implementation. + +## Considered Options + +- Simple number qualifier (0.1.32-0, 0.1.32-1, ...) fails on a subtle but important point: 0.1.32-0 is treated identically to 0.1.32 by Maven (trailing zeros are normalized away), and bare numeric qualifiers are pre-release semantics. Your "first release" would sort before the reference implementation bare version. + +- Java and number in the qualifier (0.1.32-java.N) + + - java is an unknown qualifier that sorts correctly and accurately describes what it is β€” the Java-ecosystem release of this version. + +- sp and number in the qualifier (0.1.32-sp.N) + + - sp is a known Maven qualifier with a specific semantic ("service pack"), which is slightly misleading since your -java.0 isn't a service pack β€” it's the primary release. + +My recommendation, with the test results backing it up: 0.1.32-java.0, 0.1.32-java.1, 0.1.32-java.2. It passes every constraint you stated, is accepted by Sonatype (arbitrary string, not ending in -SNAPSHOT), and is self-documenting. The only thing left to validate empirically is actually uploading one to the portal β€” but nothing in Sonatype's published requirements would reject it. See below for empirical validation. + +## Decision Outcome + +Choose option "Java and number in qualifier". + +## Supporting information + +Output from experiment to publish an artifact with GAV `io.github.edburns:helloworld:0.1.31-java.0`: + +Everything looks healthy. Here's the status: + + - βœ… GPG signing succeeded (no passphrase errors this time) + - βœ… Upload to Central succeeded β€” deployment ID + e8192af5-769d-4cd3-a6d9-5fc38b4dbf10 was accepted + - βœ… Authentication is working β€” all status checks return successfully + - ⏳ State: publishing β€” Central accepted the bundle and is propagating it. + The plugin is polling repeatedly (27+ checks so far), waiting for state to + change to published. + + This is normal. Maven Central publishing can take 5–30 minutes to transition + from publishing β†’ published. The waitUntil=published setting in the POM + means Maven will keep polling until it completes. Just let it run. + +> Given that the purpose of this exercise was to test your assertion that a GAV with a Version such as this one would be valid to publish to Maven central , can we already conclude this is a true assertion? + +- Yes. The version 0.1.31-java.0 has already passed Maven Central's + validation. The publishing state means Central accepted the bundle β€” + including its GAV β€” and is now replicating it to mirrors. If the version + were invalid, it would have been rejected during validation before entering + the publishing state. + + So yes, the assertion holds: Maven Central accepts versions with dots inside + the qualifier segment (e.g., 0.1.31-java.0). + +## Related work items + +- https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2766089 + diff --git a/java/jbang-example.java b/java/jbang-example.java new file mode 100644 index 000000000..1c41679cd --- /dev/null +++ b/java/jbang-example.java @@ -0,0 +1,42 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionUsageInfoEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +import static java.lang.System.out; + +class CopilotSDK { + public static void main(String[] args) throws Exception { + // Create and start client + try (var client = new CopilotClient()) { + client.start().get(); + + // Create a session + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); + + // Handle assistant message events + session.on(AssistantMessageEvent.class, msg -> { + out.println(msg.getData().content()); + }); + + // Handle session usage info events + session.on(SessionUsageInfoEvent.class, usage -> { + var data = usage.getData(); + out.println("\n--- Usage Metrics ---"); + out.println("Current tokens: " + data.currentTokens().intValue()); + out.println("Token limit: " + data.tokenLimit().intValue()); + out.println("Messages count: " + data.messagesLength().intValue()); + }); + + // Send a message + var completable = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")); + // and wait for completion + completable.get(); + } + } +} diff --git a/java/mvnw b/java/mvnw new file mode 100644 index 000000000..bd8896bf2 --- /dev/null +++ b/java/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/java/mvnw.cmd b/java/mvnw.cmd new file mode 100644 index 000000000..92450f932 --- /dev/null +++ b/java/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/java/pom.xml b/java/pom.xml new file mode 100644 index 000000000..d251fbce6 --- /dev/null +++ b/java/pom.xml @@ -0,0 +1,898 @@ + + + + 4.0.0 + + com.github + copilot-sdk-java + 1.0.0-beta-java.5-SNAPSHOT + jar + + GitHub Copilot SDK :: Java + SDK for programmatic control of GitHub Copilot CLI + https://github.com/github/copilot-sdk-java + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + GitHub Copilot SDK + GitHub Copilot SDK + https://github.com/github + + + + + scm:git:https://github.com/github/copilot-sdk-java.git + scm:git:https://github.com/github/copilot-sdk-java.git + https://github.com/github/copilot-sdk-java + HEAD + + + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + + + 17 + UTF-8 + + ${project.build.directory}/copilot-sdk + ${copilot.sdk.clone.dir}/test + + ${copilot.sdk.clone.dir}/nodejs/node_modules/@github/copilot/index.js + + false + + ${skip.test.harness} + + + + ^1.0.49-1 + + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.21.3 + + + com.fasterxml.jackson.core + jackson-annotations + 2.21 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.21.3 + + + + + com.github.spotbugs + spotbugs-annotations + 4.9.8 + provided + + + + + org.junit.jupiter + junit-jupiter + 5.14.4 + test + + + org.mockito + mockito-core + 5.23.0 + test + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + public + true + none + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.9.8.3 + + config/spotbugs/spotbugs-exclude.xml + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.2.0 + + + clone-or-update-copilot-sdk + generate-test-resources + + run + + + ${skip.test.harness} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.ant + ant + 1.10.17 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + install-harness-dependencies + generate-test-resources + + exec + + + ${skip.test.harness} + npm + ${copilot.sdk.clone.dir}/test/harness + + install + + + + + + install-nodejs-cli-dependencies + generate-test-resources + + exec + + + ${skip.cli.install} + npm + ${copilot.sdk.clone.dir}/nodejs + + ci + --ignore-scripts + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.5 + + alphabetical + + ${testExecutionAgentArgs} ${surefire.jvm.args} + + 2 + + ${copilot.tests.dir} + ${copilot.sdk.clone.dir} + + + + ${copilot.cli.path} + + + + + + isolated-resume-tests + test + + test + + + isolated-resume + + ${project.build.directory}/surefire-reports-isolated + + + + + default-test + + isolated-resume + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.1 + + + add-generated-source + generate-sources + + add-source + + + + ${project.basedir}/src/generated/java + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.46.1 + + + + src/generated/java/**/*.java + + + 4.33 + + + + + + true + 4 + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + + wire-up-coverage-instrumentation + + prepare-agent + + + + ${project.build.directory}/jacoco-test-results/sdk-tests.exec + + testExecutionAgentArgs + + + com/github/copilot/sdk/** + + + com/github/copilot/sdk/E2ETestContext* + com/github/copilot/sdk/CapiProxy* + + + + + + build-coverage-report-from-tests + + report + + verify + + ${project.build.directory}/jacoco-test-results/sdk-tests.exec + ${project.reporting.outputDirectory}/jacoco-coverage + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.6.0 + + config/checkstyle/checkstyle.xml + true + true + false + + + + validate + validate + + check + + + + + + com.puppycrawl.tools + checkstyle + 10.26.1 + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.5.0 + + + filter-site-markdown + pre-site + + copy-resources + + + ${project.build.directory}/filtered-site/markdown + + + src/site/markdown + true + + + + + + copy-site-descriptor + pre-site + + copy-resources + + + ${project.build.directory}/filtered-site + + + src/site + + site.xml + + false + + + + + + copy-site-resources + pre-site + + copy-resources + + + ${project.build.directory}/filtered-site/resources + + + src/site/resources + false + + + + + + overlay-jacoco-css + site + + copy-resources + + + ${project.reporting.outputDirectory}/jacoco/jacoco-resources + + + src/site/jacoco-resources + false + + + true + + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.21.0 + + UTF-8 + UTF-8 + ${project.build.directory}/filtered-site + ${project.build.directory}/filtered-site-generated + + + + org.apache.maven.doxia + doxia-module-markdown + 2.1.0 + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 + true + + central + true + + + + + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.9.0 + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + javadoc + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + ${project.build.directory}/jacoco-test-results/sdk-tests.exec + + + + + report + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.5.5 + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.9.8.3 + + config/spotbugs/spotbugs-exclude.xml + + + + + org.codehaus.mojo + taglist-maven-plugin + 3.2.2 + + + + + Todo Work + + + TODO + ignoreCase + + + FIXME + ignoreCase + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.10.0 + + + + analyze-report + + + + + + + + + + + jdk21+ + + [21,) + + + -XX:+EnableDynamicAgentLoading + + + + + skip-test-harness + + true + + + + + skip-cli-install-when-tests-skipped + + + skipTests + true + + + + true + + + + + skip-cli-install-when-maven-test-skip + + + maven.test.skip + true + + + + true + + + + + debug + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.basedir}/src/test/resources/logging-debug.properties + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.4.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + + + + + update-schemas-from-npm-artifact + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + update-copilot-schema-version + generate-sources + + exec + + + npm + ${project.basedir}/scripts/codegen + + install + @github/copilot@${copilot.schema.version} + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.2 + + + require-schema-version + validate + + enforce + + + + + copilot.schema.version + You must specify -Dcopilot.schema.version=VERSION (e.g. 1.0.25) + + + + + + + + + + + + codegen + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + + codegen-npm-install + generate-sources + + exec + + + npm + ${project.basedir}/scripts/codegen + + ci + + + + + codegen-generate + generate-sources + + exec + + + npm + ${project.basedir}/scripts/codegen + + run + generate + + + + + + + + + + diff --git a/java/scripts/codegen/.gitignore b/java/scripts/codegen/.gitignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/java/scripts/codegen/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts new file mode 100644 index 000000000..0a96ab9f1 --- /dev/null +++ b/java/scripts/codegen/java.ts @@ -0,0 +1,1552 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Java code generator for session-events and RPC types. + * Generates Java source files under src/generated/java/ from JSON Schema files. + */ + +import fs from "fs/promises"; +import path from "path"; +import { fileURLToPath } from "url"; +import type { JSONSchema7 } from "json-schema"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** Root of the copilot-sdk-java repo */ +const REPO_ROOT = path.resolve(__dirname, "../.."); + +/** Event types to exclude from generation (internal/legacy types) */ +const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); + +const AUTO_GENERATED_HEADER = `// AUTO-GENERATED FILE - DO NOT EDIT`; +const GENERATED_FROM_SESSION_EVENTS = `// Generated from: session-events.schema.json`; +const GENERATED_FROM_API = `// Generated from: api.schema.json`; +const GENERATED_ANNOTATION = `@javax.annotation.processing.Generated("copilot-sdk-codegen")`; +const COPYRIGHT = `/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n *--------------------------------------------------------------------------------------------*/`; + +// ── Naming utilities ───────────────────────────────────────────────────────── + +function toPascalCase(name: string): string { + return name.split(/[-_.]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); +} + +function toJavaClassName(typeName: string): string { + return typeName.split(/[._]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); +} + +function toCamelCase(name: string): string { + const pascal = toPascalCase(name); + return pascal.charAt(0).toLowerCase() + pascal.slice(1); +} + +function toEnumConstant(value: string): string { + return value.toUpperCase().replace(/[-. /:]/g, "_").replace(/^_+/, "").replace(/_+/g, "_"); +} + +// ── Schema path resolution ─────────────────────────────────────────────────── + +async function getSessionEventsSchemaPath(): Promise { + const candidates = [ + path.join(REPO_ROOT, "scripts/codegen/node_modules/@github/copilot/schemas/session-events.schema.json"), + path.join(REPO_ROOT, "nodejs/node_modules/@github/copilot/schemas/session-events.schema.json"), + ]; + for (const p of candidates) { + try { + await fs.access(p); + return p; + } catch { + // try next + } + } + throw new Error("session-events.schema.json not found. Run 'npm ci' in scripts/codegen first."); +} + +async function getApiSchemaPath(): Promise { + const candidates = [ + path.join(REPO_ROOT, "scripts/codegen/node_modules/@github/copilot/schemas/api.schema.json"), + path.join(REPO_ROOT, "nodejs/node_modules/@github/copilot/schemas/api.schema.json"), + ]; + for (const p of candidates) { + try { + await fs.access(p); + return p; + } catch { + // try next + } + } + throw new Error("api.schema.json not found. Run 'npm ci' in scripts/codegen first."); +} + +// ── File writing ───────────────────────────────────────────────────────────── + +async function writeGeneratedFile(relativePath: string, content: string): Promise { + const fullPath = path.join(REPO_ROOT, relativePath); + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, content, "utf-8"); + console.log(` βœ“ ${relativePath}`); + return fullPath; +} + +// ── Java type mapping ───────────────────────────────────────────────────────── + +interface JavaTypeResult { + javaType: string; + imports: Set; +} + +// Module-level state for $ref resolution during codegen. +// Set before each schema generation pass; used by schemaTypeToJava and helpers. +let currentDefinitions: Record = {}; +const pendingStandaloneTypes = new Map(); + +/** + * Resolve a $ref in a JSON Schema against the current definitions. + * Returns the resolved schema, or the original if no $ref is present. + */ +function resolveRef(schema: JSONSchema7 | undefined): JSONSchema7 | undefined { + if (!schema) return schema; + if (schema.$ref) { + const name = schema.$ref.replace(/^#\/definitions\//, ""); + const resolved = currentDefinitions[name]; + if (!resolved) { + console.warn(`[codegen] Unresolved $ref: ${schema.$ref}`); + return schema; + } + return resolved; + } + return schema; +} + +function schemaTypeToJava( + schema: JSONSchema7, + required: boolean, + context: string, + propName: string, + nestedTypes: Map +): JavaTypeResult { + const imports = new Set(); + + // Resolve $ref first β€” register standalone types for generation + if (schema.$ref) { + const name = schema.$ref.replace(/^#\/definitions\//, ""); + const resolved = currentDefinitions[name]; + if (resolved) { + // Enum or object types β†’ register for standalone generation, return ref name + if ((resolved.type === "string" && resolved.enum) || + (resolved.type === "object" && resolved.properties)) { + pendingStandaloneTypes.set(name, resolved); + return { javaType: name, imports }; + } + // Other types (primitives, arrays, maps, anyOf unions) β†’ resolve and recurse + return schemaTypeToJava(resolved, required, context, propName, nestedTypes); + } + // Unresolved $ref β€” return name as-is + console.warn(`[codegen] Unresolved $ref: ${schema.$ref}`); + return { javaType: name, imports }; + } + + if (schema.anyOf) { + const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); + const nonNull = schema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); + if (nonNull.length === 1) { + const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, context, propName, nestedTypes); + return result; + } + // Multi-branch anyOf: fall through to Object, matching the C# generator's + // behavior. Java has no union types, so Object is the correct erasure for + // anyOf[string, object] and similar multi-variant schemas. + console.warn(`[codegen] ${context}.${propName}: anyOf with ${nonNull.length} non-null branches β€” falling back to Object`); + return { javaType: "Object", imports }; + } + + if (schema.type === "string") { + if (schema.format === "uuid") { + imports.add("java.util.UUID"); + return { javaType: "UUID", imports }; + } + if (schema.format === "date-time") { + imports.add("java.time.OffsetDateTime"); + return { javaType: "OffsetDateTime", imports }; + } + if (schema.enum && Array.isArray(schema.enum)) { + const enumName = `${context}${toPascalCase(propName)}`; + nestedTypes.set(enumName, { + kind: "enum", + name: enumName, + values: schema.enum as string[], + description: schema.description, + }); + return { javaType: enumName, imports }; + } + return { javaType: "String", imports }; + } + + if (Array.isArray(schema.type)) { + const nonNullTypes = schema.type.filter((t) => t !== "null"); + if (nonNullTypes.length === 1) { + const baseSchema = { ...schema, type: nonNullTypes[0] }; + return schemaTypeToJava(baseSchema as JSONSchema7, required, context, propName, nestedTypes); + } + } + + if (schema.type === "integer") { + // JSON Schema "integer" maps to Long (boxed β€” always used for records). + // Use primitive long for required fields in mutable-bean contexts if needed. + return { javaType: required ? "long" : "Long", imports }; + } + + if (schema.type === "number") { + return { javaType: required ? "double" : "Double", imports }; + } + + if (schema.type === "boolean") { + return { javaType: required ? "boolean" : "Boolean", imports }; + } + + if (schema.type === "array") { + const items = schema.items as JSONSchema7 | undefined; + if (items) { + // Always pass required=false so primitives are boxed (List, not List) + const itemResult = schemaTypeToJava(items, false, context, propName + "Item", nestedTypes); + imports.add("java.util.List"); + for (const imp of itemResult.imports) imports.add(imp); + return { javaType: `List<${itemResult.javaType}>`, imports }; + } + imports.add("java.util.List"); + console.warn(`[codegen] ${context}.${propName}: array without typed items β€” falling back to List`); + return { javaType: "List", imports }; + } + + if (schema.type === "object") { + if (schema.properties && Object.keys(schema.properties).length > 0) { + const nestedName = `${context}${toPascalCase(propName)}`; + if (!nestedTypes.has(nestedName)) { + nestedTypes.set(nestedName, { + kind: "class", + name: nestedName, + schema, + description: schema.description, + }); + } + return { javaType: nestedName, imports }; + } + if (schema.additionalProperties) { + const valueSchema = typeof schema.additionalProperties === "object" + ? schema.additionalProperties as JSONSchema7 + : { type: "object" } as JSONSchema7; + // Always pass required=false so primitives are boxed (Map, not Map) + const valueResult = schemaTypeToJava(valueSchema, false, context, propName + "Value", nestedTypes); + imports.add("java.util.Map"); + for (const imp of valueResult.imports) imports.add(imp); + return { javaType: `Map`, imports }; + } + imports.add("java.util.Map"); + console.warn(`[codegen] ${context}.${propName}: object without typed properties or additionalProperties β€” falling back to Map`); + return { javaType: "Map", imports }; + } + + console.warn(`[codegen] ${context}.${propName}: unrecognized schema (type=${JSON.stringify(schema.type)}) β€” falling back to Object`); + return { javaType: "Object", imports }; +} + +// ── Class definitions ───────────────────────────────────────────────────────── + +interface JavaClassDef { + kind: "class" | "enum"; + name: string; + description?: string; + schema?: JSONSchema7; + values?: string[]; // for enum +} + +// ── Session Events codegen ──────────────────────────────────────────────────── + +interface EventVariant { + typeName: string; + className: string; + dataSchema: JSONSchema7 | null; + description?: string; +} + +function extractEventVariants(schema: JSONSchema7): EventVariant[] { + const definitions = schema.definitions as Record; + const sessionEvent = definitions?.SessionEvent; + if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf"); + + return (sessionEvent.anyOf as JSONSchema7[]) + .map((variant) => { + // Resolve $ref if present (1.0.35+ schema uses $ref to named definitions) + let resolved = variant; + if (variant.$ref) { + const refName = variant.$ref.replace(/^#\/definitions\//, ""); + resolved = definitions[refName]; + if (!resolved) throw new Error(`Unresolved $ref: ${variant.$ref}`); + } + const typeSchema = resolved.properties?.type as JSONSchema7; + const typeName = typeSchema?.const as string; + if (!typeName) throw new Error("Variant must have type.const"); + const baseName = toJavaClassName(typeName); + let dataSchema = resolved.properties?.data as JSONSchema7 | undefined; + // Resolve $ref on data schema if present + if (dataSchema?.$ref) { + const dataRefName = dataSchema.$ref.replace(/^#\/definitions\//, ""); + dataSchema = definitions[dataRefName]; + } + return { + typeName, + className: `${baseName}Event`, + dataSchema: dataSchema ?? null, + description: resolved.description, + }; + }) + .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName)); +} + +async function generateSessionEvents(schemaPath: string): Promise { + console.log("\nπŸ“‹ Generating session event classes..."); + const schemaContent = await fs.readFile(schemaPath, "utf-8"); + const schema = JSON.parse(schemaContent) as JSONSchema7; + + // Set module-level definitions for $ref resolution + currentDefinitions = (schema.definitions ?? {}) as Record; + pendingStandaloneTypes.clear(); + + const variants = extractEventVariants(schema); + const packageName = "com.github.copilot.sdk.generated"; + const packageDir = `src/generated/java/com/github/copilot/sdk/generated`; + + // Generate base SessionEvent class + await generateSessionEventBaseClass(variants, packageName, packageDir); + + // Generate one class file per event variant + for (const variant of variants) { + await generateEventVariantClass(variant, packageName, packageDir); + } + + // Generate standalone types discovered via $ref resolution + await generatePendingStandaloneTypes(packageName, packageDir, GENERATED_FROM_SESSION_EVENTS); + + console.log(`βœ… Generated ${variants.length + 1} session event files`); +} + +async function generateSessionEventBaseClass( + variants: EventVariant[], + packageName: string, + packageDir: string +): Promise { + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_SESSION_EVENTS); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + lines.push(`import com.fasterxml.jackson.annotation.JsonIgnoreProperties;`); + lines.push(`import com.fasterxml.jackson.annotation.JsonInclude;`); + lines.push(`import com.fasterxml.jackson.annotation.JsonProperty;`); + lines.push(`import com.fasterxml.jackson.annotation.JsonSubTypes;`); + lines.push(`import com.fasterxml.jackson.annotation.JsonTypeInfo;`); + lines.push(`import java.time.OffsetDateTime;`); + lines.push(`import java.util.UUID;`); + lines.push(`import javax.annotation.processing.Generated;`); + lines.push(""); + lines.push(`/**`); + lines.push(` * Base class for all generated session events.`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); + lines.push(`@JsonInclude(JsonInclude.Include.NON_NULL)`); + lines.push(`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = UnknownSessionEvent.class)`); + lines.push(`@JsonSubTypes({`); + for (let i = 0; i < variants.length; i++) { + const v = variants[i]; + const comma = i < variants.length - 1 ? "," : ""; + lines.push(` @JsonSubTypes.Type(value = ${v.className}.class, name = "${v.typeName}")${comma}`); + } + lines.push(`})`); + lines.push(GENERATED_ANNOTATION); + + // Build the permits clause (all variant classes + UnknownSessionEvent last) + const allPermitted = [...variants.map((v) => v.className), "UnknownSessionEvent"]; + lines.push(`public abstract sealed class SessionEvent permits`); + for (let i = 0; i < allPermitted.length; i++) { + const comma = i < allPermitted.length - 1 ? "," : " {"; + lines.push(` ${allPermitted[i]}${comma}`); + } + lines.push(""); + lines.push(` /** Unique event identifier (UUID v4), generated when the event is emitted. */`); + lines.push(` @JsonProperty("id")`); + lines.push(` private UUID id;`); + lines.push(""); + lines.push(` /** ISO 8601 timestamp when the event was created. */`); + lines.push(` @JsonProperty("timestamp")`); + lines.push(` private OffsetDateTime timestamp;`); + lines.push(""); + lines.push(` /** ID of the chronologically preceding event in the session. Null for the first event. */`); + lines.push(` @JsonProperty("parentId")`); + lines.push(` private UUID parentId;`); + lines.push(""); + lines.push(` /** When true, the event is transient and not persisted to the session event log on disk. */`); + lines.push(` @JsonProperty("ephemeral")`); + lines.push(` private Boolean ephemeral;`); + lines.push(""); + lines.push(` /**`); + lines.push(` * Returns the event-type discriminator string (e.g., {@code "session.idle"}).`); + lines.push(` *`); + lines.push(` * @return the event type`); + lines.push(` */`); + lines.push(` public abstract String getType();`); + lines.push(""); + lines.push(` public UUID getId() { return id; }`); + lines.push(` public void setId(UUID id) { this.id = id; }`); + lines.push(""); + lines.push(` public OffsetDateTime getTimestamp() { return timestamp; }`); + lines.push(` public void setTimestamp(OffsetDateTime timestamp) { this.timestamp = timestamp; }`); + lines.push(""); + lines.push(` public UUID getParentId() { return parentId; }`); + lines.push(` public void setParentId(UUID parentId) { this.parentId = parentId; }`); + lines.push(""); + lines.push(` public Boolean getEphemeral() { return ephemeral; }`); + lines.push(` public void setEphemeral(Boolean ephemeral) { this.ephemeral = ephemeral; }`); + lines.push(`}`); + lines.push(""); + + await writeGeneratedFile(`${packageDir}/SessionEvent.java`, lines.join("\n")); + + // Also generate the UnknownSessionEvent fallback + await generateUnknownEventClass(packageName, packageDir); +} + +async function generateUnknownEventClass(packageName: string, packageDir: string): Promise { + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_SESSION_EVENTS); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + lines.push(`import com.fasterxml.jackson.annotation.JsonIgnoreProperties;`); + lines.push(`import com.fasterxml.jackson.annotation.JsonProperty;`); + lines.push(`import javax.annotation.processing.Generated;`); + lines.push(""); + lines.push(`/**`); + lines.push(` * Fallback for event types not yet known to this SDK version.`); + lines.push(` *

`); + lines.push(` * {@link #getType()} returns the original type string from the JSON payload,`); + lines.push(` * preserving forward compatibility with event types introduced by newer CLI versions.`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); + lines.push(GENERATED_ANNOTATION); + lines.push(`public final class UnknownSessionEvent extends SessionEvent {`); + lines.push(""); + lines.push(` @JsonProperty("type")`); + lines.push(` private String type = "unknown";`); + lines.push(""); + lines.push(` @Override`); + lines.push(` public String getType() { return type; }`); + lines.push(`}`); + lines.push(""); + + await writeGeneratedFile(`${packageDir}/UnknownSessionEvent.java`, lines.join("\n")); +} + +/** Render a nested type (enum or record) indented at the given level. */ +function renderNestedType(nested: JavaClassDef, indentLevel: number, nestedTypes: Map, allImports: Set): string[] { + const ind = " ".repeat(indentLevel); + const lines: string[] = []; + + if (nested.kind === "enum") { + lines.push(""); + if (nested.description) { + lines.push(`${ind}/** ${nested.description} */`); + } + lines.push(`${ind}public enum ${nested.name} {`); + for (let i = 0; i < (nested.values || []).length; i++) { + const v = nested.values![i]; + const comma = i < nested.values!.length - 1 ? "," : ";"; + lines.push(`${ind} /** The {@code ${v}} variant. */`); + lines.push(`${ind} ${toEnumConstant(v)}("${v}")${comma}`); + } + lines.push(""); + lines.push(`${ind} private final String value;`); + lines.push(`${ind} ${nested.name}(String value) { this.value = value; }`); + lines.push(`${ind} @com.fasterxml.jackson.annotation.JsonValue`); + lines.push(`${ind} public String getValue() { return value; }`); + lines.push(`${ind} @com.fasterxml.jackson.annotation.JsonCreator`); + lines.push(`${ind} public static ${nested.name} fromValue(String value) {`); + lines.push(`${ind} for (${nested.name} v : values()) {`); + lines.push(`${ind} if (v.value.equals(value)) return v;`); + lines.push(`${ind} }`); + lines.push(`${ind} throw new IllegalArgumentException("Unknown ${nested.name} value: " + value);`); + lines.push(`${ind} }`); + lines.push(`${ind}}`); + } else if (nested.kind === "class" && nested.schema?.properties) { + const localNestedTypes = new Map(); + const fields: { jsonName: string; javaName: string; javaType: string; description?: string }[] = []; + + for (const [propName, propSchema] of Object.entries(nested.schema.properties)) { + if (typeof propSchema !== "object") continue; + const prop = propSchema as JSONSchema7; + // Record components are always boxed (nullable by design). + const result = schemaTypeToJava(prop, false, nested.name, propName, localNestedTypes); + for (const imp of result.imports) allImports.add(imp); + fields.push({ jsonName: propName, javaName: toCamelCase(propName), javaType: result.javaType, description: prop.description }); + } + + lines.push(""); + if (nested.description) { + lines.push(`${ind}/** ${nested.description} */`); + } + lines.push(`${ind}@JsonIgnoreProperties(ignoreUnknown = true)`); + lines.push(`${ind}@JsonInclude(JsonInclude.Include.NON_NULL)`); + if (fields.length === 0) { + lines.push(`${ind}public record ${nested.name}() {`); + } else { + lines.push(`${ind}public record ${nested.name}(`); + for (let i = 0; i < fields.length; i++) { + const f = fields[i]; + const comma = i < fields.length - 1 ? "," : ""; + if (f.description) lines.push(`${ind} /** ${f.description} */`); + lines.push(`${ind} @JsonProperty("${f.jsonName}") ${f.javaType} ${f.javaName}${comma}`); + } + lines.push(`${ind}) {`); + } + // Render any further nested types inside this record + for (const [, localNested] of localNestedTypes) { + lines.push(...renderNestedType(localNested, indentLevel + 1, nestedTypes, allImports)); + } + if (lines[lines.length - 1] !== "") lines.push(""); + lines.pop(); // remove trailing blank before closing brace + lines.push(`${ind}}`); + } + + return lines; +} + +async function generateEventVariantClass( + variant: EventVariant, + packageName: string, + packageDir: string +): Promise { + const lines: string[] = []; + const allImports = new Set([ + "com.fasterxml.jackson.annotation.JsonIgnoreProperties", + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.annotation.JsonInclude", + "javax.annotation.processing.Generated", + ]); + const nestedTypes = new Map(); + + // Collect data record fields + interface FieldInfo { + jsonName: string; + javaName: string; + javaType: string; + description?: string; + } + + const dataFields: FieldInfo[] = []; + + if (variant.dataSchema?.properties) { + for (const [propName, propSchema] of Object.entries(variant.dataSchema.properties)) { + if (typeof propSchema !== "object") continue; + const prop = propSchema as JSONSchema7; + // Record components are always boxed (nullable by design). + const result = schemaTypeToJava(prop, false, `${variant.className}Data`, propName, nestedTypes); + for (const imp of result.imports) allImports.add(imp); + dataFields.push({ + jsonName: propName, + javaName: toCamelCase(propName), + javaType: result.javaType, + description: prop.description, + }); + } + } + + // Whether a data record should be emitted (always when dataSchema is present) + const hasDataSchema = variant.dataSchema !== null; + + // Build the file + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_SESSION_EVENTS); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + + // Placeholder for imports + const importPlaceholderIdx = lines.length; + lines.push("__IMPORTS__"); + lines.push(""); + + if (variant.description) { + lines.push(`/**`); + lines.push(` * ${variant.description}`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } else { + lines.push(`/**`); + lines.push(` * The {@code ${variant.typeName}} session event.`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } + lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); + lines.push(`@JsonInclude(JsonInclude.Include.NON_NULL)`); + lines.push(GENERATED_ANNOTATION); + lines.push(`public final class ${variant.className} extends SessionEvent {`); + lines.push(""); + lines.push(` @Override`); + lines.push(` public String getType() { return "${variant.typeName}"; }`); + + if (hasDataSchema) { + lines.push(""); + lines.push(` @JsonProperty("data")`); + lines.push(` private ${variant.className}Data data;`); + lines.push(""); + lines.push(` public ${variant.className}Data getData() { return data; }`); + lines.push(` public void setData(${variant.className}Data data) { this.data = data; }`); + lines.push(""); + // Generate data inner record + lines.push(` /** Data payload for {@link ${variant.className}}. */`); + lines.push(` @JsonIgnoreProperties(ignoreUnknown = true)`); + lines.push(` @JsonInclude(JsonInclude.Include.NON_NULL)`); + if (dataFields.length === 0) { + lines.push(` public record ${variant.className}Data() {`); + } else { + lines.push(` public record ${variant.className}Data(`); + for (let i = 0; i < dataFields.length; i++) { + const field = dataFields[i]; + const comma = i < dataFields.length - 1 ? "," : ""; + if (field.description) { + lines.push(` /** ${field.description} */`); + } + lines.push(` @JsonProperty("${field.jsonName}") ${field.javaType} ${field.javaName}${comma}`); + } + lines.push(` ) {`); + } + // Render nested types inside Data record + for (const [, nested] of nestedTypes) { + lines.push(...renderNestedType(nested, 2, nestedTypes, allImports)); + } + if (nestedTypes.size > 0 && lines[lines.length - 1] === "") lines.pop(); + lines.push(` }`); + } + + lines.push(`}`); + lines.push(""); + + // Replace import placeholder + const sortedImports = [...allImports].sort(); + const importLines = sortedImports.map((i) => `import ${i};`).join("\n"); + lines[importPlaceholderIdx] = importLines; + + await writeGeneratedFile(`${packageDir}/${variant.className}.java`, lines.join("\n")); +} + +// ── Standalone $ref type generation ────────────────────────────────────────── + +/** + * Generate all pending standalone types discovered via $ref resolution. + * Iterates until no new types are discovered (handles transitive $ref chains). + */ +async function generatePendingStandaloneTypes( + packageName: string, + packageDir: string, + headerComment: string +): Promise { + const generated = new Set(); + + while (true) { + const batch: [string, JSONSchema7][] = []; + for (const [name, schema] of pendingStandaloneTypes) { + if (!generated.has(name)) { + batch.push([name, schema]); + generated.add(name); + } + } + pendingStandaloneTypes.clear(); + + if (batch.length === 0) break; + + for (const [name, schema] of batch) { + if (schema.type === "string" && schema.enum) { + await generateStandaloneEnum(name, schema, packageName, packageDir, headerComment); + } else if (schema.type === "object" && schema.properties) { + await generateStandaloneRecord(name, schema, packageName, packageDir, headerComment); + } else { + console.warn(`[codegen] Cannot generate standalone type for ${name}: type=${schema.type}`); + } + } + // Generating records may have discovered more $ref targets β€” loop again + } +} + +async function generateStandaloneEnum( + name: string, + schema: JSONSchema7, + packageName: string, + packageDir: string, + headerComment: string +): Promise { + const values = schema.enum as string[]; + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(headerComment); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + lines.push(`import javax.annotation.processing.Generated;`); + lines.push(""); + if (schema.description) { + lines.push(`/**`); + lines.push(` * ${schema.description}`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } + lines.push(GENERATED_ANNOTATION); + lines.push(`public enum ${name} {`); + for (let i = 0; i < values.length; i++) { + const v = values[i]; + const comma = i < values.length - 1 ? "," : ";"; + lines.push(` /** The {@code ${v}} variant. */`); + lines.push(` ${toEnumConstant(v)}("${v}")${comma}`); + } + lines.push(""); + lines.push(` private final String value;`); + lines.push(` ${name}(String value) { this.value = value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonValue`); + lines.push(` public String getValue() { return value; }`); + lines.push(` @com.fasterxml.jackson.annotation.JsonCreator`); + lines.push(` public static ${name} fromValue(String value) {`); + lines.push(` for (${name} v : values()) {`); + lines.push(` if (v.value.equals(value)) return v;`); + lines.push(` }`); + lines.push(` throw new IllegalArgumentException("Unknown ${name} value: " + value);`); + lines.push(` }`); + lines.push(`}`); + lines.push(""); + + await writeGeneratedFile(`${packageDir}/${name}.java`, lines.join("\n")); +} + +async function generateStandaloneRecord( + name: string, + schema: JSONSchema7, + packageName: string, + packageDir: string, + headerComment: string +): Promise { + const nestedTypes = new Map(); + const { code, imports } = generateRpcClass(name, schema, nestedTypes, packageName); + + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(headerComment); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + + const allImports = new Set([ + "com.fasterxml.jackson.annotation.JsonIgnoreProperties", + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.annotation.JsonInclude", + "javax.annotation.processing.Generated", + ...imports, + ]); + const sortedImports = [...allImports].sort(); + for (const imp of sortedImports) { + lines.push(`import ${imp};`); + } + lines.push(""); + + if (schema.description) { + lines.push(`/**`); + lines.push(` * ${schema.description}`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } + lines.push(GENERATED_ANNOTATION); + lines.push(code); + lines.push(""); + + await writeGeneratedFile(`${packageDir}/${name}.java`, lines.join("\n")); +} + +// ── RPC types codegen ───────────────────────────────────────────────────────── + +interface RpcMethod { + rpcMethod: string; + params: JSONSchema7 | null; + result: JSONSchema7 | null; + stability?: string; +} + +function isRpcMethod(node: unknown): node is RpcMethod { + return typeof node === "object" && node !== null && "rpcMethod" in node; +} + +function collectRpcMethods(node: Record): [string, RpcMethod][] { + const results: [string, RpcMethod][] = []; + for (const [key, value] of Object.entries(node)) { + if (isRpcMethod(value)) { + results.push([key, value]); + } else if (typeof value === "object" && value !== null) { + results.push(...collectRpcMethods(value as Record)); + } + } + return results; +} + +/** Convert an RPC method name to a Java class name prefix (e.g., "models.list" -> "ModelsList") */ +function rpcMethodToClassName(rpcMethod: string): string { + return rpcMethod.split(/[._-]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); +} + +/** Generate a Java record for a JSON Schema object type. Returns the class content. */ +function generateRpcClass( + className: string, + schema: JSONSchema7, + _nestedTypes: Map, + _packageName: string, + visibility: "public" | "internal" = "public" +): { code: string; imports: Set } { + const imports = new Set(); + const localNestedTypes = new Map(); + const lines: string[] = []; + const visModifier = visibility === "public" ? "public " : ""; + + const properties = Object.entries(schema.properties || {}); + const fields = properties.flatMap(([propName, propSchema]) => { + if (typeof propSchema !== "object") return []; + const prop = propSchema as JSONSchema7; + // Record components are always boxed (nullable by design). + const result = schemaTypeToJava(prop, false, className, propName, localNestedTypes); + for (const imp of result.imports) imports.add(imp); + return [{ propName, javaName: toCamelCase(propName), javaType: result.javaType, description: prop.description }]; + }); + + lines.push(`@JsonInclude(JsonInclude.Include.NON_NULL)`); + lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); + if (fields.length === 0) { + lines.push(`${visModifier}record ${className}() {`); + } else { + lines.push(`${visModifier}record ${className}(`); + for (let i = 0; i < fields.length; i++) { + const f = fields[i]; + const comma = i < fields.length - 1 ? "," : ""; + if (f.description) { + lines.push(` /** ${f.description} */`); + } + lines.push(` @JsonProperty("${f.propName}") ${f.javaType} ${f.javaName}${comma}`); + } + lines.push(`) {`); + } + + // Add nested types as nested records/enums inside this record + for (const [, nested] of localNestedTypes) { + lines.push(...renderNestedType(nested, 1, new Map(), imports)); + } + + if (localNestedTypes.size > 0 && lines[lines.length - 1] === "") lines.pop(); + lines.push(`}`); + + return { code: lines.join("\n"), imports }; +} + +async function generateRpcTypes(schemaPath: string): Promise { + console.log("\nπŸ”Œ Generating RPC types..."); + const schemaContent = await fs.readFile(schemaPath, "utf-8"); + const schema = JSON.parse(schemaContent) as Record & { + server?: Record; + session?: Record; + clientSession?: Record; + definitions?: Record; + }; + + // Set module-level definitions for $ref resolution + currentDefinitions = (schema.definitions ?? {}) as Record; + pendingStandaloneTypes.clear(); + + const packageName = "com.github.copilot.sdk.generated.rpc"; + const packageDir = `src/generated/java/com/github/copilot/sdk/generated/rpc`; + + // Collect all RPC methods from all sections + const sections: [string, Record][] = []; + if (schema.server) sections.push(["server", schema.server]); + if (schema.session) sections.push(["session", schema.session]); + if (schema.clientSession) sections.push(["clientSession", schema.clientSession]); + + const generatedClasses = new Map(); + const allFiles: string[] = []; + + for (const [, sectionNode] of sections) { + const methods = collectRpcMethods(sectionNode); + for (const [, method] of methods) { + const className = rpcMethodToClassName(method.rpcMethod); + + // Generate params class β€” resolve $ref if params is a reference + let paramsSchema = method.params as JSONSchema7 | null; + if (paramsSchema?.$ref) paramsSchema = resolveRef(paramsSchema) as JSONSchema7; + if (paramsSchema && typeof paramsSchema === "object" && paramsSchema.properties) { + const paramsClassName = `${className}Params`; + if (!generatedClasses.has(paramsClassName)) { + generatedClasses.set(paramsClassName, true); + allFiles.push(await generateRpcDataClass(paramsClassName, paramsSchema, packageName, packageDir, method.rpcMethod, "params")); + } + } + + // Generate result class β€” resolve $ref if result is a reference + let resultSchema = method.result as JSONSchema7 | null; + if (resultSchema?.$ref) resultSchema = resolveRef(resultSchema) as JSONSchema7; + if (resultSchema && typeof resultSchema === "object" && resultSchema.properties) { + const resultClassName = `${className}Result`; + if (!generatedClasses.has(resultClassName)) { + generatedClasses.set(resultClassName, true); + allFiles.push(await generateRpcDataClass(resultClassName, resultSchema, packageName, packageDir, method.rpcMethod, "result")); + } + } + } + } + + // Generate standalone types discovered via $ref resolution + await generatePendingStandaloneTypes(packageName, packageDir, GENERATED_FROM_API); + + console.log(`βœ… Generated ${allFiles.length} RPC type files`); +} + +async function generateRpcDataClass( + className: string, + schema: JSONSchema7, + packageName: string, + packageDir: string, + rpcMethod: string, + kind: "params" | "result" +): Promise { + const nestedTypes = new Map(); + const { code, imports } = generateRpcClass(className, schema, nestedTypes, packageName); + + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(""); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_API); + lines.push(""); + lines.push(`package ${packageName};`); + lines.push(""); + + const allImports = new Set([ + "com.fasterxml.jackson.annotation.JsonIgnoreProperties", + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.annotation.JsonInclude", + "javax.annotation.processing.Generated", + ...imports, + ]); + const sortedImports = [...allImports].sort(); + for (const imp of sortedImports) { + lines.push(`import ${imp};`); + } + lines.push(""); + + if (schema.description) { + lines.push(`/**`); + lines.push(` * ${schema.description}`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } else { + lines.push(`/**`); + lines.push(` * ${kind === "params" ? "Request parameters" : "Result"} for the {@code ${rpcMethod}} RPC method.`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + } + lines.push(GENERATED_ANNOTATION); + lines.push(code); + lines.push(""); + + await writeGeneratedFile(`${packageDir}/${className}.java`, lines.join("\n")); + return className; +} + +// ── RPC wrapper generation ─────────────────────────────────────────────────── + +/** A single RPC method node parsed from the schema */ +interface RpcMethodNode { + rpcMethod: string; + stability: string; + params: JSONSchema7 | null; + result: JSONSchema7 | null; +} + +/** Namespace tree node: holds direct methods and sub-namespace trees */ +interface NamespaceTree { + methods: Map; // leaf method name -> info + subspaces: Map; // sub-namespace name -> tree +} + +/** Build a namespace tree by recursively walking a schema section object */ +function buildNamespaceTree(node: Record): NamespaceTree { + const tree: NamespaceTree = { methods: new Map(), subspaces: new Map() }; + for (const [key, value] of Object.entries(node)) { + if (typeof value !== "object" || value === null) continue; + const obj = value as Record; + if ("rpcMethod" in obj) { + tree.methods.set(key, { + rpcMethod: String(obj.rpcMethod), + stability: String(obj.stability ?? "stable"), + params: (obj.params as JSONSchema7) ?? null, + result: (obj.result as JSONSchema7) ?? null, + }); + } else { + const child = buildNamespaceTree(obj); + // Only add non-empty sub-trees + if (child.methods.size > 0 || child.subspaces.size > 0) { + tree.subspaces.set(key, child); + } + } + } + return tree; +} + +/** + * Derive the Java class name for an API namespace class. + * e.g., prefix="Server", path=["mcp","config"] β†’ "ServerMcpConfigApi" + */ +function apiClassName(prefix: string, path: string[]): string { + const parts = [prefix, ...path].map((p) => p.charAt(0).toUpperCase() + p.slice(1)); + return parts.join("") + "Api"; +} + +/** + * Derive the result class name for an RPC method. + * If the result schema has no properties we use Void; if no result schema we also use Void. + */ +function wrapperResultClassName(method: RpcMethodNode): string { + let result = method.result; + if (result?.$ref) result = resolveRef(result) as JSONSchema7; + if ( + result && + typeof result === "object" && + result.properties && + Object.keys(result.properties).length > 0 + ) { + return rpcMethodToClassName(method.rpcMethod) + "Result"; + } + return "Void"; +} + +/** + * Return the params class name if the method has a params schema with properties + * other than sessionId (i.e. there are user-supplied parameters). + */ +function wrapperParamsClassName(method: RpcMethodNode): string | null { + let params = method.params; + if (params?.$ref) params = resolveRef(params) as JSONSchema7; + if (!params || typeof params !== "object") return null; + const props = params.properties ?? {}; + const userProps = Object.keys(props).filter((k) => k !== "sessionId"); + if (userProps.length === 0) return null; + return rpcMethodToClassName(method.rpcMethod) + "Params"; +} + +/** True if the method's params schema contains a "sessionId" property */ +function methodHasSessionId(method: RpcMethodNode): boolean { + let params = method.params; + if (params?.$ref) params = resolveRef(params) as JSONSchema7; + return !!params?.properties && "sessionId" in params.properties; +} + +/** + * Generate the Java source for a single method in a wrapper API class. + * Returns the Java source lines and whether an ObjectMapper is required. + */ +function generateApiMethod( + key: string, + method: RpcMethodNode, + isSession: boolean, + sessionIdExpr: string +): { lines: string[]; needsMapper: boolean } { + const resultClass = wrapperResultClassName(method); + const paramsClass = wrapperParamsClassName(method); + const hasSessionId = methodHasSessionId(method); + const hasExtraParams = paramsClass !== null; + let needsMapper = false; + + const lines: string[] = []; + + // Javadoc + const description = (method.params as JSONSchema7 | null)?.description + ?? (method.result as JSONSchema7 | null)?.description + ?? `Invokes {@code ${method.rpcMethod}}.`; + lines.push(` /**`); + lines.push(` * ${description}`); + if (isSession && hasExtraParams && hasSessionId) { + lines.push(` *

`); + lines.push(` * Note: the {@code sessionId} field in the params record is overridden`); + lines.push(` * by the session-scoped wrapper; any value provided is ignored.`); + } + if (method.stability === "experimental") { + lines.push(` *`); + lines.push(` * @apiNote This method is experimental and may change in a future version.`); + } + lines.push(` * @since 1.0.0`); + lines.push(` */`); + + // Signature + if (hasExtraParams) { + lines.push(` public CompletableFuture<${resultClass}> ${key}(${paramsClass} params) {`); + } else { + lines.push(` public CompletableFuture<${resultClass}> ${key}() {`); + } + + // Body + if (isSession) { + if (hasExtraParams) { + // Merge sessionId into the params using Jackson ObjectNode + needsMapper = true; + lines.push(` com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params);`); + lines.push(` _p.put("sessionId", ${sessionIdExpr});`); + lines.push(` return caller.invoke("${method.rpcMethod}", _p, ${resultClass}.class);`); + } else if (hasSessionId) { + lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of("sessionId", ${sessionIdExpr}), ${resultClass}.class);`); + } else { + lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${resultClass}.class);`); + } + } else { + // Server-side: pass params directly (or empty map if no params) + if (hasExtraParams) { + lines.push(` return caller.invoke("${method.rpcMethod}", params, ${resultClass}.class);`); + } else { + lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${resultClass}.class);`); + } + } + + lines.push(` }`); + lines.push(``); + + return { lines, needsMapper }; +} + +/** + * Generate a Java source file for a single namespace API class. + * Returns the generated class name and whether a mapper static field is needed. + */ +async function generateNamespaceApiFile( + prefix: string, + namespacePath: string[], + tree: NamespaceTree, + isSession: boolean, + packageName: string, + packageDir: string +): Promise { + const className = apiClassName(prefix, namespacePath); + const sessionIdExpr = "this.sessionId"; + + const classLines: string[] = []; + const allImports = new Set([ + "java.util.concurrent.CompletableFuture", + "javax.annotation.processing.Generated", + ]); + let needsMapper = false; + + // Generate sub-namespace fields + const subFields: string[] = []; + const subInits: string[] = []; + for (const [subKey, subTree] of tree.subspaces) { + const subClass = apiClassName(prefix, [...namespacePath, subKey]); + subFields.push(` /** API methods for the {@code ${[...namespacePath, subKey].join(".")}} sub-namespace. */`); + subFields.push(` public final ${subClass} ${subKey};`); + if (isSession) { + subInits.push(` this.${subKey} = new ${subClass}(caller, sessionId);`); + } else { + subInits.push(` this.${subKey} = new ${subClass}(caller);`); + } + // Recursively generate sub-namespace files + await generateNamespaceApiFile(prefix, [...namespacePath, subKey], subTree, isSession, packageName, packageDir); + } + + // Collect result/param imports and generate methods + const methodLines: string[] = []; + for (const [key, method] of tree.methods) { + const resultClass = wrapperResultClassName(method); + const paramsClass = wrapperParamsClassName(method); + if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`); + if (paramsClass) allImports.add(`${packageName}.${paramsClass}`); + + const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr); + methodLines.push(...lines); + if (nm) needsMapper = true; + } + + // Build class body + const qualifiedNs = namespacePath.length > 0 ? namespacePath.join(".") : prefix.toLowerCase(); + classLines.push(COPYRIGHT); + classLines.push(``); + classLines.push(AUTO_GENERATED_HEADER); + classLines.push(GENERATED_FROM_API); + classLines.push(``); + classLines.push(`package ${packageName};`); + classLines.push(``); + + // Add imports (skip same-package imports) + const sortedImports = [...allImports].filter(imp => !imp.startsWith(packageName + ".")).sort(); + for (const imp of sortedImports) { + classLines.push(`import ${imp};`); + } + classLines.push(``); + + // Javadoc for class + classLines.push(`/**`); + classLines.push(` * API methods for the {@code ${qualifiedNs}} namespace.`); + classLines.push(` *`); + classLines.push(` * @since 1.0.0`); + classLines.push(` */`); + classLines.push(GENERATED_ANNOTATION); + classLines.push(`public final class ${className} {`); + classLines.push(``); + if (needsMapper) { + classLines.push(` private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE;`); + classLines.push(``); + } + classLines.push(` private final RpcCaller caller;`); + if (isSession) { + classLines.push(` private final String sessionId;`); + } + + // Sub-namespace fields + if (subFields.length > 0) { + classLines.push(``); + classLines.push(...subFields); + } + + // Constructor + classLines.push(``); + if (isSession) { + classLines.push(` /** @param caller the RPC transport function */`); + classLines.push(` ${className}(RpcCaller caller, String sessionId) {`); + classLines.push(` this.caller = caller;`); + classLines.push(` this.sessionId = sessionId;`); + } else { + classLines.push(` /** @param caller the RPC transport function */`); + classLines.push(` ${className}(RpcCaller caller) {`); + classLines.push(` this.caller = caller;`); + } + for (const init of subInits) { + classLines.push(init); + } + classLines.push(` }`); + classLines.push(``); + + // Methods + classLines.push(...methodLines); + + classLines.push(`}`); + classLines.push(``); + + await writeGeneratedFile(`${packageDir}/${className}.java`, classLines.join("\n")); + return className; +} + +/** + * Generate ServerRpc.java or SessionRpc.java β€” the top-level wrapper class. + */ +async function generateRpcRootFile( + sectionName: string, // "server" | "session" + tree: NamespaceTree, + isSession: boolean, + packageName: string, + packageDir: string +): Promise { + const prefix = sectionName === "server" ? "Server" : "Session"; + const rootClassName = prefix + "Rpc"; + const sessionIdExpr = "this.sessionId"; + + const classLines: string[] = []; + const allImports = new Set([ + "java.util.concurrent.CompletableFuture", + "javax.annotation.processing.Generated", + ]); + let needsMapper = false; + + // Sub-namespace fields and init lines + const subFields: string[] = []; + const subInits: string[] = []; + for (const [nsKey, nsTree] of tree.subspaces) { + const nsClass = apiClassName(prefix, [nsKey]); + subFields.push(` /** API methods for the {@code ${nsKey}} namespace. */`); + subFields.push(` public final ${nsClass} ${nsKey};`); + if (isSession) { + subInits.push(` this.${nsKey} = new ${nsClass}(caller, sessionId);`); + } else { + subInits.push(` this.${nsKey} = new ${nsClass}(caller);`); + } + // Generate the namespace API class file (recursively) + await generateNamespaceApiFile(prefix, [nsKey], nsTree, isSession, packageName, packageDir); + } + + // Collect result/param imports and generate top-level method bodies + const methodLines: string[] = []; + for (const [key, method] of tree.methods) { + const resultClass = wrapperResultClassName(method); + const paramsClass = wrapperParamsClassName(method); + if (resultClass !== "Void") allImports.add(`${packageName}.${resultClass}`); + if (paramsClass) allImports.add(`${packageName}.${paramsClass}`); + + const { lines, needsMapper: nm } = generateApiMethod(key, method, isSession, sessionIdExpr); + methodLines.push(...lines); + if (nm) needsMapper = true; + } + + // Build file content + classLines.push(COPYRIGHT); + classLines.push(``); + classLines.push(AUTO_GENERATED_HEADER); + classLines.push(GENERATED_FROM_API); + classLines.push(``); + classLines.push(`package ${packageName};`); + classLines.push(``); + + const sortedImports = [...allImports].filter(imp => !imp.startsWith(packageName + ".")).sort(); + for (const imp of sortedImports) { + classLines.push(`import ${imp};`); + } + classLines.push(``); + + classLines.push(`/**`); + if (isSession) { + classLines.push(` * Typed client for session-scoped RPC methods.`); + classLines.push(` *

`); + classLines.push(` * Provides strongly-typed access to all session-level API namespaces.`); + classLines.push(` * The {@code sessionId} is injected automatically into every call.`); + classLines.push(` *

`); + classLines.push(` * Obtain an instance by calling {@code new SessionRpc(caller, sessionId)}.`); + } else { + classLines.push(` * Typed client for server-level RPC methods.`); + classLines.push(` *

`); + classLines.push(` * Provides strongly-typed access to all server-level API namespaces.`); + classLines.push(` *

`); + classLines.push(` * Obtain an instance by calling {@code new ServerRpc(caller)}.`); + } + classLines.push(` *`); + classLines.push(` * @since 1.0.0`); + classLines.push(` */`); + classLines.push(GENERATED_ANNOTATION); + classLines.push(`public final class ${rootClassName} {`); + classLines.push(``); + if (needsMapper) { + classLines.push(` private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE;`); + classLines.push(``); + } + classLines.push(` private final RpcCaller caller;`); + if (isSession) { + classLines.push(` private final String sessionId;`); + } + if (subFields.length > 0) { + classLines.push(``); + classLines.push(...subFields); + } + classLines.push(``); + + // Constructor + if (isSession) { + classLines.push(` /**`); + classLines.push(` * Creates a new session RPC client.`); + classLines.push(` *`); + classLines.push(` * @param caller the RPC transport function (e.g., {@code jsonRpcClient::invoke})`); + classLines.push(` * @param sessionId the session ID to inject into every request`); + classLines.push(` */`); + classLines.push(` public ${rootClassName}(RpcCaller caller, String sessionId) {`); + classLines.push(` this.caller = caller;`); + classLines.push(` this.sessionId = sessionId;`); + } else { + classLines.push(` /**`); + classLines.push(` * Creates a new server RPC client.`); + classLines.push(` *`); + classLines.push(` * @param caller the RPC transport function (e.g., {@code jsonRpcClient::invoke})`); + classLines.push(` */`); + classLines.push(` public ${rootClassName}(RpcCaller caller) {`); + classLines.push(` this.caller = caller;`); + } + for (const init of subInits) { + classLines.push(init); + } + classLines.push(` }`); + classLines.push(``); + + // Top-level methods + classLines.push(...methodLines); + + classLines.push(`}`); + classLines.push(``); + + await writeGeneratedFile(`${packageDir}/${rootClassName}.java`, classLines.join("\n")); +} + +/** Generate the RpcCaller functional interface */ +async function generateRpcCallerInterface(packageName: string, packageDir: string): Promise { + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(``); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_API); + lines.push(``); + lines.push(`package ${packageName};`); + lines.push(``); + lines.push(`import java.util.concurrent.CompletableFuture;`); + lines.push(`import javax.annotation.processing.Generated;`); + lines.push(``); + lines.push(`/**`); + lines.push(` * Interface for invoking JSON-RPC methods with typed responses.`); + lines.push(` *

`); + lines.push(` * Implementations delegate to the underlying transport layer`); + lines.push(` * (e.g., a {@code JsonRpcClient} instance). A method reference is typically the clearest`); + lines.push(` * way to adapt a generic {@code invoke} method to this interface:`); + lines.push(` *

{@code`);
+    lines.push(` * RpcCaller caller = jsonRpcClient::invoke;`);
+    lines.push(` * }
`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + lines.push(GENERATED_ANNOTATION); + lines.push(`public interface RpcCaller {`); + lines.push(``); + lines.push(` /**`); + lines.push(` * Invokes a JSON-RPC method and returns a future for the typed response.`); + lines.push(` *`); + lines.push(` * @param the expected response type`); + lines.push(` * @param method the JSON-RPC method name`); + lines.push(` * @param params the request parameters (may be a {@code Map}, DTO record, or {@code JsonNode})`); + lines.push(` * @param resultType the {@link Class} of the expected response type`); + lines.push(` * @return a {@link CompletableFuture} that completes with the deserialized result`); + lines.push(` */`); + lines.push(` CompletableFuture invoke(String method, Object params, Class resultType);`); + lines.push(`}`); + lines.push(``); + + await writeGeneratedFile(`${packageDir}/RpcCaller.java`, lines.join("\n")); +} + +/** + * Generate RpcMapper.java β€” a package-private holder for the shared ObjectMapper used + * when merging sessionId into session API call params. All session API classes that + * need an ObjectMapper reference this single instance instead of instantiating their own. + */ +async function generateRpcMapperClass(packageName: string, packageDir: string): Promise { + const lines: string[] = []; + lines.push(COPYRIGHT); + lines.push(``); + lines.push(AUTO_GENERATED_HEADER); + lines.push(GENERATED_FROM_API); + lines.push(``); + lines.push(`package ${packageName};`); + lines.push(``); + lines.push(`import javax.annotation.processing.Generated;`); + lines.push(``); + lines.push(`/**`); + lines.push(` * Package-private holder for the shared {@link com.fasterxml.jackson.databind.ObjectMapper}`); + lines.push(` * used by session API classes when merging {@code sessionId} into call parameters.`); + lines.push(` *

`); + lines.push(` * {@link com.fasterxml.jackson.databind.ObjectMapper} is thread-safe and expensive to`); + lines.push(` * instantiate, so a single shared instance is used across all generated API classes.`); + lines.push(` * The configuration mirrors {@code JsonRpcClient}'s mapper (JavaTimeModule, lenient`); + lines.push(` * unknown-property handling, ISO date format, NON_NULL inclusion).`); + lines.push(` *`); + lines.push(` * @since 1.0.0`); + lines.push(` */`); + lines.push(GENERATED_ANNOTATION); + lines.push(`final class RpcMapper {`); + lines.push(``); + lines.push(` static final com.fasterxml.jackson.databind.ObjectMapper INSTANCE = createMapper();`); + lines.push(``); + lines.push(` private static com.fasterxml.jackson.databind.ObjectMapper createMapper() {`); + lines.push(` com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();`); + lines.push(` mapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());`); + lines.push(` mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);`); + lines.push(` mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);`); + lines.push(` mapper.setDefaultPropertyInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL);`); + lines.push(` return mapper;`); + lines.push(` }`); + lines.push(``); + lines.push(` private RpcMapper() {}`); + lines.push(`}`); + lines.push(``); + + await writeGeneratedFile(`${packageDir}/RpcMapper.java`, lines.join("\n")); +} + +/** Main entry point for RPC wrapper generation */ +async function generateRpcWrappers(schemaPath: string): Promise { + console.log("\nπŸ”§ Generating RPC wrapper classes..."); + + const schemaContent = await fs.readFile(schemaPath, "utf-8"); + const schema = JSON.parse(schemaContent) as { + server?: Record; + session?: Record; + clientSession?: Record; + definitions?: Record; + }; + + // Set module-level definitions for $ref resolution in wrapper helpers + currentDefinitions = (schema.definitions ?? {}) as Record; + + const packageName = "com.github.copilot.sdk.generated.rpc"; + const packageDir = `src/generated/java/com/github/copilot/sdk/generated/rpc`; + + // RpcCaller interface and shared ObjectMapper holder + await generateRpcCallerInterface(packageName, packageDir); + await generateRpcMapperClass(packageName, packageDir); + + // Server-side wrappers + if (schema.server) { + const serverTree = buildNamespaceTree(schema.server); + await generateRpcRootFile("server", serverTree, false, packageName, packageDir); + } + + // Session-side wrappers + if (schema.session) { + const sessionTree = buildNamespaceTree(schema.session); + await generateRpcRootFile("session", sessionTree, true, packageName, packageDir); + } + + console.log(`βœ… RPC wrapper classes generated`); +} + +// ── Main entry point ────────────────────────────────────────────────────────── + +async function main(): Promise { + console.log("πŸš€ Java SDK code generator"); + console.log("============================"); + + const sessionEventsSchemaPath = await getSessionEventsSchemaPath(); + console.log(`πŸ“„ Session events schema: ${sessionEventsSchemaPath}`); + const apiSchemaPath = await getApiSchemaPath(); + console.log(`πŸ“„ API schema: ${apiSchemaPath}`); + + await generateSessionEvents(sessionEventsSchemaPath); + await generateRpcTypes(apiSchemaPath); + await generateRpcWrappers(apiSchemaPath); + + console.log("\nβœ… Java code generation complete!"); +} + +main().catch((err) => { + console.error("❌ Code generation failed:", err); + process.exit(1); +}); diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json new file mode 100644 index 000000000..c1d610b39 --- /dev/null +++ b/java/scripts/codegen/package-lock.json @@ -0,0 +1,615 @@ +{ + "name": "copilot-sdk-java-codegen", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copilot-sdk-java-codegen", + "dependencies": { + "@github/copilot": "^1.0.49-1", + "json-schema": "^0.4.0", + "tsx": "^4.20.6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@github/copilot": { + "version": "1.0.49-3", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.49-3.tgz", + "integrity": "sha512-MoaUolkTfeDTeUpKjzLNZLyykdJmhW7xNEzSmM+zz9erZE0St1ACFz3TigJMM05r4L1cPR6j1TY37XfPExqdyg==", + "license": "SEE LICENSE IN LICENSE.md", + "bin": { + "copilot": "npm-loader.js" + }, + "optionalDependencies": { + "@github/copilot-darwin-arm64": "1.0.49-3", + "@github/copilot-darwin-x64": "1.0.49-3", + "@github/copilot-linux-arm64": "1.0.49-3", + "@github/copilot-linux-x64": "1.0.49-3", + "@github/copilot-linuxmusl-arm64": "1.0.49-3", + "@github/copilot-linuxmusl-x64": "1.0.49-3", + "@github/copilot-win32-arm64": "1.0.49-3", + "@github/copilot-win32-x64": "1.0.49-3" + } + }, + "node_modules/@github/copilot-darwin-arm64": { + "version": "1.0.49-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.49-3.tgz", + "integrity": "sha512-z6WpgoT+aro2nuA2zGfpxsMPtGSS3ZNACXERjfBxBzEoVjTMJi8kD1tpHFIPPCcLfaLniIi01Q6rvxMmZC6iKw==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-arm64": "copilot" + } + }, + "node_modules/@github/copilot-darwin-x64": { + "version": "1.0.49-3", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.49-3.tgz", + "integrity": "sha512-ox9zs0uaFroB5SujopKFMz6/1shs2JsI5eIx4Kb/gugDrwU+Y3VVJJLw+dbEElJjQOCsb33kD9n+MsV1T6dubA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ], + "bin": { + "copilot-darwin-x64": "copilot" + } + }, + "node_modules/@github/copilot-linux-arm64": { + "version": "1.0.49-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.49-3.tgz", + "integrity": "sha512-1uZaRtTH5H8HcPWKiN7eWJHsmmaW+tq6Eaxdme95Dfup4G9hemZMDHfdTjPXjZ6xykuoVKqWgC6knlk71JTWxQ==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-arm64": "copilot" + } + }, + "node_modules/@github/copilot-linux-x64": { + "version": "1.0.49-3", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.49-3.tgz", + "integrity": "sha512-OebfGDDFFn+KbiEbSHX8TvXRe77JeH1SBJyzle5QRSD/nBqNGEkNClRMGm8M5/cqyke6TbRP2XmmAQAApJmaQA==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "copilot-linux-x64": "copilot" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json new file mode 100644 index 000000000..8726bacc1 --- /dev/null +++ b/java/scripts/codegen/package.json @@ -0,0 +1,14 @@ +{ + "name": "copilot-sdk-java-codegen", + "private": true, + "type": "module", + "scripts": { + "generate": "tsx java.ts", + "generate:java": "tsx java.ts" + }, + "dependencies": { + "@github/copilot": "^1.0.49-1", + "json-schema": "^0.4.0", + "tsx": "^4.20.6" + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java new file mode 100644 index 000000000..297f23f72 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AbortEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "abort". Turn abort information including the reason for termination + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AbortEvent extends SessionEvent { + + @Override + public String getType() { return "abort"; } + + @JsonProperty("data") + private AbortEventData data; + + public AbortEventData getData() { return data; } + public void setData(AbortEventData data) { this.data = data; } + + /** Data payload for {@link AbortEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AbortEventData( + /** Finite reason code describing why the current turn was aborted */ + @JsonProperty("reason") AbortReason reason + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java b/java/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java new file mode 100644 index 000000000..2bb93b886 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AbortReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Finite reason code describing why the current turn was aborted + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AbortReason { + /** The {@code user_initiated} variant. */ + USER_INITIATED("user_initiated"), + /** The {@code remote_command} variant. */ + REMOTE_COMMAND("remote_command"), + /** The {@code user_abort} variant. */ + USER_ABORT("user_abort"); + + private final String value; + AbortReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AbortReason fromValue(String value) { + for (AbortReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AbortReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantIntentEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantIntentEvent.java new file mode 100644 index 000000000..332daeb17 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantIntentEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.intent". Agent intent description for current activity or plan + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantIntentEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.intent"; } + + @JsonProperty("data") + private AssistantIntentEventData data; + + public AssistantIntentEventData getData() { return data; } + public void setData(AssistantIntentEventData data) { this.data = data; } + + /** Data payload for {@link AssistantIntentEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantIntentEventData( + /** Short description of what the agent is currently doing or planning to do */ + @JsonProperty("intent") String intent + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageDeltaEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageDeltaEvent.java new file mode 100644 index 000000000..ff84f757d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageDeltaEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.message_delta". Streaming assistant message delta for incremental response updates + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageDeltaEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message_delta"; } + + @JsonProperty("data") + private AssistantMessageDeltaEventData data; + + public AssistantMessageDeltaEventData getData() { return data; } + public void setData(AssistantMessageDeltaEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageDeltaEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageDeltaEventData( + /** Message ID this delta belongs to, matching the corresponding assistant.message event */ + @JsonProperty("messageId") String messageId, + /** Incremental text chunk to append to the message content */ + @JsonProperty("deltaContent") String deltaContent, + /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java new file mode 100644 index 000000000..98af7b90a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageEvent.java @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.message". Assistant response containing text content, optional tool requests, and interaction metadata + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message"; } + + @JsonProperty("data") + private AssistantMessageEventData data; + + public AssistantMessageEventData getData() { return data; } + public void setData(AssistantMessageEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageEventData( + /** Unique identifier for this assistant message */ + @JsonProperty("messageId") String messageId, + /** Model that produced this assistant message, if known */ + @JsonProperty("model") String model, + /** The assistant's text response content */ + @JsonProperty("content") String content, + /** Tool invocations requested by the assistant in this message */ + @JsonProperty("toolRequests") List toolRequests, + /** Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. */ + @JsonProperty("reasoningOpaque") String reasoningOpaque, + /** Readable reasoning text from the model's extended thinking */ + @JsonProperty("reasoningText") String reasoningText, + /** Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. */ + @JsonProperty("encryptedContent") String encryptedContent, + /** Generation phase for phased-output models (e.g., thinking vs. response phases) */ + @JsonProperty("phase") String phase, + /** Actual output token count from the API response (completion_tokens), used for accurate token accounting */ + @JsonProperty("outputTokens") Double outputTokens, + /** CAPI interaction ID for correlating this message with upstream telemetry */ + @JsonProperty("interactionId") String interactionId, + /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ + @JsonProperty("requestId") String requestId, + /** Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping */ + @JsonProperty("anthropicAdvisorBlocks") List anthropicAdvisorBlocks, + /** Anthropic advisor model ID used for this response, for timeline display on replay */ + @JsonProperty("anthropicAdvisorModel") String anthropicAdvisorModel, + /** Identifier for the agent loop turn that produced this message, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, + /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java new file mode 100644 index 000000000..8a83da943 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageStartEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.message_start". Streaming assistant message start metadata + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantMessageStartEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.message_start"; } + + @JsonProperty("data") + private AssistantMessageStartEventData data; + + public AssistantMessageStartEventData getData() { return data; } + public void setData(AssistantMessageStartEventData data) { this.data = data; } + + /** Data payload for {@link AssistantMessageStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantMessageStartEventData( + /** Message ID this start event belongs to, matching subsequent deltas and assistant.message */ + @JsonProperty("messageId") String messageId, + /** Generation phase this message belongs to for phased-output models */ + @JsonProperty("phase") String phase + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java new file mode 100644 index 000000000..e185a01fa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequest.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * A tool invocation request from the assistant + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantMessageToolRequest( + /** Unique identifier for this tool call */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the tool being invoked */ + @JsonProperty("name") String name, + /** Arguments to pass to the tool, format depends on the tool */ + @JsonProperty("arguments") Object arguments, + /** Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ + @JsonProperty("type") AssistantMessageToolRequestType type, + /** Human-readable display title for the tool */ + @JsonProperty("toolTitle") String toolTitle, + /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ + @JsonProperty("mcpServerName") String mcpServerName, + /** Original tool name on the MCP server, when the tool is an MCP tool */ + @JsonProperty("mcpToolName") String mcpToolName, + /** Resolved intention summary describing what this specific call does */ + @JsonProperty("intentionSummary") String intentionSummary +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequestType.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequestType.java new file mode 100644 index 000000000..024b845d6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantMessageToolRequestType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AssistantMessageToolRequestType { + /** The {@code function} variant. */ + FUNCTION("function"), + /** The {@code custom} variant. */ + CUSTOM("custom"); + + private final String value; + AssistantMessageToolRequestType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AssistantMessageToolRequestType fromValue(String value) { + for (AssistantMessageToolRequestType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AssistantMessageToolRequestType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningDeltaEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningDeltaEvent.java new file mode 100644 index 000000000..5c7a6f94b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningDeltaEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.reasoning_delta". Streaming reasoning delta for incremental extended thinking updates + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantReasoningDeltaEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.reasoning_delta"; } + + @JsonProperty("data") + private AssistantReasoningDeltaEventData data; + + public AssistantReasoningDeltaEventData getData() { return data; } + public void setData(AssistantReasoningDeltaEventData data) { this.data = data; } + + /** Data payload for {@link AssistantReasoningDeltaEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantReasoningDeltaEventData( + /** Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event */ + @JsonProperty("reasoningId") String reasoningId, + /** Incremental text chunk to append to the reasoning content */ + @JsonProperty("deltaContent") String deltaContent + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningEvent.java new file mode 100644 index 000000000..58a7e665d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantReasoningEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.reasoning". Assistant reasoning content for timeline display with complete thinking text + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantReasoningEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.reasoning"; } + + @JsonProperty("data") + private AssistantReasoningEventData data; + + public AssistantReasoningEventData getData() { return data; } + public void setData(AssistantReasoningEventData data) { this.data = data; } + + /** Data payload for {@link AssistantReasoningEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantReasoningEventData( + /** Unique identifier for this reasoning block */ + @JsonProperty("reasoningId") String reasoningId, + /** The complete extended thinking text from the model */ + @JsonProperty("content") String content + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java new file mode 100644 index 000000000..70707a56e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantStreamingDeltaEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.streaming_delta". Streaming response progress with cumulative byte count + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantStreamingDeltaEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.streaming_delta"; } + + @JsonProperty("data") + private AssistantStreamingDeltaEventData data; + + public AssistantStreamingDeltaEventData getData() { return data; } + public void setData(AssistantStreamingDeltaEventData data) { this.data = data; } + + /** Data payload for {@link AssistantStreamingDeltaEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantStreamingDeltaEventData( + /** Cumulative total bytes received from the streaming response so far */ + @JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnEndEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnEndEvent.java new file mode 100644 index 000000000..e349711dc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnEndEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.turn_end". Turn completion metadata including the turn identifier + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantTurnEndEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.turn_end"; } + + @JsonProperty("data") + private AssistantTurnEndEventData data; + + public AssistantTurnEndEventData getData() { return data; } + public void setData(AssistantTurnEndEventData data) { this.data = data; } + + /** Data payload for {@link AssistantTurnEndEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantTurnEndEventData( + /** Identifier of the turn that has ended, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnStartEvent.java new file mode 100644 index 000000000..245803774 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantTurnStartEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.turn_start". Turn initialization metadata including identifier and interaction tracking + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantTurnStartEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.turn_start"; } + + @JsonProperty("data") + private AssistantTurnStartEventData data; + + public AssistantTurnStartEventData getData() { return data; } + public void setData(AssistantTurnStartEventData data) { this.data = data; } + + /** Data payload for {@link AssistantTurnStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantTurnStartEventData( + /** Identifier for this turn within the agentic loop, typically a stringified turn number */ + @JsonProperty("turnId") String turnId, + /** CAPI interaction ID for correlating this turn with upstream telemetry */ + @JsonProperty("interactionId") String interactionId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java new file mode 100644 index 000000000..e69e4ef86 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageApiEndpoint.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AssistantUsageApiEndpoint { + /** The {@code /chat/completions} variant. */ + CHAT_COMPLETIONS("/chat/completions"), + /** The {@code /v1/messages} variant. */ + V1_MESSAGES("/v1/messages"), + /** The {@code /responses} variant. */ + RESPONSES("/responses"), + /** The {@code ws:/responses} variant. */ + WS_RESPONSES("ws:/responses"); + + private final String value; + AssistantUsageApiEndpoint(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AssistantUsageApiEndpoint fromValue(String value) { + for (AssistantUsageApiEndpoint v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AssistantUsageApiEndpoint value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java new file mode 100644 index 000000000..ee3e9f9cf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsage.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Per-request cost and usage data from the CAPI copilot_usage response field + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantUsageCopilotUsage( + /** Itemized token usage breakdown */ + @JsonProperty("tokenDetails") List tokenDetails, + /** Total cost in nano-AI units for this request */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java new file mode 100644 index 000000000..895f19030 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageCopilotUsageTokenDetail.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage detail for a single billing category + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantUsageCopilotUsageTokenDetail( + /** Number of tokens in this billing batch */ + @JsonProperty("batchSize") Double batchSize, + /** Cost per batch of tokens */ + @JsonProperty("costPerBatch") Double costPerBatch, + /** Total token count for this entry */ + @JsonProperty("tokenCount") Double tokenCount, + /** Token category (e.g., "input", "output") */ + @JsonProperty("tokenType") String tokenType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java new file mode 100644 index 000000000..d704a862c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageEvent.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.usage". LLM API call usage metrics including tokens, costs, quotas, and billing information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantUsageEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.usage"; } + + @JsonProperty("data") + private AssistantUsageEventData data; + + public AssistantUsageEventData getData() { return data; } + public void setData(AssistantUsageEventData data) { this.data = data; } + + /** Data payload for {@link AssistantUsageEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantUsageEventData( + /** Model identifier used for this API call */ + @JsonProperty("model") String model, + /** Number of input tokens consumed */ + @JsonProperty("inputTokens") Double inputTokens, + /** Number of output tokens produced */ + @JsonProperty("outputTokens") Double outputTokens, + /** Number of tokens read from prompt cache */ + @JsonProperty("cacheReadTokens") Double cacheReadTokens, + /** Number of tokens written to prompt cache */ + @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + /** Number of output tokens used for reasoning (e.g., chain-of-thought) */ + @JsonProperty("reasoningTokens") Double reasoningTokens, + /** Model multiplier cost for billing purposes */ + @JsonProperty("cost") Double cost, + /** Duration of the API call in milliseconds */ + @JsonProperty("duration") Double duration, + /** Time to first token in milliseconds. Only available for streaming requests */ + @JsonProperty("ttftMs") Double ttftMs, + /** Average inter-token latency in milliseconds. Only available for streaming requests */ + @JsonProperty("interTokenLatencyMs") Double interTokenLatencyMs, + /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ + @JsonProperty("initiator") String initiator, + /** Completion ID from the model provider (e.g., chatcmpl-abc123) */ + @JsonProperty("apiCallId") String apiCallId, + /** GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ + @JsonProperty("providerCallId") String providerCallId, + /** API endpoint used for this model call, matching CAPI supported_endpoints vocabulary */ + @JsonProperty("apiEndpoint") AssistantUsageApiEndpoint apiEndpoint, + /** Parent tool call ID when this usage originates from a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId, + /** Per-quota resource usage snapshots, keyed by quota identifier */ + @JsonProperty("quotaSnapshots") Map quotaSnapshots, + /** Per-request cost and usage data from the CAPI copilot_usage response field */ + @JsonProperty("copilotUsage") AssistantUsageCopilotUsage copilotUsage, + /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ + @JsonProperty("reasoningEffort") String reasoningEffort + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java new file mode 100644 index 000000000..9e5675360 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AssistantUsageQuotaSnapshot.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Schema for the `AssistantUsageQuotaSnapshot` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AssistantUsageQuotaSnapshot( + /** Whether the user has an unlimited usage entitlement */ + @JsonProperty("isUnlimitedEntitlement") Boolean isUnlimitedEntitlement, + /** Total requests allowed by the entitlement */ + @JsonProperty("entitlementRequests") Double entitlementRequests, + /** Number of requests already consumed */ + @JsonProperty("usedRequests") Double usedRequests, + /** Whether usage is still permitted after quota exhaustion */ + @JsonProperty("usageAllowedWithExhaustedQuota") Boolean usageAllowedWithExhaustedQuota, + /** Number of requests over the entitlement limit */ + @JsonProperty("overage") Double overage, + /** Whether overage is allowed when quota is exhausted */ + @JsonProperty("overageAllowedWithExhaustedQuota") Boolean overageAllowedWithExhaustedQuota, + /** Percentage of quota remaining (0.0 to 1.0) */ + @JsonProperty("remainingPercentage") Double remainingPercentage, + /** Date when the quota resets */ + @JsonProperty("resetDate") OffsetDateTime resetDate +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchCompletedEvent.java new file mode 100644 index 000000000..09c15cc1a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchCompletedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "auto_mode_switch.completed". Auto mode switch completion notification + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AutoModeSwitchCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "auto_mode_switch.completed"; } + + @JsonProperty("data") + private AutoModeSwitchCompletedEventData data; + + public AutoModeSwitchCompletedEventData getData() { return data; } + public void setData(AutoModeSwitchCompletedEventData data) { this.data = data; } + + /** Data payload for {@link AutoModeSwitchCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AutoModeSwitchCompletedEventData( + /** Request ID of the resolved request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId, + /** The user's auto-mode-switch choice */ + @JsonProperty("response") AutoModeSwitchResponse response + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java new file mode 100644 index 000000000..b1a768adc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchRequestedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "auto_mode_switch.requested". Auto mode switch request notification requiring user approval + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AutoModeSwitchRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "auto_mode_switch.requested"; } + + @JsonProperty("data") + private AutoModeSwitchRequestedEventData data; + + public AutoModeSwitchRequestedEventData getData() { return data; } + public void setData(AutoModeSwitchRequestedEventData data) { this.data = data; } + + /** Data payload for {@link AutoModeSwitchRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AutoModeSwitchRequestedEventData( + /** Unique identifier for this request; used to respond via session.respondToAutoModeSwitch() */ + @JsonProperty("requestId") String requestId, + /** The rate limit error code that triggered this request */ + @JsonProperty("errorCode") String errorCode, + /** Seconds until the rate limit resets, when known. Lets clients render a humanized reset time alongside the prompt. */ + @JsonProperty("retryAfterSeconds") Double retryAfterSeconds + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchResponse.java new file mode 100644 index 000000000..3f677ca20 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/AutoModeSwitchResponse.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The user's auto-mode-switch choice + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoModeSwitchResponse { + /** The {@code yes} variant. */ + YES("yes"), + /** The {@code yes_always} variant. */ + YES_ALWAYS("yes_always"), + /** The {@code no} variant. */ + NO("no"); + + private final String value; + AutoModeSwitchResponse(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoModeSwitchResponse fromValue(String value) { + for (AutoModeSwitchResponse v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoModeSwitchResponse value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedEvent.java new file mode 100644 index 000000000..ff359b04a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "capabilities.changed". Session capability change notification + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CapabilitiesChangedEvent extends SessionEvent { + + @Override + public String getType() { return "capabilities.changed"; } + + @JsonProperty("data") + private CapabilitiesChangedEventData data; + + public CapabilitiesChangedEventData getData() { return data; } + public void setData(CapabilitiesChangedEventData data) { this.data = data; } + + /** Data payload for {@link CapabilitiesChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record CapabilitiesChangedEventData( + /** UI capability changes */ + @JsonProperty("ui") CapabilitiesChangedUI ui + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedUI.java b/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedUI.java new file mode 100644 index 000000000..828733b04 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CapabilitiesChangedUI.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * UI capability changes + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CapabilitiesChangedUI( + /** Whether elicitation is now supported */ + @JsonProperty("elicitation") Boolean elicitation +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CommandCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CommandCompletedEvent.java new file mode 100644 index 000000000..584cde38b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CommandCompletedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "command.completed". Queued command completion notification signaling UI dismissal + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CommandCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "command.completed"; } + + @JsonProperty("data") + private CommandCompletedEventData data; + + public CommandCompletedEventData getData() { return data; } + public void setData(CommandCompletedEventData data) { this.data = data; } + + /** Data payload for {@link CommandCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record CommandCompletedEventData( + /** Request ID of the resolved command request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CommandExecuteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CommandExecuteEvent.java new file mode 100644 index 000000000..5d07300e8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CommandExecuteEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "command.execute". Registered command dispatch request routed to the owning client + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CommandExecuteEvent extends SessionEvent { + + @Override + public String getType() { return "command.execute"; } + + @JsonProperty("data") + private CommandExecuteEventData data; + + public CommandExecuteEventData getData() { return data; } + public void setData(CommandExecuteEventData data) { this.data = data; } + + /** Data payload for {@link CommandExecuteEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record CommandExecuteEventData( + /** Unique identifier; used to respond via session.commands.handlePendingCommand() */ + @JsonProperty("requestId") String requestId, + /** The full command text (e.g., /deploy production) */ + @JsonProperty("command") String command, + /** Command name without leading / */ + @JsonProperty("commandName") String commandName, + /** Raw argument string after the command name */ + @JsonProperty("args") String args + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CommandQueuedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CommandQueuedEvent.java new file mode 100644 index 000000000..f724f5299 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CommandQueuedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "command.queued". Queued slash command dispatch request for client execution + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CommandQueuedEvent extends SessionEvent { + + @Override + public String getType() { return "command.queued"; } + + @JsonProperty("data") + private CommandQueuedEventData data; + + public CommandQueuedEventData getData() { return data; } + public void setData(CommandQueuedEventData data) { this.data = data; } + + /** Data payload for {@link CommandQueuedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record CommandQueuedEventData( + /** Unique identifier for this request; used to respond via session.respondToQueuedCommand() */ + @JsonProperty("requestId") String requestId, + /** The slash command text to be executed (e.g., /help, /clear) */ + @JsonProperty("command") String command + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedCommand.java b/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedCommand.java new file mode 100644 index 000000000..cb446ee24 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedCommand.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `CommandsChangedCommand` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CommandsChangedCommand( + /** Slash command name without the leading slash. */ + @JsonProperty("name") String name, + /** Optional human-readable command description. */ + @JsonProperty("description") String description +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedEvent.java new file mode 100644 index 000000000..7b2d3c2c1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CommandsChangedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "commands.changed". SDK command registration change notification + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CommandsChangedEvent extends SessionEvent { + + @Override + public String getType() { return "commands.changed"; } + + @JsonProperty("data") + private CommandsChangedEventData data; + + public CommandsChangedEventData getData() { return data; } + public void setData(CommandsChangedEventData data) { this.data = data; } + + /** Data payload for {@link CommandsChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record CommandsChangedEventData( + /** Current list of registered SDK commands */ + @JsonProperty("commands") List commands + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java new file mode 100644 index 000000000..440979392 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsed.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompactionCompleteCompactionTokensUsed( + /** Input tokens consumed by the compaction LLM call */ + @JsonProperty("inputTokens") Double inputTokens, + /** Output tokens produced by the compaction LLM call */ + @JsonProperty("outputTokens") Double outputTokens, + /** Cached input tokens reused in the compaction LLM call */ + @JsonProperty("cacheReadTokens") Double cacheReadTokens, + /** Tokens written to prompt cache in the compaction LLM call */ + @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + /** Per-request cost and usage data from the CAPI copilot_usage response field */ + @JsonProperty("copilotUsage") CompactionCompleteCompactionTokensUsedCopilotUsage copilotUsage, + /** Duration of the compaction LLM call in milliseconds */ + @JsonProperty("duration") Double duration, + /** Model identifier used for the compaction LLM call */ + @JsonProperty("model") String model +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java new file mode 100644 index 000000000..76e5a0ed8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsage.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Per-request cost and usage data from the CAPI copilot_usage response field + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompactionCompleteCompactionTokensUsedCopilotUsage( + /** Itemized token usage breakdown */ + @JsonProperty("tokenDetails") List tokenDetails, + /** Total cost in nano-AI units for this request */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java new file mode 100644 index 000000000..3c7fde8ad --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage detail for a single billing category + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail( + /** Number of tokens in this billing batch */ + @JsonProperty("batchSize") Double batchSize, + /** Cost per batch of tokens */ + @JsonProperty("costPerBatch") Double costPerBatch, + /** Total token count for this entry */ + @JsonProperty("tokenCount") Double tokenCount, + /** Token category (e.g., "input", "output") */ + @JsonProperty("tokenType") String tokenType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java b/java/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java new file mode 100644 index 000000000..154e76377 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/CustomAgentsUpdatedAgent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `CustomAgentsUpdatedAgent` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CustomAgentsUpdatedAgent( + /** Unique identifier for the agent */ + @JsonProperty("id") String id, + /** Internal name of the agent */ + @JsonProperty("name") String name, + /** Human-readable display name */ + @JsonProperty("displayName") String displayName, + /** Description of what the agent does */ + @JsonProperty("description") String description, + /** Source location: user, project, inherited, remote, or plugin */ + @JsonProperty("source") String source, + /** List of tool names available to this agent, or null when all tools are available */ + @JsonProperty("tools") List tools, + /** Whether the agent can be selected by the user */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Model override for this agent, if set */ + @JsonProperty("model") String model +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedAction.java b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedAction.java new file mode 100644 index 000000000..32e4723e5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedAction.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ElicitationCompletedAction { + /** The {@code accept} variant. */ + ACCEPT("accept"), + /** The {@code decline} variant. */ + DECLINE("decline"), + /** The {@code cancel} variant. */ + CANCEL("cancel"); + + private final String value; + ElicitationCompletedAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ElicitationCompletedAction fromValue(String value) { + for (ElicitationCompletedAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ElicitationCompletedAction value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedEvent.java new file mode 100644 index 000000000..ee713a100 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationCompletedEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "elicitation.completed". Elicitation request completion with the user's response + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ElicitationCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "elicitation.completed"; } + + @JsonProperty("data") + private ElicitationCompletedEventData data; + + public ElicitationCompletedEventData getData() { return data; } + public void setData(ElicitationCompletedEventData data) { this.data = data; } + + /** Data payload for {@link ElicitationCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ElicitationCompletedEventData( + /** Request ID of the resolved elicitation request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId, + /** The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) */ + @JsonProperty("action") ElicitationCompletedAction action, + /** The submitted form data when action is 'accept'; keys match the requested schema fields */ + @JsonProperty("content") Map content + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedEvent.java new file mode 100644 index 000000000..000c242c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "elicitation.requested". Elicitation request; may be form-based (structured input) or URL-based (browser redirect) + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ElicitationRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "elicitation.requested"; } + + @JsonProperty("data") + private ElicitationRequestedEventData data; + + public ElicitationRequestedEventData getData() { return data; } + public void setData(ElicitationRequestedEventData data) { this.data = data; } + + /** Data payload for {@link ElicitationRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ElicitationRequestedEventData( + /** Unique identifier for this elicitation request; used to respond via session.respondToElicitation() */ + @JsonProperty("requestId") String requestId, + /** Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs */ + @JsonProperty("toolCallId") String toolCallId, + /** The source that initiated the request (MCP server name, or absent for agent-initiated) */ + @JsonProperty("elicitationSource") String elicitationSource, + /** Message describing what information is needed from the user */ + @JsonProperty("message") String message, + /** Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. */ + @JsonProperty("mode") ElicitationRequestedMode mode, + /** JSON Schema describing the form fields to present to the user (form mode only) */ + @JsonProperty("requestedSchema") ElicitationRequestedSchema requestedSchema, + /** URL to open in the user's browser (url mode only) */ + @JsonProperty("url") String url + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedMode.java new file mode 100644 index 000000000..ffe24b56f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ElicitationRequestedMode { + /** The {@code form} variant. */ + FORM("form"), + /** The {@code url} variant. */ + URL("url"); + + private final String value; + ElicitationRequestedMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ElicitationRequestedMode fromValue(String value) { + for (ElicitationRequestedMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ElicitationRequestedMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedSchema.java b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedSchema.java new file mode 100644 index 000000000..4234867ad --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ElicitationRequestedSchema.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * JSON Schema describing the form fields to present to the user (form mode only) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ElicitationRequestedSchema( + /** Schema type indicator (always 'object') */ + @JsonProperty("type") String type, + /** Form field definitions, keyed by field name */ + @JsonProperty("properties") Map properties, + /** List of required field names */ + @JsonProperty("required") List required +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeAction.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeAction.java new file mode 100644 index 000000000..a8b85ad94 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeAction.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Exit plan mode action + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ExitPlanModeAction { + /** The {@code exit_only} variant. */ + EXIT_ONLY("exit_only"), + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"), + /** The {@code autopilot_fleet} variant. */ + AUTOPILOT_FLEET("autopilot_fleet"); + + private final String value; + ExitPlanModeAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ExitPlanModeAction fromValue(String value) { + for (ExitPlanModeAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ExitPlanModeAction value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeCompletedEvent.java new file mode 100644 index 000000000..378853293 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeCompletedEvent.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "exit_plan_mode.completed". Plan mode exit completion with the user's approval decision and optional feedback + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ExitPlanModeCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "exit_plan_mode.completed"; } + + @JsonProperty("data") + private ExitPlanModeCompletedEventData data; + + public ExitPlanModeCompletedEventData getData() { return data; } + public void setData(ExitPlanModeCompletedEventData data) { this.data = data; } + + /** Data payload for {@link ExitPlanModeCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ExitPlanModeCompletedEventData( + /** Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId, + /** Whether the plan was approved by the user */ + @JsonProperty("approved") Boolean approved, + /** Action selected by the user */ + @JsonProperty("selectedAction") ExitPlanModeAction selectedAction, + /** Whether edits should be auto-approved without confirmation */ + @JsonProperty("autoApproveEdits") Boolean autoApproveEdits, + /** Free-form feedback from the user if they requested changes to the plan */ + @JsonProperty("feedback") String feedback + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeRequestedEvent.java new file mode 100644 index 000000000..e96124bc7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExitPlanModeRequestedEvent.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "exit_plan_mode.requested". Plan approval request with plan content and available user actions + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ExitPlanModeRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "exit_plan_mode.requested"; } + + @JsonProperty("data") + private ExitPlanModeRequestedEventData data; + + public ExitPlanModeRequestedEventData getData() { return data; } + public void setData(ExitPlanModeRequestedEventData data) { this.data = data; } + + /** Data payload for {@link ExitPlanModeRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ExitPlanModeRequestedEventData( + /** Unique identifier for this request; used to respond via session.respondToExitPlanMode() */ + @JsonProperty("requestId") String requestId, + /** Summary of the plan that was created */ + @JsonProperty("summary") String summary, + /** Full content of the plan file */ + @JsonProperty("planContent") String planContent, + /** Available actions the user can take */ + @JsonProperty("actions") List actions, + /** Recommended action to preselect for the user */ + @JsonProperty("recommendedAction") ExitPlanModeAction recommendedAction + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtension.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtension.java new file mode 100644 index 000000000..32e8ae460 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtension.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ExtensionsLoadedExtension` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ExtensionsLoadedExtension( + /** Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper') */ + @JsonProperty("id") String id, + /** Extension name (directory name) */ + @JsonProperty("name") String name, + /** Discovery source */ + @JsonProperty("source") ExtensionsLoadedExtensionSource source, + /** Current status: running, disabled, failed, or starting */ + @JsonProperty("status") ExtensionsLoadedExtensionStatus status +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionSource.java new file mode 100644 index 000000000..d6409caf4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionSource.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Discovery source + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ExtensionsLoadedExtensionSource { + /** The {@code project} variant. */ + PROJECT("project"), + /** The {@code user} variant. */ + USER("user"); + + private final String value; + ExtensionsLoadedExtensionSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ExtensionsLoadedExtensionSource fromValue(String value) { + for (ExtensionsLoadedExtensionSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ExtensionsLoadedExtensionSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionStatus.java new file mode 100644 index 000000000..a4ef8de99 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExtensionsLoadedExtensionStatus.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Current status: running, disabled, failed, or starting + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ExtensionsLoadedExtensionStatus { + /** The {@code running} variant. */ + RUNNING("running"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code starting} variant. */ + STARTING("starting"); + + private final String value; + ExtensionsLoadedExtensionStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ExtensionsLoadedExtensionStatus fromValue(String value) { + for (ExtensionsLoadedExtensionStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ExtensionsLoadedExtensionStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolCompletedEvent.java new file mode 100644 index 000000000..be086cb6d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolCompletedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "external_tool.completed". External tool completion notification signaling UI dismissal + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ExternalToolCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "external_tool.completed"; } + + @JsonProperty("data") + private ExternalToolCompletedEventData data; + + public ExternalToolCompletedEventData getData() { return data; } + public void setData(ExternalToolCompletedEventData data) { this.data = data; } + + /** Data payload for {@link ExternalToolCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ExternalToolCompletedEventData( + /** Request ID of the resolved external tool request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolRequestedEvent.java new file mode 100644 index 000000000..72591dd47 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ExternalToolRequestedEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "external_tool.requested". External tool invocation request for client-side tool execution + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ExternalToolRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "external_tool.requested"; } + + @JsonProperty("data") + private ExternalToolRequestedEventData data; + + public ExternalToolRequestedEventData getData() { return data; } + public void setData(ExternalToolRequestedEventData data) { this.data = data; } + + /** Data payload for {@link ExternalToolRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ExternalToolRequestedEventData( + /** Unique identifier for this request; used to respond via session.respondToExternalTool() */ + @JsonProperty("requestId") String requestId, + /** Session ID that this external tool request belongs to */ + @JsonProperty("sessionId") String sessionId, + /** Tool call ID assigned to this external tool invocation */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the external tool to invoke */ + @JsonProperty("toolName") String toolName, + /** Arguments to pass to the external tool */ + @JsonProperty("arguments") Object arguments, + /** W3C Trace Context traceparent header for the execute_tool span */ + @JsonProperty("traceparent") String traceparent, + /** W3C Trace Context tracestate header for the execute_tool span */ + @JsonProperty("tracestate") String tracestate + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/HandoffRepository.java b/java/src/generated/java/com/github/copilot/sdk/generated/HandoffRepository.java new file mode 100644 index 000000000..a87002c9e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/HandoffRepository.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Repository context for the handed-off session + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HandoffRepository( + /** Repository owner (user or organization) */ + @JsonProperty("owner") String owner, + /** Repository name */ + @JsonProperty("name") String name, + /** Git branch name, if applicable */ + @JsonProperty("branch") String branch +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/HandoffSourceType.java b/java/src/generated/java/com/github/copilot/sdk/generated/HandoffSourceType.java new file mode 100644 index 000000000..06f39c214 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/HandoffSourceType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Origin type of the session being handed off + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HandoffSourceType { + /** The {@code remote} variant. */ + REMOTE("remote"), + /** The {@code local} variant. */ + LOCAL("local"); + + private final String value; + HandoffSourceType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HandoffSourceType fromValue(String value) { + for (HandoffSourceType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HandoffSourceType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/HookEndError.java b/java/src/generated/java/com/github/copilot/sdk/generated/HookEndError.java new file mode 100644 index 000000000..bbf992536 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/HookEndError.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Error details when the hook failed + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HookEndError( + /** Human-readable error message */ + @JsonProperty("message") String message, + /** Error stack trace, when available */ + @JsonProperty("stack") String stack +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/HookEndEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/HookEndEvent.java new file mode 100644 index 000000000..71b148fb1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/HookEndEvent.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "hook.end". Hook invocation completion details including output, success status, and error information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HookEndEvent extends SessionEvent { + + @Override + public String getType() { return "hook.end"; } + + @JsonProperty("data") + private HookEndEventData data; + + public HookEndEventData getData() { return data; } + public void setData(HookEndEventData data) { this.data = data; } + + /** Data payload for {@link HookEndEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record HookEndEventData( + /** Identifier matching the corresponding hook.start event */ + @JsonProperty("hookInvocationId") String hookInvocationId, + /** Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart") */ + @JsonProperty("hookType") String hookType, + /** Output data produced by the hook */ + @JsonProperty("output") Object output, + /** Whether the hook completed successfully */ + @JsonProperty("success") Boolean success, + /** Error details when the hook failed */ + @JsonProperty("error") HookEndError error + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/HookStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/HookStartEvent.java new file mode 100644 index 000000000..0505c4182 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/HookStartEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "hook.start". Hook invocation start details including type and input data + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HookStartEvent extends SessionEvent { + + @Override + public String getType() { return "hook.start"; } + + @JsonProperty("data") + private HookStartEventData data; + + public HookStartEventData getData() { return data; } + public void setData(HookStartEventData data) { this.data = data; } + + /** Data payload for {@link HookStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record HookStartEventData( + /** Unique identifier for this hook invocation */ + @JsonProperty("hookInvocationId") String hookInvocationId, + /** Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") */ + @JsonProperty("hookType") String hookType, + /** Input data passed to the hook */ + @JsonProperty("input") Object input + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthCompletedEvent.java new file mode 100644 index 000000000..33a56f824 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthCompletedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.oauth_completed". MCP OAuth request completion notification + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpOauthCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.oauth_completed"; } + + @JsonProperty("data") + private McpOauthCompletedEventData data; + + public McpOauthCompletedEventData getData() { return data; } + public void setData(McpOauthCompletedEventData data) { this.data = data; } + + /** Data payload for {@link McpOauthCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpOauthCompletedEventData( + /** Request ID of the resolved OAuth request */ + @JsonProperty("requestId") String requestId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredEvent.java new file mode 100644 index 000000000..c2e9843da --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.oauth_required". OAuth authentication request for an MCP server + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpOauthRequiredEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.oauth_required"; } + + @JsonProperty("data") + private McpOauthRequiredEventData data; + + public McpOauthRequiredEventData getData() { return data; } + public void setData(McpOauthRequiredEventData data) { this.data = data; } + + /** Data payload for {@link McpOauthRequiredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpOauthRequiredEventData( + /** Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth() */ + @JsonProperty("requestId") String requestId, + /** Display name of the MCP server that requires OAuth */ + @JsonProperty("serverName") String serverName, + /** URL of the MCP server that requires OAuth */ + @JsonProperty("serverUrl") String serverUrl, + /** Static OAuth client configuration, if the server specifies one */ + @JsonProperty("staticClientConfig") McpOauthRequiredStaticClientConfig staticClientConfig + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java new file mode 100644 index 000000000..b037b82ac --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpOauthRequiredStaticClientConfig.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Static OAuth client configuration, if the server specifies one + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpOauthRequiredStaticClientConfig( + /** OAuth client ID for the server */ + @JsonProperty("clientId") String clientId, + /** Whether this is a public OAuth client */ + @JsonProperty("publicClient") Boolean publicClient, + /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ + @JsonProperty("grantType") String grantType +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpServerSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerSource.java new file mode 100644 index 000000000..15cff507a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Configuration source: user, workspace, plugin, or builtin + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServerSource { + /** The {@code user} variant. */ + USER("user"), + /** The {@code workspace} variant. */ + WORKSPACE("workspace"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + McpServerSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServerSource fromValue(String value) { + for (McpServerSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServerSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatus.java new file mode 100644 index 000000000..f9c948f10 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Connection status: connected, failed, needs-auth, pending, disabled, or not_configured + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServerStatus { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code needs-auth} variant. */ + NEEDS_AUTH("needs-auth"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code not_configured} variant. */ + NOT_CONFIGURED("not_configured"); + + private final String value; + McpServerStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServerStatus fromValue(String value) { + for (McpServerStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServerStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatusChangedStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatusChangedStatus.java new file mode 100644 index 000000000..c0a6d989d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpServerStatusChangedStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * New connection status: connected, failed, needs-auth, pending, disabled, or not_configured + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServerStatusChangedStatus { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code needs-auth} variant. */ + NEEDS_AUTH("needs-auth"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code not_configured} variant. */ + NOT_CONFIGURED("not_configured"); + + private final String value; + McpServerStatusChangedStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServerStatusChangedStatus fromValue(String value) { + for (McpServerStatusChangedStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServerStatusChangedStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServer.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServer.java new file mode 100644 index 000000000..dbd08ede7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServer.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `McpServersLoadedServer` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServersLoadedServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Connection status: connected, failed, needs-auth, pending, disabled, or not_configured */ + @JsonProperty("status") McpServerStatus status, + /** Configuration source: user, workspace, plugin, or builtin */ + @JsonProperty("source") McpServerSource source, + /** Error message if the server failed to connect */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServerStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServerStatus.java new file mode 100644 index 000000000..4d09fe2a3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/McpServersLoadedServerStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Connection status: connected, failed, needs-auth, pending, disabled, or not_configured + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServersLoadedServerStatus { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code needs-auth} variant. */ + NEEDS_AUTH("needs-auth"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code not_configured} variant. */ + NOT_CONFIGURED("not_configured"); + + private final String value; + McpServersLoadedServerStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServersLoadedServerStatus fromValue(String value) { + for (McpServersLoadedServerStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServersLoadedServerStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java new file mode 100644 index 000000000..33a3f8618 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "model.call_failure". Failed LLM API call metadata for telemetry + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ModelCallFailureEvent extends SessionEvent { + + @Override + public String getType() { return "model.call_failure"; } + + @JsonProperty("data") + private ModelCallFailureEventData data; + + public ModelCallFailureEventData getData() { return data; } + public void setData(ModelCallFailureEventData data) { this.data = data; } + + /** Data payload for {@link ModelCallFailureEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ModelCallFailureEventData( + /** Model identifier used for the failed API call */ + @JsonProperty("model") String model, + /** What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls */ + @JsonProperty("initiator") String initiator, + /** Completion ID from the model provider (e.g., chatcmpl-abc123) */ + @JsonProperty("apiCallId") String apiCallId, + /** GitHub request tracing ID (x-github-request-id header) for server-side log correlation */ + @JsonProperty("providerCallId") String providerCallId, + /** HTTP status code from the failed request */ + @JsonProperty("statusCode") Long statusCode, + /** Duration of the failed API call in milliseconds */ + @JsonProperty("durationMs") Double durationMs, + /** Where the failed model call originated */ + @JsonProperty("source") ModelCallFailureSource source, + /** Raw provider/runtime error message for restricted telemetry */ + @JsonProperty("errorMessage") String errorMessage + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java new file mode 100644 index 000000000..469adaab4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ModelCallFailureSource.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Where the failed model call originated + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelCallFailureSource { + /** The {@code top_level} variant. */ + TOP_LEVEL("top_level"), + /** The {@code subagent} variant. */ + SUBAGENT("subagent"), + /** The {@code mcp_sampling} variant. */ + MCP_SAMPLING("mcp_sampling"); + + private final String value; + ModelCallFailureSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelCallFailureSource fromValue(String value) { + for (ModelCallFailureSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelCallFailureSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PendingMessagesModifiedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/PendingMessagesModifiedEvent.java new file mode 100644 index 000000000..7cdcf1d3a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PendingMessagesModifiedEvent.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "pending_messages.modified". Empty payload; the event signals that the pending message queue has changed + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class PendingMessagesModifiedEvent extends SessionEvent { + + @Override + public String getType() { return "pending_messages.modified"; } + + @JsonProperty("data") + private PendingMessagesModifiedEventData data; + + public PendingMessagesModifiedEventData getData() { return data; } + public void setData(PendingMessagesModifiedEventData data) { this.data = data; } + + /** Data payload for {@link PendingMessagesModifiedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record PendingMessagesModifiedEventData() { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java new file mode 100644 index 000000000..feeb4ca82 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "permission.completed". Permission request completion notification signaling UI dismissal + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class PermissionCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "permission.completed"; } + + @JsonProperty("data") + private PermissionCompletedEventData data; + + public PermissionCompletedEventData getData() { return data; } + public void setData(PermissionCompletedEventData data) { this.data = data; } + + /** Data payload for {@link PermissionCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record PermissionCompletedEventData( + /** Request ID of the resolved permission request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId, + /** Optional tool call ID associated with this permission prompt; clients may use it to correlate UI created from tool-scoped prompts */ + @JsonProperty("toolCallId") String toolCallId, + /** The result of the permission request */ + @JsonProperty("result") Object result + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedKind.java b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedKind.java new file mode 100644 index 000000000..c02f221fd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedKind.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The outcome of the permission request + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PermissionCompletedKind { + /** The {@code approved} variant. */ + APPROVED("approved"), + /** The {@code approved-for-session} variant. */ + APPROVED_FOR_SESSION("approved-for-session"), + /** The {@code approved-for-location} variant. */ + APPROVED_FOR_LOCATION("approved-for-location"), + /** The {@code denied-by-rules} variant. */ + DENIED_BY_RULES("denied-by-rules"), + /** The {@code denied-no-approval-rule-and-could-not-request-from-user} variant. */ + DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER("denied-no-approval-rule-and-could-not-request-from-user"), + /** The {@code denied-interactively-by-user} variant. */ + DENIED_INTERACTIVELY_BY_USER("denied-interactively-by-user"), + /** The {@code denied-by-content-exclusion-policy} variant. */ + DENIED_BY_CONTENT_EXCLUSION_POLICY("denied-by-content-exclusion-policy"), + /** The {@code denied-by-permission-request-hook} variant. */ + DENIED_BY_PERMISSION_REQUEST_HOOK("denied-by-permission-request-hook"); + + private final String value; + PermissionCompletedKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PermissionCompletedKind fromValue(String value) { + for (PermissionCompletedKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PermissionCompletedKind value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedResult.java new file mode 100644 index 000000000..4a180001c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionCompletedResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The result of the permission request + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PermissionCompletedResult( + /** The outcome of the permission request */ + @JsonProperty("kind") PermissionCompletedKind kind +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PermissionRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionRequestedEvent.java new file mode 100644 index 000000000..fda4db42f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PermissionRequestedEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "permission.requested". Permission request notification requiring client approval with request details + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class PermissionRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "permission.requested"; } + + @JsonProperty("data") + private PermissionRequestedEventData data; + + public PermissionRequestedEventData getData() { return data; } + public void setData(PermissionRequestedEventData data) { this.data = data; } + + /** Data payload for {@link PermissionRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record PermissionRequestedEventData( + /** Unique identifier for this permission request; used to respond via session.respondToPermission() */ + @JsonProperty("requestId") String requestId, + /** Details of the permission being requested */ + @JsonProperty("permissionRequest") Object permissionRequest, + /** Derived user-facing permission prompt details for UI consumers */ + @JsonProperty("promptRequest") Object promptRequest, + /** When true, this permission was already resolved by a permissionRequest hook and requires no client action */ + @JsonProperty("resolvedByHook") Boolean resolvedByHook + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/PlanChangedOperation.java b/java/src/generated/java/com/github/copilot/sdk/generated/PlanChangedOperation.java new file mode 100644 index 000000000..cd52d7ec1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/PlanChangedOperation.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The type of operation performed on the plan file + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PlanChangedOperation { + /** The {@code create} variant. */ + CREATE("create"), + /** The {@code update} variant. */ + UPDATE("update"), + /** The {@code delete} variant. */ + DELETE("delete"); + + private final String value; + PlanChangedOperation(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PlanChangedOperation fromValue(String value) { + for (PlanChangedOperation v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PlanChangedOperation value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ReasoningSummary.java b/java/src/generated/java/com/github/copilot/sdk/generated/ReasoningSummary.java new file mode 100644 index 000000000..98897bbc9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ReasoningSummary.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ReasoningSummary { + /** The {@code none} variant. */ + NONE("none"), + /** The {@code concise} variant. */ + CONCISE("concise"), + /** The {@code detailed} variant. */ + DETAILED("detailed"); + + private final String value; + ReasoningSummary(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ReasoningSummary fromValue(String value) { + for (ReasoningSummary v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ReasoningSummary value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SamplingCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SamplingCompletedEvent.java new file mode 100644 index 000000000..2c6b264aa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SamplingCompletedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "sampling.completed". Sampling request completion notification signaling UI dismissal + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SamplingCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "sampling.completed"; } + + @JsonProperty("data") + private SamplingCompletedEventData data; + + public SamplingCompletedEventData getData() { return data; } + public void setData(SamplingCompletedEventData data) { this.data = data; } + + /** Data payload for {@link SamplingCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SamplingCompletedEventData( + /** Request ID of the resolved sampling request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SamplingRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SamplingRequestedEvent.java new file mode 100644 index 000000000..2be3cfc49 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SamplingRequestedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "sampling.requested". Sampling request from an MCP server; contains the server name and a requestId for correlation + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SamplingRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "sampling.requested"; } + + @JsonProperty("data") + private SamplingRequestedEventData data; + + public SamplingRequestedEventData getData() { return data; } + public void setData(SamplingRequestedEventData data) { this.data = data; } + + /** Data payload for {@link SamplingRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SamplingRequestedEventData( + /** Unique identifier for this sampling request; used to respond via session.respondToSampling() */ + @JsonProperty("requestId") String requestId, + /** Name of the MCP server that initiated the sampling request */ + @JsonProperty("serverName") String serverName, + /** The JSON-RPC request ID from the MCP protocol */ + @JsonProperty("mcpRequestId") Object mcpRequestId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionBackgroundTasksChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionBackgroundTasksChangedEvent.java new file mode 100644 index 000000000..062954dfb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionBackgroundTasksChangedEvent.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.background_tasks_changed". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionBackgroundTasksChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.background_tasks_changed"; } + + @JsonProperty("data") + private SessionBackgroundTasksChangedEventData data; + + public SessionBackgroundTasksChangedEventData getData() { return data; } + public void setData(SessionBackgroundTasksChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionBackgroundTasksChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionBackgroundTasksChangedEventData() { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java new file mode 100644 index 000000000..b6a3775e9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionCompleteEvent.java @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.compaction_complete". Conversation compaction results including success status, metrics, and optional error details + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCompactionCompleteEvent extends SessionEvent { + + @Override + public String getType() { return "session.compaction_complete"; } + + @JsonProperty("data") + private SessionCompactionCompleteEventData data; + + public SessionCompactionCompleteEventData getData() { return data; } + public void setData(SessionCompactionCompleteEventData data) { this.data = data; } + + /** Data payload for {@link SessionCompactionCompleteEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCompactionCompleteEventData( + /** Whether compaction completed successfully */ + @JsonProperty("success") Boolean success, + /** Error message if compaction failed */ + @JsonProperty("error") String error, + /** Total tokens in conversation before compaction */ + @JsonProperty("preCompactionTokens") Double preCompactionTokens, + /** Total tokens in conversation after compaction */ + @JsonProperty("postCompactionTokens") Double postCompactionTokens, + /** Number of messages before compaction */ + @JsonProperty("preCompactionMessagesLength") Double preCompactionMessagesLength, + /** Number of messages removed during compaction */ + @JsonProperty("messagesRemoved") Double messagesRemoved, + /** Number of tokens removed during compaction */ + @JsonProperty("tokensRemoved") Double tokensRemoved, + /** LLM-generated summary of the compacted conversation history */ + @JsonProperty("summaryContent") String summaryContent, + /** Checkpoint snapshot number created for recovery */ + @JsonProperty("checkpointNumber") Double checkpointNumber, + /** File path where the checkpoint was stored */ + @JsonProperty("checkpointPath") String checkpointPath, + /** Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) */ + @JsonProperty("compactionTokensUsed") CompactionCompleteCompactionTokensUsed compactionTokensUsed, + /** GitHub request tracing ID (x-github-request-id header) for the compaction LLM call */ + @JsonProperty("requestId") String requestId, + /** Token count from system message(s) after compaction */ + @JsonProperty("systemTokens") Double systemTokens, + /** Token count from non-system messages (user, assistant, tool) after compaction */ + @JsonProperty("conversationTokens") Double conversationTokens, + /** Token count from tool definitions after compaction */ + @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java new file mode 100644 index 000000000..2ff7ace48 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCompactionStartEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.compaction_start". Context window breakdown at the start of LLM-powered conversation compaction + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCompactionStartEvent extends SessionEvent { + + @Override + public String getType() { return "session.compaction_start"; } + + @JsonProperty("data") + private SessionCompactionStartEventData data; + + public SessionCompactionStartEventData getData() { return data; } + public void setData(SessionCompactionStartEventData data) { this.data = data; } + + /** Data payload for {@link SessionCompactionStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCompactionStartEventData( + /** Token count from system message(s) at compaction start */ + @JsonProperty("systemTokens") Double systemTokens, + /** Token count from non-system messages (user, assistant, tool) at compaction start */ + @JsonProperty("conversationTokens") Double conversationTokens, + /** Token count from tool definitions at compaction start */ + @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionContextChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionContextChangedEvent.java new file mode 100644 index 000000000..6cc775d52 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionContextChangedEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.context_changed". Updated working directory and git context after the change + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionContextChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.context_changed"; } + + @JsonProperty("data") + private SessionContextChangedEventData data; + + public SessionContextChangedEventData getData() { return data; } + public void setData(SessionContextChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionContextChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionContextChangedEventData( + /** Current working directory path */ + @JsonProperty("cwd") String cwd, + /** Root directory of the git repository, resolved via git rev-parse */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ + @JsonProperty("repository") String repository, + /** Hosting platform type of the repository (github or ado) */ + @JsonProperty("hostType") WorkingDirectoryContextHostType hostType, + /** Raw host string from the git remote URL (e.g. "github.com", "mycompany.ghe.com", "dev.azure.com") */ + @JsonProperty("repositoryHost") String repositoryHost, + /** Current git branch name */ + @JsonProperty("branch") String branch, + /** Head commit of current git branch at session start time */ + @JsonProperty("headCommit") String headCommit, + /** Base commit of current git branch at session start time */ + @JsonProperty("baseCommit") String baseCommit + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomAgentsUpdatedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomAgentsUpdatedEvent.java new file mode 100644 index 000000000..ec10ed9c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomAgentsUpdatedEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "session.custom_agents_updated". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCustomAgentsUpdatedEvent extends SessionEvent { + + @Override + public String getType() { return "session.custom_agents_updated"; } + + @JsonProperty("data") + private SessionCustomAgentsUpdatedEventData data; + + public SessionCustomAgentsUpdatedEventData getData() { return data; } + public void setData(SessionCustomAgentsUpdatedEventData data) { this.data = data; } + + /** Data payload for {@link SessionCustomAgentsUpdatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCustomAgentsUpdatedEventData( + /** Array of loaded custom agent metadata */ + @JsonProperty("agents") List agents, + /** Non-fatal warnings from agent loading */ + @JsonProperty("warnings") List warnings, + /** Fatal errors from agent loading */ + @JsonProperty("errors") List errors + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java new file mode 100644 index 000000000..3078895b7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionCustomNotificationEvent.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "session.custom_notification". Opaque custom notification data. Consumers may branch on source and name, but payload semantics are source-defined. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCustomNotificationEvent extends SessionEvent { + + @Override + public String getType() { return "session.custom_notification"; } + + @JsonProperty("data") + private SessionCustomNotificationEventData data; + + public SessionCustomNotificationEventData getData() { return data; } + public void setData(SessionCustomNotificationEventData data) { this.data = data; } + + /** Data payload for {@link SessionCustomNotificationEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCustomNotificationEventData( + /** Namespace for the custom notification producer */ + @JsonProperty("source") String source, + /** Source-defined custom notification name */ + @JsonProperty("name") String name, + /** Optional source-defined payload schema version */ + @JsonProperty("version") Long version, + /** Optional source-defined string identifiers describing the payload subject */ + @JsonProperty("subject") Map subject, + /** Source-defined JSON payload for the custom notification */ + @JsonProperty("payload") Object payload + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java new file mode 100644 index 000000000..48ae04ade --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionErrorEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.error". Error details for timeline display including message and optional diagnostic information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionErrorEvent extends SessionEvent { + + @Override + public String getType() { return "session.error"; } + + @JsonProperty("data") + private SessionErrorEventData data; + + public SessionErrorEventData getData() { return data; } + public void setData(SessionErrorEventData data) { this.data = data; } + + /** Data payload for {@link SessionErrorEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionErrorEventData( + /** Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "context_limit", "query") */ + @JsonProperty("errorType") String errorType, + /** Fine-grained error code from the upstream provider, when available. For `errorType: "rate_limit"`, this is one of the `RateLimitErrorCode` values (e.g., `"user_weekly_rate_limited"`, `"user_global_rate_limited"`, `"rate_limited"`, `"user_model_rate_limited"`, `"integration_rate_limited"`). For `errorType: "quota"`, this is the CAPI quota error code (e.g., `"quota_exceeded"`, `"session_quota_exceeded"`, `"billing_not_configured"`). */ + @JsonProperty("errorCode") String errorCode, + /** Only set on `errorType: "rate_limit"`. When `true`, the runtime will follow this error with an `auto_mode_switch.requested` event (or silently switch if `continueOnAutoMode` is enabled). UI clients can use this flag to suppress duplicate rendering of the rate-limit error when they show their own auto-mode-switch prompt. */ + @JsonProperty("eligibleForAutoSwitch") Boolean eligibleForAutoSwitch, + /** Human-readable error message */ + @JsonProperty("message") String message, + /** Error stack trace, when available */ + @JsonProperty("stack") String stack, + /** HTTP status code from the upstream request, if applicable */ + @JsonProperty("statusCode") Long statusCode, + /** GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ + @JsonProperty("providerCallId") String providerCallId, + /** Optional URL associated with this error that the user can open in a browser */ + @JsonProperty("url") String url + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java new file mode 100644 index 000000000..2181be197 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionEvent.java @@ -0,0 +1,229 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import java.time.OffsetDateTime; +import java.util.UUID; +import javax.annotation.processing.Generated; + +/** + * Base class for all generated session events. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = UnknownSessionEvent.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SessionStartEvent.class, name = "session.start"), + @JsonSubTypes.Type(value = SessionResumeEvent.class, name = "session.resume"), + @JsonSubTypes.Type(value = SessionRemoteSteerableChangedEvent.class, name = "session.remote_steerable_changed"), + @JsonSubTypes.Type(value = SessionErrorEvent.class, name = "session.error"), + @JsonSubTypes.Type(value = SessionIdleEvent.class, name = "session.idle"), + @JsonSubTypes.Type(value = SessionTitleChangedEvent.class, name = "session.title_changed"), + @JsonSubTypes.Type(value = SessionScheduleCreatedEvent.class, name = "session.schedule_created"), + @JsonSubTypes.Type(value = SessionScheduleCancelledEvent.class, name = "session.schedule_cancelled"), + @JsonSubTypes.Type(value = SessionInfoEvent.class, name = "session.info"), + @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), + @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), + @JsonSubTypes.Type(value = SessionModeChangedEvent.class, name = "session.mode_changed"), + @JsonSubTypes.Type(value = SessionPlanChangedEvent.class, name = "session.plan_changed"), + @JsonSubTypes.Type(value = SessionWorkspaceFileChangedEvent.class, name = "session.workspace_file_changed"), + @JsonSubTypes.Type(value = SessionHandoffEvent.class, name = "session.handoff"), + @JsonSubTypes.Type(value = SessionTruncationEvent.class, name = "session.truncation"), + @JsonSubTypes.Type(value = SessionSnapshotRewindEvent.class, name = "session.snapshot_rewind"), + @JsonSubTypes.Type(value = SessionShutdownEvent.class, name = "session.shutdown"), + @JsonSubTypes.Type(value = SessionContextChangedEvent.class, name = "session.context_changed"), + @JsonSubTypes.Type(value = SessionUsageInfoEvent.class, name = "session.usage_info"), + @JsonSubTypes.Type(value = SessionCompactionStartEvent.class, name = "session.compaction_start"), + @JsonSubTypes.Type(value = SessionCompactionCompleteEvent.class, name = "session.compaction_complete"), + @JsonSubTypes.Type(value = SessionTaskCompleteEvent.class, name = "session.task_complete"), + @JsonSubTypes.Type(value = UserMessageEvent.class, name = "user.message"), + @JsonSubTypes.Type(value = PendingMessagesModifiedEvent.class, name = "pending_messages.modified"), + @JsonSubTypes.Type(value = AssistantTurnStartEvent.class, name = "assistant.turn_start"), + @JsonSubTypes.Type(value = AssistantIntentEvent.class, name = "assistant.intent"), + @JsonSubTypes.Type(value = AssistantReasoningEvent.class, name = "assistant.reasoning"), + @JsonSubTypes.Type(value = AssistantReasoningDeltaEvent.class, name = "assistant.reasoning_delta"), + @JsonSubTypes.Type(value = AssistantStreamingDeltaEvent.class, name = "assistant.streaming_delta"), + @JsonSubTypes.Type(value = AssistantMessageEvent.class, name = "assistant.message"), + @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), + @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), + @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), + @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), + @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), + @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), + @JsonSubTypes.Type(value = ToolUserRequestedEvent.class, name = "tool.user_requested"), + @JsonSubTypes.Type(value = ToolExecutionStartEvent.class, name = "tool.execution_start"), + @JsonSubTypes.Type(value = ToolExecutionPartialResultEvent.class, name = "tool.execution_partial_result"), + @JsonSubTypes.Type(value = ToolExecutionProgressEvent.class, name = "tool.execution_progress"), + @JsonSubTypes.Type(value = ToolExecutionCompleteEvent.class, name = "tool.execution_complete"), + @JsonSubTypes.Type(value = SkillInvokedEvent.class, name = "skill.invoked"), + @JsonSubTypes.Type(value = SubagentStartedEvent.class, name = "subagent.started"), + @JsonSubTypes.Type(value = SubagentCompletedEvent.class, name = "subagent.completed"), + @JsonSubTypes.Type(value = SubagentFailedEvent.class, name = "subagent.failed"), + @JsonSubTypes.Type(value = SubagentSelectedEvent.class, name = "subagent.selected"), + @JsonSubTypes.Type(value = SubagentDeselectedEvent.class, name = "subagent.deselected"), + @JsonSubTypes.Type(value = HookStartEvent.class, name = "hook.start"), + @JsonSubTypes.Type(value = HookEndEvent.class, name = "hook.end"), + @JsonSubTypes.Type(value = SystemMessageEvent.class, name = "system.message"), + @JsonSubTypes.Type(value = SystemNotificationEvent.class, name = "system.notification"), + @JsonSubTypes.Type(value = PermissionRequestedEvent.class, name = "permission.requested"), + @JsonSubTypes.Type(value = PermissionCompletedEvent.class, name = "permission.completed"), + @JsonSubTypes.Type(value = UserInputRequestedEvent.class, name = "user_input.requested"), + @JsonSubTypes.Type(value = UserInputCompletedEvent.class, name = "user_input.completed"), + @JsonSubTypes.Type(value = ElicitationRequestedEvent.class, name = "elicitation.requested"), + @JsonSubTypes.Type(value = ElicitationCompletedEvent.class, name = "elicitation.completed"), + @JsonSubTypes.Type(value = SamplingRequestedEvent.class, name = "sampling.requested"), + @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), + @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), + @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), + @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), + @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), + @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), + @JsonSubTypes.Type(value = CommandQueuedEvent.class, name = "command.queued"), + @JsonSubTypes.Type(value = CommandExecuteEvent.class, name = "command.execute"), + @JsonSubTypes.Type(value = CommandCompletedEvent.class, name = "command.completed"), + @JsonSubTypes.Type(value = AutoModeSwitchRequestedEvent.class, name = "auto_mode_switch.requested"), + @JsonSubTypes.Type(value = AutoModeSwitchCompletedEvent.class, name = "auto_mode_switch.completed"), + @JsonSubTypes.Type(value = CommandsChangedEvent.class, name = "commands.changed"), + @JsonSubTypes.Type(value = CapabilitiesChangedEvent.class, name = "capabilities.changed"), + @JsonSubTypes.Type(value = ExitPlanModeRequestedEvent.class, name = "exit_plan_mode.requested"), + @JsonSubTypes.Type(value = ExitPlanModeCompletedEvent.class, name = "exit_plan_mode.completed"), + @JsonSubTypes.Type(value = SessionToolsUpdatedEvent.class, name = "session.tools_updated"), + @JsonSubTypes.Type(value = SessionBackgroundTasksChangedEvent.class, name = "session.background_tasks_changed"), + @JsonSubTypes.Type(value = SessionSkillsLoadedEvent.class, name = "session.skills_loaded"), + @JsonSubTypes.Type(value = SessionCustomAgentsUpdatedEvent.class, name = "session.custom_agents_updated"), + @JsonSubTypes.Type(value = SessionMcpServersLoadedEvent.class, name = "session.mcp_servers_loaded"), + @JsonSubTypes.Type(value = SessionMcpServerStatusChangedEvent.class, name = "session.mcp_server_status_changed"), + @JsonSubTypes.Type(value = SessionExtensionsLoadedEvent.class, name = "session.extensions_loaded") +}) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract sealed class SessionEvent permits + SessionStartEvent, + SessionResumeEvent, + SessionRemoteSteerableChangedEvent, + SessionErrorEvent, + SessionIdleEvent, + SessionTitleChangedEvent, + SessionScheduleCreatedEvent, + SessionScheduleCancelledEvent, + SessionInfoEvent, + SessionWarningEvent, + SessionModelChangeEvent, + SessionModeChangedEvent, + SessionPlanChangedEvent, + SessionWorkspaceFileChangedEvent, + SessionHandoffEvent, + SessionTruncationEvent, + SessionSnapshotRewindEvent, + SessionShutdownEvent, + SessionContextChangedEvent, + SessionUsageInfoEvent, + SessionCompactionStartEvent, + SessionCompactionCompleteEvent, + SessionTaskCompleteEvent, + UserMessageEvent, + PendingMessagesModifiedEvent, + AssistantTurnStartEvent, + AssistantIntentEvent, + AssistantReasoningEvent, + AssistantReasoningDeltaEvent, + AssistantStreamingDeltaEvent, + AssistantMessageEvent, + AssistantMessageStartEvent, + AssistantMessageDeltaEvent, + AssistantTurnEndEvent, + AssistantUsageEvent, + ModelCallFailureEvent, + AbortEvent, + ToolUserRequestedEvent, + ToolExecutionStartEvent, + ToolExecutionPartialResultEvent, + ToolExecutionProgressEvent, + ToolExecutionCompleteEvent, + SkillInvokedEvent, + SubagentStartedEvent, + SubagentCompletedEvent, + SubagentFailedEvent, + SubagentSelectedEvent, + SubagentDeselectedEvent, + HookStartEvent, + HookEndEvent, + SystemMessageEvent, + SystemNotificationEvent, + PermissionRequestedEvent, + PermissionCompletedEvent, + UserInputRequestedEvent, + UserInputCompletedEvent, + ElicitationRequestedEvent, + ElicitationCompletedEvent, + SamplingRequestedEvent, + SamplingCompletedEvent, + McpOauthRequiredEvent, + McpOauthCompletedEvent, + SessionCustomNotificationEvent, + ExternalToolRequestedEvent, + ExternalToolCompletedEvent, + CommandQueuedEvent, + CommandExecuteEvent, + CommandCompletedEvent, + AutoModeSwitchRequestedEvent, + AutoModeSwitchCompletedEvent, + CommandsChangedEvent, + CapabilitiesChangedEvent, + ExitPlanModeRequestedEvent, + ExitPlanModeCompletedEvent, + SessionToolsUpdatedEvent, + SessionBackgroundTasksChangedEvent, + SessionSkillsLoadedEvent, + SessionCustomAgentsUpdatedEvent, + SessionMcpServersLoadedEvent, + SessionMcpServerStatusChangedEvent, + SessionExtensionsLoadedEvent, + UnknownSessionEvent { + + /** Unique event identifier (UUID v4), generated when the event is emitted. */ + @JsonProperty("id") + private UUID id; + + /** ISO 8601 timestamp when the event was created. */ + @JsonProperty("timestamp") + private OffsetDateTime timestamp; + + /** ID of the chronologically preceding event in the session. Null for the first event. */ + @JsonProperty("parentId") + private UUID parentId; + + /** When true, the event is transient and not persisted to the session event log on disk. */ + @JsonProperty("ephemeral") + private Boolean ephemeral; + + /** + * Returns the event-type discriminator string (e.g., {@code "session.idle"}). + * + * @return the event type + */ + public abstract String getType(); + + public UUID getId() { return id; } + public void setId(UUID id) { this.id = id; } + + public OffsetDateTime getTimestamp() { return timestamp; } + public void setTimestamp(OffsetDateTime timestamp) { this.timestamp = timestamp; } + + public UUID getParentId() { return parentId; } + public void setParentId(UUID parentId) { this.parentId = parentId; } + + public Boolean getEphemeral() { return ephemeral; } + public void setEphemeral(Boolean ephemeral) { this.ephemeral = ephemeral; } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionExtensionsLoadedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionExtensionsLoadedEvent.java new file mode 100644 index 000000000..a1b9b8fce --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionExtensionsLoadedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "session.extensions_loaded". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionExtensionsLoadedEvent extends SessionEvent { + + @Override + public String getType() { return "session.extensions_loaded"; } + + @JsonProperty("data") + private SessionExtensionsLoadedEventData data; + + public SessionExtensionsLoadedEventData getData() { return data; } + public void setData(SessionExtensionsLoadedEventData data) { this.data = data; } + + /** Data payload for {@link SessionExtensionsLoadedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionExtensionsLoadedEventData( + /** Array of discovered extensions and their status */ + @JsonProperty("extensions") List extensions + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionHandoffEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionHandoffEvent.java new file mode 100644 index 000000000..11599defa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionHandoffEvent.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Session event "session.handoff". Session handoff metadata including source, context, and repository information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionHandoffEvent extends SessionEvent { + + @Override + public String getType() { return "session.handoff"; } + + @JsonProperty("data") + private SessionHandoffEventData data; + + public SessionHandoffEventData getData() { return data; } + public void setData(SessionHandoffEventData data) { this.data = data; } + + /** Data payload for {@link SessionHandoffEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionHandoffEventData( + /** ISO 8601 timestamp when the handoff occurred */ + @JsonProperty("handoffTime") OffsetDateTime handoffTime, + /** Origin type of the session being handed off */ + @JsonProperty("sourceType") HandoffSourceType sourceType, + /** Repository context for the handed-off session */ + @JsonProperty("repository") HandoffRepository repository, + /** Additional context information for the handoff */ + @JsonProperty("context") String context, + /** Summary of the work done in the source session */ + @JsonProperty("summary") String summary, + /** Session ID of the remote session being handed off */ + @JsonProperty("remoteSessionId") String remoteSessionId, + /** GitHub host URL for the source session (e.g., https://github.com or https://tenant.ghe.com) */ + @JsonProperty("host") String host + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionIdleEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionIdleEvent.java new file mode 100644 index 000000000..cdb38a344 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionIdleEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.idle". Payload indicating the session is idle with no background agents in flight + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionIdleEvent extends SessionEvent { + + @Override + public String getType() { return "session.idle"; } + + @JsonProperty("data") + private SessionIdleEventData data; + + public SessionIdleEventData getData() { return data; } + public void setData(SessionIdleEventData data) { this.data = data; } + + /** Data payload for {@link SessionIdleEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionIdleEventData( + /** True when the preceding agentic loop was cancelled via abort signal */ + @JsonProperty("aborted") Boolean aborted + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java new file mode 100644 index 000000000..c0613dfa2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionInfoEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.info". Informational message for timeline display with categorization + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionInfoEvent extends SessionEvent { + + @Override + public String getType() { return "session.info"; } + + @JsonProperty("data") + private SessionInfoEventData data; + + public SessionInfoEventData getData() { return data; } + public void setData(SessionInfoEventData data) { this.data = data; } + + /** Data payload for {@link SessionInfoEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionInfoEventData( + /** Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model") */ + @JsonProperty("infoType") String infoType, + /** Human-readable informational message for display in the timeline */ + @JsonProperty("message") String message, + /** Optional URL associated with this message that the user can open in a browser */ + @JsonProperty("url") String url, + /** Optional actionable tip displayed with this message */ + @JsonProperty("tip") String tip + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServerStatusChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServerStatusChangedEvent.java new file mode 100644 index 000000000..27cf4d9cc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServerStatusChangedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mcp_server_status_changed". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServerStatusChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_server_status_changed"; } + + @JsonProperty("data") + private SessionMcpServerStatusChangedEventData data; + + public SessionMcpServerStatusChangedEventData getData() { return data; } + public void setData(SessionMcpServerStatusChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServerStatusChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServerStatusChangedEventData( + /** Name of the MCP server whose status changed */ + @JsonProperty("serverName") String serverName, + /** Connection status: connected, failed, needs-auth, pending, disabled, or not_configured */ + @JsonProperty("status") McpServerStatus status + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServersLoadedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServersLoadedEvent.java new file mode 100644 index 000000000..0a0b7bc50 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMcpServersLoadedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mcp_servers_loaded". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServersLoadedEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_servers_loaded"; } + + @JsonProperty("data") + private SessionMcpServersLoadedEventData data; + + public SessionMcpServersLoadedEventData getData() { return data; } + public void setData(SessionMcpServersLoadedEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServersLoadedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServersLoadedEventData( + /** Array of MCP server status summaries */ + @JsonProperty("servers") List servers + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMode.java new file mode 100644 index 000000000..f6359c0df --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The session mode the agent is operating in + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"); + + private final String value; + SessionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionMode fromValue(String value) { + for (SessionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionModeChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionModeChangedEvent.java new file mode 100644 index 000000000..c997f9850 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionModeChangedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mode_changed". Agent mode change details including previous and new modes + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModeChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.mode_changed"; } + + @JsonProperty("data") + private SessionModeChangedEventData data; + + public SessionModeChangedEventData getData() { return data; } + public void setData(SessionModeChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionModeChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionModeChangedEventData( + /** The session mode the agent is operating in */ + @JsonProperty("previousMode") SessionMode previousMode, + /** The session mode the agent is operating in */ + @JsonProperty("newMode") SessionMode newMode + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java new file mode 100644 index 000000000..8225fc78f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionModelChangeEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.model_change". Model change details including previous and new model identifiers + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModelChangeEvent extends SessionEvent { + + @Override + public String getType() { return "session.model_change"; } + + @JsonProperty("data") + private SessionModelChangeEventData data; + + public SessionModelChangeEventData getData() { return data; } + public void setData(SessionModelChangeEventData data) { this.data = data; } + + /** Data payload for {@link SessionModelChangeEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionModelChangeEventData( + /** Model that was previously selected, if any */ + @JsonProperty("previousModel") String previousModel, + /** Newly selected model identifier */ + @JsonProperty("newModel") String newModel, + /** Reasoning effort level before the model change, if applicable */ + @JsonProperty("previousReasoningEffort") String previousReasoningEffort, + /** Reasoning effort level after the model change, if applicable */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reasoning summary mode before the model change, if applicable */ + @JsonProperty("previousReasoningSummary") ReasoningSummary previousReasoningSummary, + /** Reasoning summary mode after the model change, if applicable */ + @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. */ + @JsonProperty("cause") String cause + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionPlanChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionPlanChangedEvent.java new file mode 100644 index 000000000..266ec307d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionPlanChangedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.plan_changed". Plan file operation details indicating what changed + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPlanChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.plan_changed"; } + + @JsonProperty("data") + private SessionPlanChangedEventData data; + + public SessionPlanChangedEventData getData() { return data; } + public void setData(SessionPlanChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionPlanChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionPlanChangedEventData( + /** The type of operation performed on the plan file */ + @JsonProperty("operation") PlanChangedOperation operation + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionRemoteSteerableChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionRemoteSteerableChangedEvent.java new file mode 100644 index 000000000..5ba942315 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionRemoteSteerableChangedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.remote_steerable_changed". Notifies that the session's remote steering capability has changed + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionRemoteSteerableChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.remote_steerable_changed"; } + + @JsonProperty("data") + private SessionRemoteSteerableChangedEventData data; + + public SessionRemoteSteerableChangedEventData getData() { return data; } + public void setData(SessionRemoteSteerableChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionRemoteSteerableChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionRemoteSteerableChangedEventData( + /** Whether this session now supports remote steering via GitHub */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java new file mode 100644 index 000000000..93316117b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionResumeEvent.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Session event "session.resume". Session resume metadata including current context and event count + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionResumeEvent extends SessionEvent { + + @Override + public String getType() { return "session.resume"; } + + @JsonProperty("data") + private SessionResumeEventData data; + + public SessionResumeEventData getData() { return data; } + public void setData(SessionResumeEventData data) { this.data = data; } + + /** Data payload for {@link SessionResumeEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionResumeEventData( + /** ISO 8601 timestamp when the session was resumed */ + @JsonProperty("resumeTime") OffsetDateTime resumeTime, + /** Total number of persisted events in the session at the time of resume */ + @JsonProperty("eventCount") Double eventCount, + /** Model currently selected at resume time */ + @JsonProperty("selectedModel") String selectedModel, + /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ + @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Updated working directory and git context at resume time */ + @JsonProperty("context") WorkingDirectoryContext context, + /** Whether the session was already in use by another client at resume time */ + @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** True when this resume attached to a session that the runtime already had running in-memory (for example, an extension joining a session another client was actively driving). False (or omitted) for cold resumes β€” the runtime had to reconstitute the session from its persisted event log. */ + @JsonProperty("sessionWasActive") Boolean sessionWasActive, + /** Whether this session supports remote steering via GitHub */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When true, tool calls and permission requests left in flight by the previous session lifetime remain pending after resume and the agentic loop awaits their results. User sends are queued behind the pending work until all such requests reach a terminal state. When false (the default), any such tool calls and permission requests are immediately marked as interrupted on resume. */ + @JsonProperty("continuePendingWork") Boolean continuePendingWork + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java new file mode 100644 index 000000000..2c480a757 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCancelledEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.schedule_cancelled". Scheduled prompt cancelled from the schedule manager dialog + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCancelledEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_cancelled"; } + + @JsonProperty("data") + private SessionScheduleCancelledEventData data; + + public SessionScheduleCancelledEventData getData() { return data; } + public void setData(SessionScheduleCancelledEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCancelledEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCancelledEventData( + /** Id of the scheduled prompt that was cancelled */ + @JsonProperty("id") Long id + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java new file mode 100644 index 000000000..eb051a014 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionScheduleCreatedEvent.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.schedule_created". Scheduled prompt registered via /every or /after + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionScheduleCreatedEvent extends SessionEvent { + + @Override + public String getType() { return "session.schedule_created"; } + + @JsonProperty("data") + private SessionScheduleCreatedEventData data; + + public SessionScheduleCreatedEventData getData() { return data; } + public void setData(SessionScheduleCreatedEventData data) { this.data = data; } + + /** Data payload for {@link SessionScheduleCreatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionScheduleCreatedEventData( + /** Sequential id assigned to the scheduled prompt within the session */ + @JsonProperty("id") Long id, + /** Interval between ticks in milliseconds */ + @JsonProperty("intervalMs") Long intervalMs, + /** Prompt text that gets enqueued on every tick */ + @JsonProperty("prompt") String prompt, + /** Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`) */ + @JsonProperty("recurring") Boolean recurring, + /** Optional user-facing label shown in the timeline instead of the actual prompt (e.g. `/skill-name args` when the prompt is a skill invocation expansion) */ + @JsonProperty("displayPrompt") String displayPrompt + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java new file mode 100644 index 000000000..269416994 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionShutdownEvent.java @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "session.shutdown". Session termination metrics including usage statistics, code changes, and shutdown reason + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionShutdownEvent extends SessionEvent { + + @Override + public String getType() { return "session.shutdown"; } + + @JsonProperty("data") + private SessionShutdownEventData data; + + public SessionShutdownEventData getData() { return data; } + public void setData(SessionShutdownEventData data) { this.data = data; } + + /** Data payload for {@link SessionShutdownEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionShutdownEventData( + /** Whether the session ended normally ("routine") or due to a crash/fatal error ("error") */ + @JsonProperty("shutdownType") ShutdownType shutdownType, + /** Error description when shutdownType is "error" */ + @JsonProperty("errorReason") String errorReason, + /** Total number of premium API requests used during the session */ + @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, + /** Cumulative time spent in API calls during the session, in milliseconds */ + @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, + /** Unix timestamp (milliseconds) when the session started */ + @JsonProperty("sessionStartTime") Double sessionStartTime, + /** Aggregate code change metrics for the session */ + @JsonProperty("codeChanges") ShutdownCodeChanges codeChanges, + /** Per-model usage breakdown, keyed by model identifier */ + @JsonProperty("modelMetrics") Map modelMetrics, + /** Model that was selected at the time of shutdown */ + @JsonProperty("currentModel") String currentModel, + /** Total tokens in context window at shutdown */ + @JsonProperty("currentTokens") Double currentTokens, + /** System message token count at shutdown */ + @JsonProperty("systemTokens") Double systemTokens, + /** Non-system message token count at shutdown */ + @JsonProperty("conversationTokens") Double conversationTokens, + /** Tool definitions token count at shutdown */ + @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionSkillsLoadedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSkillsLoadedEvent.java new file mode 100644 index 000000000..c429125bf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSkillsLoadedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "session.skills_loaded". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionSkillsLoadedEvent extends SessionEvent { + + @Override + public String getType() { return "session.skills_loaded"; } + + @JsonProperty("data") + private SessionSkillsLoadedEventData data; + + public SessionSkillsLoadedEventData getData() { return data; } + public void setData(SessionSkillsLoadedEventData data) { this.data = data; } + + /** Data payload for {@link SessionSkillsLoadedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionSkillsLoadedEventData( + /** Array of resolved skill metadata */ + @JsonProperty("skills") List skills + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java new file mode 100644 index 000000000..0b564f291 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionSnapshotRewindEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.snapshot_rewind". Session rewind details including target event and count of removed events + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionSnapshotRewindEvent extends SessionEvent { + + @Override + public String getType() { return "session.snapshot_rewind"; } + + @JsonProperty("data") + private SessionSnapshotRewindEventData data; + + public SessionSnapshotRewindEventData getData() { return data; } + public void setData(SessionSnapshotRewindEventData data) { this.data = data; } + + /** Data payload for {@link SessionSnapshotRewindEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionSnapshotRewindEventData( + /** Event ID that was rewound to; this event and all after it were removed */ + @JsonProperty("upToEventId") String upToEventId, + /** Number of events that were removed by the rewind */ + @JsonProperty("eventsRemoved") Double eventsRemoved + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java new file mode 100644 index 000000000..d3484b3e1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionStartEvent.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Session event "session.start". Session initialization metadata including context and configuration + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionStartEvent extends SessionEvent { + + @Override + public String getType() { return "session.start"; } + + @JsonProperty("data") + private SessionStartEventData data; + + public SessionStartEventData getData() { return data; } + public void setData(SessionStartEventData data) { this.data = data; } + + /** Data payload for {@link SessionStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionStartEventData( + /** Unique identifier for the session */ + @JsonProperty("sessionId") String sessionId, + /** Schema version number for the session event format */ + @JsonProperty("version") Double version, + /** Identifier of the software producing the events (e.g., "copilot-agent") */ + @JsonProperty("producer") String producer, + /** Version string of the Copilot application */ + @JsonProperty("copilotVersion") String copilotVersion, + /** ISO 8601 timestamp when the session was created */ + @JsonProperty("startTime") OffsetDateTime startTime, + /** Model selected at session creation time, if any */ + @JsonProperty("selectedModel") String selectedModel, + /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ + @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Working directory and git context at session start */ + @JsonProperty("context") WorkingDirectoryContext context, + /** Whether the session was already in use by another client at start time */ + @JsonProperty("alreadyInUse") Boolean alreadyInUse, + /** Whether this session supports remote steering via GitHub */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable, + /** When set, identifies a parent session whose context this session continues β€” e.g., a detached headless rem-agent run launched on the parent's interactive shutdown. Telemetry from this session is reported under the parent's session_id. */ + @JsonProperty("detachedFromSpawningParentSessionId") String detachedFromSpawningParentSessionId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionTaskCompleteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTaskCompleteEvent.java new file mode 100644 index 000000000..f114ff82b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTaskCompleteEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.task_complete". Task completion notification with summary from the agent + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTaskCompleteEvent extends SessionEvent { + + @Override + public String getType() { return "session.task_complete"; } + + @JsonProperty("data") + private SessionTaskCompleteEventData data; + + public SessionTaskCompleteEventData getData() { return data; } + public void setData(SessionTaskCompleteEventData data) { this.data = data; } + + /** Data payload for {@link SessionTaskCompleteEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionTaskCompleteEventData( + /** Summary of the completed task, provided by the agent */ + @JsonProperty("summary") String summary, + /** Whether the tool call succeeded. False when validation failed (e.g., invalid arguments) */ + @JsonProperty("success") Boolean success + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionTitleChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTitleChangedEvent.java new file mode 100644 index 000000000..4d6086bf3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTitleChangedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.title_changed". Session title change payload containing the new display title + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTitleChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.title_changed"; } + + @JsonProperty("data") + private SessionTitleChangedEventData data; + + public SessionTitleChangedEventData getData() { return data; } + public void setData(SessionTitleChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionTitleChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionTitleChangedEventData( + /** The new display title for the session */ + @JsonProperty("title") String title + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionToolsUpdatedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionToolsUpdatedEvent.java new file mode 100644 index 000000000..0cb8614c7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionToolsUpdatedEvent.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.tools_updated". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionToolsUpdatedEvent extends SessionEvent { + + @Override + public String getType() { return "session.tools_updated"; } + + @JsonProperty("data") + private SessionToolsUpdatedEventData data; + + public SessionToolsUpdatedEventData getData() { return data; } + public void setData(SessionToolsUpdatedEventData data) { this.data = data; } + + /** Data payload for {@link SessionToolsUpdatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionToolsUpdatedEventData( + /** Identifier of the model the resolved tools apply to. */ + @JsonProperty("model") String model + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java new file mode 100644 index 000000000..b2ffad48f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionTruncationEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.truncation". Conversation truncation statistics including token counts and removed content metrics + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTruncationEvent extends SessionEvent { + + @Override + public String getType() { return "session.truncation"; } + + @JsonProperty("data") + private SessionTruncationEventData data; + + public SessionTruncationEventData getData() { return data; } + public void setData(SessionTruncationEventData data) { this.data = data; } + + /** Data payload for {@link SessionTruncationEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionTruncationEventData( + /** Maximum token count for the model's context window */ + @JsonProperty("tokenLimit") Double tokenLimit, + /** Total tokens in conversation messages before truncation */ + @JsonProperty("preTruncationTokensInMessages") Double preTruncationTokensInMessages, + /** Number of conversation messages before truncation */ + @JsonProperty("preTruncationMessagesLength") Double preTruncationMessagesLength, + /** Total tokens in conversation messages after truncation */ + @JsonProperty("postTruncationTokensInMessages") Double postTruncationTokensInMessages, + /** Number of conversation messages after truncation */ + @JsonProperty("postTruncationMessagesLength") Double postTruncationMessagesLength, + /** Number of tokens removed by truncation */ + @JsonProperty("tokensRemovedDuringTruncation") Double tokensRemovedDuringTruncation, + /** Number of messages removed by truncation */ + @JsonProperty("messagesRemovedDuringTruncation") Double messagesRemovedDuringTruncation, + /** Identifier of the component that performed truncation (e.g., "BasicTruncator") */ + @JsonProperty("performedBy") String performedBy + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java new file mode 100644 index 000000000..7c9c19eaf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionUsageInfoEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.usage_info". Current context window usage statistics including token and message counts + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionUsageInfoEvent extends SessionEvent { + + @Override + public String getType() { return "session.usage_info"; } + + @JsonProperty("data") + private SessionUsageInfoEventData data; + + public SessionUsageInfoEventData getData() { return data; } + public void setData(SessionUsageInfoEventData data) { this.data = data; } + + /** Data payload for {@link SessionUsageInfoEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionUsageInfoEventData( + /** Maximum token count for the model's context window */ + @JsonProperty("tokenLimit") Double tokenLimit, + /** Current number of tokens in the context window */ + @JsonProperty("currentTokens") Double currentTokens, + /** Current number of messages in the conversation */ + @JsonProperty("messagesLength") Double messagesLength, + /** Token count from system message(s) */ + @JsonProperty("systemTokens") Double systemTokens, + /** Token count from non-system messages (user, assistant, tool) */ + @JsonProperty("conversationTokens") Double conversationTokens, + /** Token count from tool definitions */ + @JsonProperty("toolDefinitionsTokens") Double toolDefinitionsTokens, + /** Whether this is the first usage_info event emitted in this session */ + @JsonProperty("isInitial") Boolean isInitial + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionWarningEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionWarningEvent.java new file mode 100644 index 000000000..fc0a0778e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionWarningEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.warning". Warning message for timeline display with categorization + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionWarningEvent extends SessionEvent { + + @Override + public String getType() { return "session.warning"; } + + @JsonProperty("data") + private SessionWarningEventData data; + + public SessionWarningEventData getData() { return data; } + public void setData(SessionWarningEventData data) { this.data = data; } + + /** Data payload for {@link SessionWarningEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionWarningEventData( + /** Category of warning (e.g., "subscription", "policy", "mcp") */ + @JsonProperty("warningType") String warningType, + /** Human-readable warning message for display in the timeline */ + @JsonProperty("message") String message, + /** Optional URL associated with this warning that the user can open in a browser */ + @JsonProperty("url") String url + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SessionWorkspaceFileChangedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SessionWorkspaceFileChangedEvent.java new file mode 100644 index 000000000..ba9032664 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SessionWorkspaceFileChangedEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.workspace_file_changed". Workspace file change details including path and operation type + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionWorkspaceFileChangedEvent extends SessionEvent { + + @Override + public String getType() { return "session.workspace_file_changed"; } + + @JsonProperty("data") + private SessionWorkspaceFileChangedEventData data; + + public SessionWorkspaceFileChangedEventData getData() { return data; } + public void setData(SessionWorkspaceFileChangedEventData data) { this.data = data; } + + /** Data payload for {@link SessionWorkspaceFileChangedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionWorkspaceFileChangedEventData( + /** Relative path within the session workspace files directory */ + @JsonProperty("path") String path, + /** Whether the file was newly created or updated */ + @JsonProperty("operation") WorkspaceFileChangedOperation operation + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java new file mode 100644 index 000000000..1afb58b69 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownCodeChanges.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Aggregate code change metrics for the session + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownCodeChanges( + /** Total number of lines added during the session */ + @JsonProperty("linesAdded") Double linesAdded, + /** Total number of lines removed during the session */ + @JsonProperty("linesRemoved") Double linesRemoved, + /** List of file paths that were modified during the session */ + @JsonProperty("filesModified") List filesModified +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java new file mode 100644 index 000000000..bc7d41d8b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetric.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ShutdownModelMetric` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetric( + /** Request count and cost metrics */ + @JsonProperty("requests") ShutdownModelMetricRequests requests, + /** Token usage breakdown */ + @JsonProperty("usage") ShutdownModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Double totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java new file mode 100644 index 000000000..1872a6603 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricRequests.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request count and cost metrics + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricRequests( + /** Total number of API requests made to this model */ + @JsonProperty("count") Double count, + /** Cumulative cost multiplier for requests to this model */ + @JsonProperty("cost") Double cost +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java new file mode 100644 index 000000000..658efe35a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ShutdownModelMetricTokenDetail` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java new file mode 100644 index 000000000..bd47eaeb5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownModelMetricUsage.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage breakdown + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownModelMetricUsage( + /** Total input tokens consumed across all requests to this model */ + @JsonProperty("inputTokens") Double inputTokens, + /** Total output tokens produced across all requests to this model */ + @JsonProperty("outputTokens") Double outputTokens, + /** Total tokens read from prompt cache across all requests */ + @JsonProperty("cacheReadTokens") Double cacheReadTokens, + /** Total tokens written to prompt cache across all requests */ + @JsonProperty("cacheWriteTokens") Double cacheWriteTokens, + /** Total reasoning tokens produced across all requests to this model */ + @JsonProperty("reasoningTokens") Double reasoningTokens +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java new file mode 100644 index 000000000..6a9b3188a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ShutdownTokenDetail` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ShutdownTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Double tokenCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownType.java b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownType.java new file mode 100644 index 000000000..fbc627df8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ShutdownType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Whether the session ended normally ("routine") or due to a crash/fatal error ("error") + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ShutdownType { + /** The {@code routine} variant. */ + ROUTINE("routine"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + ShutdownType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ShutdownType fromValue(String value) { + for (ShutdownType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ShutdownType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SkillInvokedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SkillInvokedEvent.java new file mode 100644 index 000000000..6aa238544 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SkillInvokedEvent.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "skill.invoked". Skill invocation details including content, allowed tools, and plugin metadata + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SkillInvokedEvent extends SessionEvent { + + @Override + public String getType() { return "skill.invoked"; } + + @JsonProperty("data") + private SkillInvokedEventData data; + + public SkillInvokedEventData getData() { return data; } + public void setData(SkillInvokedEventData data) { this.data = data; } + + /** Data payload for {@link SkillInvokedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SkillInvokedEventData( + /** Name of the invoked skill */ + @JsonProperty("name") String name, + /** File path to the SKILL.md definition */ + @JsonProperty("path") String path, + /** Full content of the skill file, injected into the conversation for the model */ + @JsonProperty("content") String content, + /** Tool names that should be auto-approved when this skill is active */ + @JsonProperty("allowedTools") List allowedTools, + /** Name of the plugin this skill originated from, when applicable */ + @JsonProperty("pluginName") String pluginName, + /** Version of the plugin this skill originated from, when applicable */ + @JsonProperty("pluginVersion") String pluginVersion, + /** Description of the skill from its SKILL.md frontmatter */ + @JsonProperty("description") String description + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SkillSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/SkillSource.java new file mode 100644 index 000000000..9951c1da9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SkillSource.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Source location type (e.g., project, personal-copilot, plugin, builtin) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillSource { + /** The {@code project} variant. */ + PROJECT("project"), + /** The {@code inherited} variant. */ + INHERITED("inherited"), + /** The {@code personal-copilot} variant. */ + PERSONAL_COPILOT("personal-copilot"), + /** The {@code personal-agents} variant. */ + PERSONAL_AGENTS("personal-agents"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code custom} variant. */ + CUSTOM("custom"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + SkillSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillSource fromValue(String value) { + for (SkillSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SkillsLoadedSkill.java b/java/src/generated/java/com/github/copilot/sdk/generated/SkillsLoadedSkill.java new file mode 100644 index 000000000..21f0dc0f4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SkillsLoadedSkill.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SkillsLoadedSkill` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillsLoadedSkill( + /** Unique identifier for the skill */ + @JsonProperty("name") String name, + /** Description of what the skill does */ + @JsonProperty("description") String description, + /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + @JsonProperty("source") SkillSource source, + /** Whether the skill can be invoked by the user as a slash command */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether the skill is currently enabled */ + @JsonProperty("enabled") Boolean enabled, + /** Absolute path to the skill file, if available */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java new file mode 100644 index 000000000..f61235b4a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentCompletedEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "subagent.completed". Sub-agent completion details for successful execution + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SubagentCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "subagent.completed"; } + + @JsonProperty("data") + private SubagentCompletedEventData data; + + public SubagentCompletedEventData getData() { return data; } + public void setData(SubagentCompletedEventData data) { this.data = data; } + + /** Data payload for {@link SubagentCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SubagentCompletedEventData( + /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + @JsonProperty("toolCallId") String toolCallId, + /** Internal name of the sub-agent */ + @JsonProperty("agentName") String agentName, + /** Human-readable display name of the sub-agent */ + @JsonProperty("agentDisplayName") String agentDisplayName, + /** Model used by the sub-agent */ + @JsonProperty("model") String model, + /** Total number of tool calls made by the sub-agent */ + @JsonProperty("totalToolCalls") Double totalToolCalls, + /** Total tokens (input + output) consumed by the sub-agent */ + @JsonProperty("totalTokens") Double totalTokens, + /** Wall-clock duration of the sub-agent execution in milliseconds */ + @JsonProperty("durationMs") Double durationMs + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentDeselectedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentDeselectedEvent.java new file mode 100644 index 000000000..391df6d49 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentDeselectedEvent.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "subagent.deselected". Empty payload; the event signals that the custom agent was deselected, returning to the default agent + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SubagentDeselectedEvent extends SessionEvent { + + @Override + public String getType() { return "subagent.deselected"; } + + @JsonProperty("data") + private SubagentDeselectedEventData data; + + public SubagentDeselectedEventData getData() { return data; } + public void setData(SubagentDeselectedEventData data) { this.data = data; } + + /** Data payload for {@link SubagentDeselectedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SubagentDeselectedEventData() { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java new file mode 100644 index 000000000..19644eee1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentFailedEvent.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "subagent.failed". Sub-agent failure details including error message and agent information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SubagentFailedEvent extends SessionEvent { + + @Override + public String getType() { return "subagent.failed"; } + + @JsonProperty("data") + private SubagentFailedEventData data; + + public SubagentFailedEventData getData() { return data; } + public void setData(SubagentFailedEventData data) { this.data = data; } + + /** Data payload for {@link SubagentFailedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SubagentFailedEventData( + /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + @JsonProperty("toolCallId") String toolCallId, + /** Internal name of the sub-agent */ + @JsonProperty("agentName") String agentName, + /** Human-readable display name of the sub-agent */ + @JsonProperty("agentDisplayName") String agentDisplayName, + /** Error message describing why the sub-agent failed */ + @JsonProperty("error") String error, + /** Model used by the sub-agent (if any model calls succeeded before failure) */ + @JsonProperty("model") String model, + /** Total number of tool calls made before the sub-agent failed */ + @JsonProperty("totalToolCalls") Double totalToolCalls, + /** Total tokens (input + output) consumed before the sub-agent failed */ + @JsonProperty("totalTokens") Double totalTokens, + /** Wall-clock duration of the sub-agent execution in milliseconds */ + @JsonProperty("durationMs") Double durationMs + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentSelectedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentSelectedEvent.java new file mode 100644 index 000000000..b342e4230 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentSelectedEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "subagent.selected". Custom agent selection details including name and available tools + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SubagentSelectedEvent extends SessionEvent { + + @Override + public String getType() { return "subagent.selected"; } + + @JsonProperty("data") + private SubagentSelectedEventData data; + + public SubagentSelectedEventData getData() { return data; } + public void setData(SubagentSelectedEventData data) { this.data = data; } + + /** Data payload for {@link SubagentSelectedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SubagentSelectedEventData( + /** Internal name of the selected custom agent */ + @JsonProperty("agentName") String agentName, + /** Human-readable display name of the selected custom agent */ + @JsonProperty("agentDisplayName") String agentDisplayName, + /** List of tool names available to this agent, or null for all tools */ + @JsonProperty("tools") List tools + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java new file mode 100644 index 000000000..737d773f1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SubagentStartedEvent.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "subagent.started". Sub-agent startup details including parent tool call and agent information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SubagentStartedEvent extends SessionEvent { + + @Override + public String getType() { return "subagent.started"; } + + @JsonProperty("data") + private SubagentStartedEventData data; + + public SubagentStartedEventData getData() { return data; } + public void setData(SubagentStartedEventData data) { this.data = data; } + + /** Data payload for {@link SubagentStartedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SubagentStartedEventData( + /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + @JsonProperty("toolCallId") String toolCallId, + /** Internal name of the sub-agent */ + @JsonProperty("agentName") String agentName, + /** Human-readable display name of the sub-agent */ + @JsonProperty("agentDisplayName") String agentDisplayName, + /** Description of what the sub-agent does */ + @JsonProperty("agentDescription") String agentDescription, + /** Model the sub-agent will run with, when known at start. Surfaced in the timeline for auto-selected sub-agents (e.g. rubber-duck). */ + @JsonProperty("model") String model + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageEvent.java new file mode 100644 index 000000000..82976c004 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageEvent.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "system.message". System/developer instruction content with role and optional template metadata + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemMessageEvent extends SessionEvent { + + @Override + public String getType() { return "system.message"; } + + @JsonProperty("data") + private SystemMessageEventData data; + + public SystemMessageEventData getData() { return data; } + public void setData(SystemMessageEventData data) { this.data = data; } + + /** Data payload for {@link SystemMessageEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SystemMessageEventData( + /** The system or developer prompt text sent as model input */ + @JsonProperty("content") String content, + /** Message role: "system" for system prompts, "developer" for developer-injected instructions */ + @JsonProperty("role") SystemMessageRole role, + /** Optional name identifier for the message source */ + @JsonProperty("name") String name, + /** Metadata about the prompt template and its construction */ + @JsonProperty("metadata") SystemMessageMetadata metadata + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageMetadata.java b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageMetadata.java new file mode 100644 index 000000000..2b054dc94 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageMetadata.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Metadata about the prompt template and its construction + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SystemMessageMetadata( + /** Version identifier of the prompt template used */ + @JsonProperty("promptVersion") String promptVersion, + /** Template variables used when constructing the prompt */ + @JsonProperty("variables") Map variables +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageRole.java b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageRole.java new file mode 100644 index 000000000..921b69ec0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SystemMessageRole.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Message role: "system" for system prompts, "developer" for developer-injected instructions + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SystemMessageRole { + /** The {@code system} variant. */ + SYSTEM("system"), + /** The {@code developer} variant. */ + DEVELOPER("developer"); + + private final String value; + SystemMessageRole(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SystemMessageRole fromValue(String value) { + for (SystemMessageRole v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SystemMessageRole value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/SystemNotificationEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/SystemNotificationEvent.java new file mode 100644 index 000000000..ba11910e0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/SystemNotificationEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "system.notification". System-generated notification for runtime events like background task completion + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationEvent extends SessionEvent { + + @Override + public String getType() { return "system.notification"; } + + @JsonProperty("data") + private SystemNotificationEventData data; + + public SystemNotificationEventData getData() { return data; } + public void setData(SystemNotificationEventData data) { this.data = data; } + + /** Data payload for {@link SystemNotificationEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SystemNotificationEventData( + /** The notification text, typically wrapped in XML tags */ + @JsonProperty("content") String content, + /** Structured metadata identifying what triggered this notification */ + @JsonProperty("kind") Object kind + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteError.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteError.java new file mode 100644 index 000000000..dbdc99ba7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteError.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Error details when the tool execution failed + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionCompleteError( + /** Human-readable error message */ + @JsonProperty("message") String message, + /** Machine-readable error code */ + @JsonProperty("code") String code +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java new file mode 100644 index 000000000..b4ac9b799 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteEvent.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.execution_complete". Tool execution completion results including success status, detailed output, and error information + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionCompleteEvent extends SessionEvent { + + @Override + public String getType() { return "tool.execution_complete"; } + + @JsonProperty("data") + private ToolExecutionCompleteEventData data; + + public ToolExecutionCompleteEventData getData() { return data; } + public void setData(ToolExecutionCompleteEventData data) { this.data = data; } + + /** Data payload for {@link ToolExecutionCompleteEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolExecutionCompleteEventData( + /** Unique identifier for the completed tool call */ + @JsonProperty("toolCallId") String toolCallId, + /** Whether the tool execution completed successfully */ + @JsonProperty("success") Boolean success, + /** Model identifier that generated this tool call */ + @JsonProperty("model") String model, + /** CAPI interaction ID for correlating this tool execution with upstream telemetry */ + @JsonProperty("interactionId") String interactionId, + /** Whether this tool call was explicitly requested by the user rather than the assistant */ + @JsonProperty("isUserRequested") Boolean isUserRequested, + /** Tool execution result on success */ + @JsonProperty("result") ToolExecutionCompleteResult result, + /** Error details when the tool execution failed */ + @JsonProperty("error") ToolExecutionCompleteError error, + /** Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ + @JsonProperty("toolTelemetry") Map toolTelemetry, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, + /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteResult.java new file mode 100644 index 000000000..8f2830541 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionCompleteResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Tool execution result on success + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionCompleteResult( + /** Concise tool result text sent to the LLM for chat completion, potentially truncated for token efficiency */ + @JsonProperty("content") String content, + /** Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. */ + @JsonProperty("detailedContent") String detailedContent, + /** Structured content blocks (text, images, audio, resources) returned by the tool in their native format */ + @JsonProperty("contents") List contents +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionPartialResultEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionPartialResultEvent.java new file mode 100644 index 000000000..e548cc1b0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionPartialResultEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionPartialResultEvent extends SessionEvent { + + @Override + public String getType() { return "tool.execution_partial_result"; } + + @JsonProperty("data") + private ToolExecutionPartialResultEventData data; + + public ToolExecutionPartialResultEventData getData() { return data; } + public void setData(ToolExecutionPartialResultEventData data) { this.data = data; } + + /** Data payload for {@link ToolExecutionPartialResultEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolExecutionPartialResultEventData( + /** Tool call ID this partial result belongs to */ + @JsonProperty("toolCallId") String toolCallId, + /** Incremental output chunk from the running tool */ + @JsonProperty("partialOutput") String partialOutput + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionProgressEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionProgressEvent.java new file mode 100644 index 000000000..d2b20312a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionProgressEvent.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.execution_progress". Tool execution progress notification with status message + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionProgressEvent extends SessionEvent { + + @Override + public String getType() { return "tool.execution_progress"; } + + @JsonProperty("data") + private ToolExecutionProgressEventData data; + + public ToolExecutionProgressEventData getData() { return data; } + public void setData(ToolExecutionProgressEventData data) { this.data = data; } + + /** Data payload for {@link ToolExecutionProgressEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolExecutionProgressEventData( + /** Tool call ID this progress notification belongs to */ + @JsonProperty("toolCallId") String toolCallId, + /** Human-readable progress status message (e.g., from an MCP server) */ + @JsonProperty("progressMessage") String progressMessage + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java new file mode 100644 index 000000000..a98f7dec3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolExecutionStartEvent.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.execution_start". Tool execution startup details including MCP server information when applicable + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionStartEvent extends SessionEvent { + + @Override + public String getType() { return "tool.execution_start"; } + + @JsonProperty("data") + private ToolExecutionStartEventData data; + + public ToolExecutionStartEventData getData() { return data; } + public void setData(ToolExecutionStartEventData data) { this.data = data; } + + /** Data payload for {@link ToolExecutionStartEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolExecutionStartEventData( + /** Unique identifier for this tool call */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the tool being executed */ + @JsonProperty("toolName") String toolName, + /** Arguments passed to the tool */ + @JsonProperty("arguments") Object arguments, + /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ + @JsonProperty("mcpServerName") String mcpServerName, + /** Original tool name on the MCP server, when the tool is an MCP tool */ + @JsonProperty("mcpToolName") String mcpToolName, + /** Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ + @JsonProperty("turnId") String turnId, + /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ + @JsonProperty("parentToolCallId") String parentToolCallId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/ToolUserRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/ToolUserRequestedEvent.java new file mode 100644 index 000000000..ffd69535d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/ToolUserRequestedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "tool.user_requested". User-initiated tool invocation request with tool name and arguments + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolUserRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "tool.user_requested"; } + + @JsonProperty("data") + private ToolUserRequestedEventData data; + + public ToolUserRequestedEventData getData() { return data; } + public void setData(ToolUserRequestedEventData data) { this.data = data; } + + /** Data payload for {@link ToolUserRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolUserRequestedEventData( + /** Unique identifier for this tool call */ + @JsonProperty("toolCallId") String toolCallId, + /** Name of the tool the user wants to invoke */ + @JsonProperty("toolName") String toolName, + /** Arguments for the tool invocation */ + @JsonProperty("arguments") Object arguments + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UnknownSessionEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/UnknownSessionEvent.java new file mode 100644 index 000000000..cf56b4b4f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UnknownSessionEvent.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Fallback for event types not yet known to this SDK version. + *

+ * {@link #getType()} returns the original type string from the JSON payload, + * preserving forward compatibility with event types introduced by newer CLI versions. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class UnknownSessionEvent extends SessionEvent { + + @JsonProperty("type") + private String type = "unknown"; + + @Override + public String getType() { return type; } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UserInputCompletedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/UserInputCompletedEvent.java new file mode 100644 index 000000000..66883aa62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UserInputCompletedEvent.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "user_input.completed". User input request completion with the user's response + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class UserInputCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "user_input.completed"; } + + @JsonProperty("data") + private UserInputCompletedEventData data; + + public UserInputCompletedEventData getData() { return data; } + public void setData(UserInputCompletedEventData data) { this.data = data; } + + /** Data payload for {@link UserInputCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record UserInputCompletedEventData( + /** Request ID of the resolved user input request; clients should dismiss any UI for this request */ + @JsonProperty("requestId") String requestId, + /** The user's answer to the input request */ + @JsonProperty("answer") String answer, + /** Whether the answer was typed as free-form text rather than selected from choices */ + @JsonProperty("wasFreeform") Boolean wasFreeform + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UserInputRequestedEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/UserInputRequestedEvent.java new file mode 100644 index 000000000..bb903191b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UserInputRequestedEvent.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "user_input.requested". User input request notification with question and optional predefined choices + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class UserInputRequestedEvent extends SessionEvent { + + @Override + public String getType() { return "user_input.requested"; } + + @JsonProperty("data") + private UserInputRequestedEventData data; + + public UserInputRequestedEventData getData() { return data; } + public void setData(UserInputRequestedEventData data) { this.data = data; } + + /** Data payload for {@link UserInputRequestedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record UserInputRequestedEventData( + /** Unique identifier for this input request; used to respond via session.respondToUserInput() */ + @JsonProperty("requestId") String requestId, + /** The question or prompt to present to the user */ + @JsonProperty("question") String question, + /** Predefined choices for the user to select from, if applicable */ + @JsonProperty("choices") List choices, + /** Whether the user can provide a free-form text response in addition to predefined choices */ + @JsonProperty("allowFreeform") Boolean allowFreeform, + /** The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses */ + @JsonProperty("toolCallId") String toolCallId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageAgentMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageAgentMode.java new file mode 100644 index 000000000..6c1710602 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageAgentMode.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * The agent mode that was active when this message was sent + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UserMessageAgentMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"), + /** The {@code shell} variant. */ + SHELL("shell"); + + private final String value; + UserMessageAgentMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UserMessageAgentMode fromValue(String value) { + for (UserMessageAgentMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UserMessageAgentMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java new file mode 100644 index 000000000..bf30633b0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/UserMessageEvent.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Session event "user.message". + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class UserMessageEvent extends SessionEvent { + + @Override + public String getType() { return "user.message"; } + + @JsonProperty("data") + private UserMessageEventData data; + + public UserMessageEventData getData() { return data; } + public void setData(UserMessageEventData data) { this.data = data; } + + /** Data payload for {@link UserMessageEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record UserMessageEventData( + /** The user's message text as displayed in the timeline */ + @JsonProperty("content") String content, + /** Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching */ + @JsonProperty("transformedContent") String transformedContent, + /** Files, selections, or GitHub references attached to the message */ + @JsonProperty("attachments") List attachments, + /** Normalized document MIME types that were sent natively instead of through tagged_files XML */ + @JsonProperty("supportedNativeDocumentMimeTypes") List supportedNativeDocumentMimeTypes, + /** Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit */ + @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, + /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ + @JsonProperty("source") String source, + /** The agent mode that was active when this message was sent */ + @JsonProperty("agentMode") UserMessageAgentMode agentMode, + /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ + @JsonProperty("isAutopilotContinuation") Boolean isAutopilotContinuation, + /** CAPI interaction ID for correlating this user message with its turn */ + @JsonProperty("interactionId") String interactionId, + /** Parent agent task ID for background telemetry correlated to this user turn */ + @JsonProperty("parentAgentTaskId") String parentAgentTaskId + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContext.java b/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContext.java new file mode 100644 index 000000000..b023859e2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContext.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Working directory and git context at session start + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkingDirectoryContext( + /** Current working directory path */ + @JsonProperty("cwd") String cwd, + /** Root directory of the git repository, resolved via git rev-parse */ + @JsonProperty("gitRoot") String gitRoot, + /** Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ + @JsonProperty("repository") String repository, + /** Hosting platform type of the repository (github or ado) */ + @JsonProperty("hostType") WorkingDirectoryContextHostType hostType, + /** Raw host string from the git remote URL (e.g. "github.com", "mycompany.ghe.com", "dev.azure.com") */ + @JsonProperty("repositoryHost") String repositoryHost, + /** Current git branch name */ + @JsonProperty("branch") String branch, + /** Head commit of current git branch at session start time */ + @JsonProperty("headCommit") String headCommit, + /** Base commit of current git branch at session start time */ + @JsonProperty("baseCommit") String baseCommit +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContextHostType.java b/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContextHostType.java new file mode 100644 index 000000000..4786b8bc0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/WorkingDirectoryContextHostType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Hosting platform type of the repository (github or ado) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkingDirectoryContextHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + WorkingDirectoryContextHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkingDirectoryContextHostType fromValue(String value) { + for (WorkingDirectoryContextHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkingDirectoryContextHostType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/WorkspaceFileChangedOperation.java b/java/src/generated/java/com/github/copilot/sdk/generated/WorkspaceFileChangedOperation.java new file mode 100644 index 000000000..7e21ec548 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/WorkspaceFileChangedOperation.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.sdk.generated; + +import javax.annotation.processing.Generated; + +/** + * Whether the file was newly created or updated + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkspaceFileChangedOperation { + /** The {@code create} variant. */ + CREATE("create"), + /** The {@code update} variant. */ + UPDATE("update"); + + private final String value; + WorkspaceFileChangedOperation(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkspaceFileChangedOperation fromValue(String value) { + for (WorkspaceFileChangedOperation v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkspaceFileChangedOperation value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountGetQuotaResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountGetQuotaResult.java new file mode 100644 index 000000000..468519623 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountGetQuotaResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Quota usage snapshots for the resolved user, keyed by quota type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AccountGetQuotaResult( + /** Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) */ + @JsonProperty("quotaSnapshots") Map quotaSnapshots +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java new file mode 100644 index 000000000..2bd1ac2f6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AccountQuotaSnapshot.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Schema for the `AccountQuotaSnapshot` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AccountQuotaSnapshot( + /** Whether the user has an unlimited usage entitlement */ + @JsonProperty("isUnlimitedEntitlement") Boolean isUnlimitedEntitlement, + /** Number of requests included in the entitlement, or -1 for unlimited entitlements */ + @JsonProperty("entitlementRequests") Long entitlementRequests, + /** Number of requests used so far this period */ + @JsonProperty("usedRequests") Long usedRequests, + /** Whether usage is still permitted after quota exhaustion */ + @JsonProperty("usageAllowedWithExhaustedQuota") Boolean usageAllowedWithExhaustedQuota, + /** Percentage of entitlement remaining */ + @JsonProperty("remainingPercentage") Double remainingPercentage, + /** Number of overage requests made this period */ + @JsonProperty("overage") Double overage, + /** Whether overage is allowed when quota is exhausted */ + @JsonProperty("overageAllowedWithExhaustedQuota") Boolean overageAllowedWithExhaustedQuota, + /** Date when the quota resets (ISO 8601 string) */ + @JsonProperty("resetDate") OffsetDateTime resetDate +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java new file mode 100644 index 000000000..66493bce2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AgentInfo.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `AgentInfo` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AgentInfo( + /** Unique identifier of the custom agent */ + @JsonProperty("name") String name, + /** Human-readable display name */ + @JsonProperty("displayName") String displayName, + /** Description of the agent's purpose */ + @JsonProperty("description") String description, + /** Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AuthInfoType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AuthInfoType.java new file mode 100644 index 000000000..89c5db85c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/AuthInfoType.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Authentication type + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AuthInfoType { + /** The {@code hmac} variant. */ + HMAC("hmac"), + /** The {@code env} variant. */ + ENV("env"), + /** The {@code user} variant. */ + USER("user"), + /** The {@code gh-cli} variant. */ + GH_CLI("gh-cli"), + /** The {@code api-key} variant. */ + API_KEY("api-key"), + /** The {@code token} variant. */ + TOKEN("token"), + /** The {@code copilot-api-token} variant. */ + COPILOT_API_TOKEN("copilot-api-token"); + + private final String value; + AuthInfoType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AuthInfoType fromValue(String value) { + for (AuthInfoType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AuthInfoType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java new file mode 100644 index 000000000..b22245ed9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional connection token presented by the SDK client during the handshake. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectParams( + /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ + @JsonProperty("token") String token +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java new file mode 100644 index 000000000..f51b1a9c3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Handshake result reporting the server's protocol version and package version on success. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectResult( + /** Always true on success */ + @JsonProperty("ok") Boolean ok, + /** Server protocol version number */ + @JsonProperty("protocolVersion") Long protocolVersion, + /** Server package version */ + @JsonProperty("version") String version +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadata.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadata.java new file mode 100644 index 000000000..006b750c1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadata.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Metadata for a connected remote session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectedRemoteSessionMetadata( + /** SDK session ID for the connected remote session. */ + @JsonProperty("sessionId") String sessionId, + /** Optional friendly session name. */ + @JsonProperty("name") String name, + /** Optional session summary. */ + @JsonProperty("summary") String summary, + /** Session start time as an ISO 8601 string. */ + @JsonProperty("startTime") OffsetDateTime startTime, + /** Last session update time as an ISO 8601 string. */ + @JsonProperty("modifiedTime") OffsetDateTime modifiedTime, + /** Repository associated with the connected remote session. */ + @JsonProperty("repository") ConnectedRemoteSessionMetadataRepository repository, + /** Pull request number associated with the session. */ + @JsonProperty("pullRequestNumber") Long pullRequestNumber, + /** Original remote resource identifier. */ + @JsonProperty("resourceId") String resourceId, + /** Neutral SDK discriminator for the connected remote session kind. */ + @JsonProperty("kind") ConnectedRemoteSessionMetadataKind kind, + /** Remote session staleness deadline as an ISO 8601 string. */ + @JsonProperty("staleAt") OffsetDateTime staleAt, + /** Remote session state returned by the backing service. */ + @JsonProperty("state") String state +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataKind.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataKind.java new file mode 100644 index 000000000..14e4e4b22 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Neutral SDK discriminator for the connected remote session kind. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ConnectedRemoteSessionMetadataKind { + /** The {@code remote-session} variant. */ + REMOTE_SESSION("remote-session"), + /** The {@code coding-agent} variant. */ + CODING_AGENT("coding-agent"); + + private final String value; + ConnectedRemoteSessionMetadataKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ConnectedRemoteSessionMetadataKind fromValue(String value) { + for (ConnectedRemoteSessionMetadataKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ConnectedRemoteSessionMetadataKind value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataRepository.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataRepository.java new file mode 100644 index 000000000..562dfba8a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ConnectedRemoteSessionMetadataRepository.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Repository associated with the connected remote session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectedRemoteSessionMetadataRepository( + /** Repository owner or organization login. */ + @JsonProperty("owner") String owner, + /** Repository name. */ + @JsonProperty("name") String name, + /** Branch associated with the remote session. */ + @JsonProperty("branch") String branch +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServer.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServer.java new file mode 100644 index 000000000..2cfe49334 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServer.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `DiscoveredMcpServer` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredMcpServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Server transport type: stdio, http, sse, or memory */ + @JsonProperty("type") DiscoveredMcpServerType type, + /** Configuration source: user, workspace, plugin, or builtin */ + @JsonProperty("source") McpServerSource source, + /** Whether the server is enabled (not in the disabled list) */ + @JsonProperty("enabled") Boolean enabled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerSource.java new file mode 100644 index 000000000..6bc451b34 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Configuration source + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum DiscoveredMcpServerSource { + /** The {@code user} variant. */ + USER("user"), + /** The {@code workspace} variant. */ + WORKSPACE("workspace"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + DiscoveredMcpServerSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static DiscoveredMcpServerSource fromValue(String value) { + for (DiscoveredMcpServerSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown DiscoveredMcpServerSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerType.java new file mode 100644 index 000000000..0d4293d9a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/DiscoveredMcpServerType.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Server transport type: stdio, http, sse, or memory + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum DiscoveredMcpServerType { + /** The {@code stdio} variant. */ + STDIO("stdio"), + /** The {@code http} variant. */ + HTTP("http"), + /** The {@code sse} variant. */ + SSE("sse"), + /** The {@code memory} variant. */ + MEMORY("memory"); + + private final String value; + DiscoveredMcpServerType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static DiscoveredMcpServerType fromValue(String value) { + for (DiscoveredMcpServerType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown DiscoveredMcpServerType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Extension.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Extension.java new file mode 100644 index 000000000..7784fa5a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Extension.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `Extension` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Extension( + /** Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper') */ + @JsonProperty("id") String id, + /** Extension name (directory name) */ + @JsonProperty("name") String name, + /** Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) */ + @JsonProperty("source") ExtensionSource source, + /** Current status: running, disabled, failed, or starting */ + @JsonProperty("status") ExtensionStatus status, + /** Process ID if the extension is running */ + @JsonProperty("pid") Long pid +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionSource.java new file mode 100644 index 000000000..2ec3da397 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionSource.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ExtensionSource { + /** The {@code project} variant. */ + PROJECT("project"), + /** The {@code user} variant. */ + USER("user"); + + private final String value; + ExtensionSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ExtensionSource fromValue(String value) { + for (ExtensionSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ExtensionSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionStatus.java new file mode 100644 index 000000000..241a5cd60 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ExtensionStatus.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Current status: running, disabled, failed, or starting + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ExtensionStatus { + /** The {@code running} variant. */ + RUNNING("running"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code starting} variant. */ + STARTING("starting"); + + private final String value; + ExtensionStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ExtensionStatus fromValue(String value) { + for (ExtensionStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ExtensionStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/HistoryCompactContextWindow.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/HistoryCompactContextWindow.java new file mode 100644 index 000000000..4ac1a8fa9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/HistoryCompactContextWindow.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Post-compaction context window usage breakdown + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HistoryCompactContextWindow( + /** Maximum token count for the model's context window */ + @JsonProperty("tokenLimit") Long tokenLimit, + /** Current total tokens in the context window (system + conversation + tool definitions) */ + @JsonProperty("currentTokens") Long currentTokens, + /** Current number of messages in the conversation */ + @JsonProperty("messagesLength") Long messagesLength, + /** Token count from system message(s) */ + @JsonProperty("systemTokens") Long systemTokens, + /** Token count from non-system messages (user, assistant, tool) */ + @JsonProperty("conversationTokens") Long conversationTokens, + /** Token count from tool definitions */ + @JsonProperty("toolDefinitionsTokens") Long toolDefinitionsTokens +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java new file mode 100644 index 000000000..d0a23b337 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSources.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `InstructionsSources` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record InstructionsSources( + /** Unique identifier for this source (used for toggling) */ + @JsonProperty("id") String id, + /** Human-readable label */ + @JsonProperty("label") String label, + /** File path relative to repo or absolute for home */ + @JsonProperty("sourcePath") String sourcePath, + /** Raw content of the instruction file */ + @JsonProperty("content") String content, + /** Category of instruction source β€” used for merge logic */ + @JsonProperty("type") InstructionsSourcesType type, + /** Where this source lives β€” used for UI grouping */ + @JsonProperty("location") InstructionsSourcesLocation location, + /** Glob pattern from frontmatter β€” when set, this instruction applies only to matching files */ + @JsonProperty("applyTo") String applyTo, + /** Short description (body after frontmatter) for use in instruction tables */ + @JsonProperty("description") String description +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java new file mode 100644 index 000000000..01b702cfe --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesLocation.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Where this source lives β€” used for UI grouping + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum InstructionsSourcesLocation { + /** The {@code user} variant. */ + USER("user"), + /** The {@code repository} variant. */ + REPOSITORY("repository"), + /** The {@code working-directory} variant. */ + WORKING_DIRECTORY("working-directory"); + + private final String value; + InstructionsSourcesLocation(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static InstructionsSourcesLocation fromValue(String value) { + for (InstructionsSourcesLocation v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown InstructionsSourcesLocation value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java new file mode 100644 index 000000000..8de131942 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/InstructionsSourcesType.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Category of instruction source β€” used for merge logic + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum InstructionsSourcesType { + /** The {@code home} variant. */ + HOME("home"), + /** The {@code repo} variant. */ + REPO("repo"), + /** The {@code model} variant. */ + MODEL("model"), + /** The {@code vscode} variant. */ + VSCODE("vscode"), + /** The {@code nested-agents} variant. */ + NESTED_AGENTS("nested-agents"), + /** The {@code child-instructions} variant. */ + CHILD_INSTRUCTIONS("child-instructions"); + + private final String value; + InstructionsSourcesType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static InstructionsSourcesType fromValue(String value) { + for (InstructionsSourcesType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown InstructionsSourcesType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigAddParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigAddParams.java new file mode 100644 index 000000000..a865e900e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigAddParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * MCP server name and configuration to add to user configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigAddParams( + /** Unique name for the MCP server */ + @JsonProperty("name") String name, + /** MCP server configuration (stdio process or remote HTTP/SSE) */ + @JsonProperty("config") Object config +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigDisableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigDisableParams.java new file mode 100644 index 000000000..49664f8e8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigDisableParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * MCP server names to disable for new sessions. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigDisableParams( + /** Names of MCP servers to disable. Each server is added to the persisted disabled list so new sessions skip it. Already-disabled names are ignored. Active sessions keep their current connections until they end. */ + @JsonProperty("names") List names +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigEnableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigEnableParams.java new file mode 100644 index 000000000..4bf628465 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigEnableParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * MCP server names to enable for new sessions. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigEnableParams( + /** Names of MCP servers to enable. Each server is removed from the persisted disabled list so new sessions spawn it. Unknown or already-enabled names are ignored. */ + @JsonProperty("names") List names +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigListResult.java new file mode 100644 index 000000000..f2be8641a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * User-configured MCP servers, keyed by server name. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigListResult( + /** All MCP servers from user config, keyed by name */ + @JsonProperty("servers") Map servers +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigRemoveParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigRemoveParams.java new file mode 100644 index 000000000..9e87063c4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigRemoveParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * MCP server name to remove from user configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigRemoveParams( + /** Name of the MCP server to remove */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigUpdateParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigUpdateParams.java new file mode 100644 index 000000000..8c2df6072 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpConfigUpdateParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * MCP server name and replacement configuration to write to user configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfigUpdateParams( + /** Name of the MCP server to update */ + @JsonProperty("name") String name, + /** MCP server configuration (stdio process or remote HTTP/SSE) */ + @JsonProperty("config") Object config +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverParams.java new file mode 100644 index 000000000..29b386052 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional working directory used as context for MCP server discovery. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpDiscoverParams( + /** Working directory used as context for discovery (e.g., plugin resolution) */ + @JsonProperty("workingDirectory") String workingDirectory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverResult.java new file mode 100644 index 000000000..074aedc61 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpDiscoverResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * MCP servers discovered from user, workspace, plugin, and built-in sources. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpDiscoverResult( + /** MCP servers discovered from all sources */ + @JsonProperty("servers") List servers +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServer.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServer.java new file mode 100644 index 000000000..e8b71f7e8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServer.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `McpServer` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Connection status: connected, failed, needs-auth, pending, disabled, or not_configured */ + @JsonProperty("status") McpServerStatus status, + /** Configuration source: user, workspace, plugin, or builtin */ + @JsonProperty("source") McpServerSource source, + /** Error message if the server failed to connect */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerSource.java new file mode 100644 index 000000000..3ffe4b797 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Configuration source: user, workspace, plugin, or builtin + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServerSource { + /** The {@code user} variant. */ + USER("user"), + /** The {@code workspace} variant. */ + WORKSPACE("workspace"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + McpServerSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServerSource fromValue(String value) { + for (McpServerSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServerSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerStatus.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerStatus.java new file mode 100644 index 000000000..06bec4f30 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/McpServerStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Connection status: connected, failed, needs-auth, pending, disabled, or not_configured + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpServerStatus { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code needs-auth} variant. */ + NEEDS_AUTH("needs-auth"), + /** The {@code pending} variant. */ + PENDING("pending"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code not_configured} variant. */ + NOT_CONFIGURED("not_configured"); + + private final String value; + McpServerStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpServerStatus fromValue(String value) { + for (McpServerStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpServerStatus value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java new file mode 100644 index 000000000..1a808911c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Model.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `Model` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Model( + /** Model identifier (e.g., "claude-sonnet-4.5") */ + @JsonProperty("id") String id, + /** Display name */ + @JsonProperty("name") String name, + /** Model capabilities and limits */ + @JsonProperty("capabilities") ModelCapabilities capabilities, + /** Policy state (if applicable) */ + @JsonProperty("policy") ModelPolicy policy, + /** Billing information */ + @JsonProperty("billing") ModelBilling billing, + /** Supported reasoning effort levels (only present if model supports reasoning effort) */ + @JsonProperty("supportedReasoningEfforts") List supportedReasoningEfforts, + /** Default reasoning effort level (only present if model supports reasoning effort) */ + @JsonProperty("defaultReasoningEffort") String defaultReasoningEffort, + /** Model capability category for grouping in the model picker */ + @JsonProperty("modelPickerCategory") ModelPickerCategory modelPickerCategory, + /** Relative cost tier for token-based billing users */ + @JsonProperty("modelPickerPriceCategory") ModelPickerPriceCategory modelPickerPriceCategory +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java new file mode 100644 index 000000000..9e634bb79 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBilling.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Billing information + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelBilling( + /** Billing cost multiplier relative to the base rate */ + @JsonProperty("multiplier") Double multiplier, + /** Token-level pricing information for this model */ + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java new file mode 100644 index 000000000..34005daf1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelBillingTokenPrices.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token-level pricing information for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelBillingTokenPrices( + /** Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("inputPrice") Long inputPrice, + /** Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("outputPrice") Long outputPrice, + /** Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) */ + @JsonProperty("cachePrice") Long cachePrice, + /** Number of tokens per standard billing batch */ + @JsonProperty("batchSize") Long batchSize +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilities.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilities.java new file mode 100644 index 000000000..4c6b5e3af --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilities.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Model capabilities and limits + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilities( + /** Feature flags indicating what the model supports */ + @JsonProperty("supports") ModelCapabilitiesSupports supports, + /** Token limits for prompts, outputs, and context window */ + @JsonProperty("limits") ModelCapabilitiesLimits limits +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimits.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimits.java new file mode 100644 index 000000000..8adf6812b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimits.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token limits for prompts, outputs, and context window + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesLimits( + /** Maximum number of prompt/input tokens */ + @JsonProperty("max_prompt_tokens") Long maxPromptTokens, + /** Maximum number of output/completion tokens */ + @JsonProperty("max_output_tokens") Long maxOutputTokens, + /** Maximum total context window size in tokens */ + @JsonProperty("max_context_window_tokens") Long maxContextWindowTokens, + /** Vision-specific limits */ + @JsonProperty("vision") ModelCapabilitiesLimitsVision vision +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimitsVision.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimitsVision.java new file mode 100644 index 000000000..cbfc7c3b8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesLimitsVision.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Vision-specific limits + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesLimitsVision( + /** MIME types the model accepts */ + @JsonProperty("supported_media_types") List supportedMediaTypes, + /** Maximum number of images per prompt */ + @JsonProperty("max_prompt_images") Long maxPromptImages, + /** Maximum image size in bytes */ + @JsonProperty("max_prompt_image_size") Long maxPromptImageSize +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverride.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverride.java new file mode 100644 index 000000000..1ec67824e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverride.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Override individual model capabilities resolved by the runtime + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesOverride( + /** Feature flags indicating what the model supports */ + @JsonProperty("supports") ModelCapabilitiesOverrideSupports supports, + /** Token limits for prompts, outputs, and context window */ + @JsonProperty("limits") ModelCapabilitiesOverrideLimits limits +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimits.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimits.java new file mode 100644 index 000000000..f5b0b4e5e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimits.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token limits for prompts, outputs, and context window + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesOverrideLimits( + /** Maximum number of prompt/input tokens */ + @JsonProperty("max_prompt_tokens") Long maxPromptTokens, + /** Maximum number of output/completion tokens */ + @JsonProperty("max_output_tokens") Long maxOutputTokens, + /** Maximum total context window size in tokens */ + @JsonProperty("max_context_window_tokens") Long maxContextWindowTokens, + /** Vision-specific limits */ + @JsonProperty("vision") ModelCapabilitiesOverrideLimitsVision vision +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimitsVision.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimitsVision.java new file mode 100644 index 000000000..0d53e8532 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideLimitsVision.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Vision-specific limits + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesOverrideLimitsVision( + /** MIME types the model accepts */ + @JsonProperty("supported_media_types") List supportedMediaTypes, + /** Maximum number of images per prompt */ + @JsonProperty("max_prompt_images") Long maxPromptImages, + /** Maximum image size in bytes */ + @JsonProperty("max_prompt_image_size") Long maxPromptImageSize +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideSupports.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideSupports.java new file mode 100644 index 000000000..23304c82d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesOverrideSupports.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Feature flags indicating what the model supports + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesOverrideSupports( + /** Whether this model supports vision/image input */ + @JsonProperty("vision") Boolean vision, + /** Whether this model supports reasoning effort configuration */ + @JsonProperty("reasoningEffort") Boolean reasoningEffort +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesSupports.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesSupports.java new file mode 100644 index 000000000..f898f130f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelCapabilitiesSupports.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Feature flags indicating what the model supports + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelCapabilitiesSupports( + /** Whether this model supports vision/image input */ + @JsonProperty("vision") Boolean vision, + /** Whether this model supports reasoning effort configuration */ + @JsonProperty("reasoningEffort") Boolean reasoningEffort +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java new file mode 100644 index 000000000..ba0bdddfd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerCategory.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Model capability category for grouping in the model picker + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelPickerCategory { + /** The {@code lightweight} variant. */ + LIGHTWEIGHT("lightweight"), + /** The {@code versatile} variant. */ + VERSATILE("versatile"), + /** The {@code powerful} variant. */ + POWERFUL("powerful"); + + private final String value; + ModelPickerCategory(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelPickerCategory fromValue(String value) { + for (ModelPickerCategory v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelPickerCategory value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java new file mode 100644 index 000000000..cf722e496 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPickerPriceCategory.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Relative cost tier for token-based billing users + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelPickerPriceCategory { + /** The {@code low} variant. */ + LOW("low"), + /** The {@code medium} variant. */ + MEDIUM("medium"), + /** The {@code high} variant. */ + HIGH("high"), + /** The {@code very_high} variant. */ + VERY_HIGH("very_high"); + + private final String value; + ModelPickerPriceCategory(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelPickerPriceCategory fromValue(String value) { + for (ModelPickerPriceCategory v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelPickerPriceCategory value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicy.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicy.java new file mode 100644 index 000000000..a6cdeb5d5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicy.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Policy state (if applicable) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelPolicy( + /** Current policy state for this model */ + @JsonProperty("state") ModelPolicyState state, + /** Usage terms or conditions for this model */ + @JsonProperty("terms") String terms +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicyState.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicyState.java new file mode 100644 index 000000000..1673080d0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelPolicyState.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Current policy state for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelPolicyState { + /** The {@code enabled} variant. */ + ENABLED("enabled"), + /** The {@code disabled} variant. */ + DISABLED("disabled"), + /** The {@code unconfigured} variant. */ + UNCONFIGURED("unconfigured"); + + private final String value; + ModelPolicyState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelPolicyState fromValue(String value) { + for (ModelPolicyState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelPolicyState value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelsListResult.java new file mode 100644 index 000000000..db9dd791a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ModelsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * List of Copilot models available to the resolved user, including capabilities and billing metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelsListResult( + /** List of available models with full metadata */ + @JsonProperty("models") List models +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingParams.java new file mode 100644 index 000000000..564478b17 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional message to echo back to the caller. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PingParams( + /** Optional message to echo back */ + @JsonProperty("message") String message +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java new file mode 100644 index 000000000..299d8f358 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/PingResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Server liveness response, including the echoed message, current timestamp, and protocol version. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record PingResult( + /** Echoed message (or default greeting) */ + @JsonProperty("message") String message, + /** Server timestamp in milliseconds */ + @JsonProperty("timestamp") Long timestamp, + /** Server protocol version number */ + @JsonProperty("protocolVersion") Long protocolVersion +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Plugin.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Plugin.java new file mode 100644 index 000000000..1b09c22ce --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Plugin.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `Plugin` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Plugin( + /** Plugin name */ + @JsonProperty("name") String name, + /** Marketplace the plugin came from */ + @JsonProperty("marketplace") String marketplace, + /** Installed version */ + @JsonProperty("version") String version, + /** Whether the plugin is currently enabled */ + @JsonProperty("enabled") Boolean enabled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ReasoningSummary.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ReasoningSummary.java new file mode 100644 index 000000000..5e534e214 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ReasoningSummary.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Reasoning summary mode to request for supported model clients + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ReasoningSummary { + /** The {@code none} variant. */ + NONE("none"), + /** The {@code concise} variant. */ + CONCISE("concise"), + /** The {@code detailed} variant. */ + DETAILED("detailed"); + + private final String value; + ReasoningSummary(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ReasoningSummary fromValue(String value) { + for (ReasoningSummary v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ReasoningSummary value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java new file mode 100644 index 000000000..93238eef4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RemoteSessionMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemoteSessionMode { + /** The {@code off} variant. */ + OFF("off"), + /** The {@code export} variant. */ + EXPORT("export"), + /** The {@code on} variant. */ + ON("on"); + + private final String value; + RemoteSessionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemoteSessionMode fromValue(String value) { + for (RemoteSessionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemoteSessionMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcCaller.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcCaller.java new file mode 100644 index 000000000..d15513ce0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcCaller.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * Interface for invoking JSON-RPC methods with typed responses. + *

+ * Implementations delegate to the underlying transport layer + * (e.g., a {@code JsonRpcClient} instance). A method reference is typically the clearest + * way to adapt a generic {@code invoke} method to this interface: + *

{@code
+ * RpcCaller caller = jsonRpcClient::invoke;
+ * }
+ * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public interface RpcCaller { + + /** + * Invokes a JSON-RPC method and returns a future for the typed response. + * + * @param the expected response type + * @param method the JSON-RPC method name + * @param params the request parameters (may be a {@code Map}, DTO record, or {@code JsonNode}) + * @param resultType the {@link Class} of the expected response type + * @return a {@link CompletableFuture} that completes with the deserialized result + */ + CompletableFuture invoke(String method, Object params, Class resultType); +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcMapper.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcMapper.java new file mode 100644 index 000000000..87d432820 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/RpcMapper.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Package-private holder for the shared {@link com.fasterxml.jackson.databind.ObjectMapper} + * used by session API classes when merging {@code sessionId} into call parameters. + *

+ * {@link com.fasterxml.jackson.databind.ObjectMapper} is thread-safe and expensive to + * instantiate, so a single shared instance is used across all generated API classes. + * The configuration mirrors {@code JsonRpcClient}'s mapper (JavaTimeModule, lenient + * unknown-property handling, ISO date format, NON_NULL inclusion). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +final class RpcMapper { + + static final com.fasterxml.jackson.databind.ObjectMapper INSTANCE = createMapper(); + + private static com.fasterxml.jackson.databind.ObjectMapper createMapper() { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + mapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()); + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setDefaultPropertyInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL); + return mapper; + } + + private RpcMapper() {} +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerAccountApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerAccountApi.java new file mode 100644 index 000000000..583e11085 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerAccountApi.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code account} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerAccountApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerAccountApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional GitHub token used to look up quota for a specific user instead of the global auth context. + * @since 1.0.0 + */ + public CompletableFuture getQuota() { + return caller.invoke("account.getQuota", java.util.Map.of(), AccountGetQuotaResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpApi.java new file mode 100644 index 000000000..db70171c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerMcpApi { + + private final RpcCaller caller; + + /** API methods for the {@code mcp.config} sub-namespace. */ + public final ServerMcpConfigApi config; + + /** @param caller the RPC transport function */ + ServerMcpApi(RpcCaller caller) { + this.caller = caller; + this.config = new ServerMcpConfigApi(caller); + } + + /** + * Optional working directory used as context for MCP server discovery. + * @since 1.0.0 + */ + public CompletableFuture discover(McpDiscoverParams params) { + return caller.invoke("mcp.discover", params, McpDiscoverResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpConfigApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpConfigApi.java new file mode 100644 index 000000000..cec231c61 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerMcpConfigApi.java @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp.config} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerMcpConfigApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerMcpConfigApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * User-configured MCP servers, keyed by server name. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("mcp.config.list", java.util.Map.of(), McpConfigListResult.class); + } + + /** + * MCP server name and configuration to add to user configuration. + * @since 1.0.0 + */ + public CompletableFuture add(McpConfigAddParams params) { + return caller.invoke("mcp.config.add", params, Void.class); + } + + /** + * MCP server name and replacement configuration to write to user configuration. + * @since 1.0.0 + */ + public CompletableFuture update(McpConfigUpdateParams params) { + return caller.invoke("mcp.config.update", params, Void.class); + } + + /** + * MCP server name to remove from user configuration. + * @since 1.0.0 + */ + public CompletableFuture remove(McpConfigRemoveParams params) { + return caller.invoke("mcp.config.remove", params, Void.class); + } + + /** + * MCP server names to enable for new sessions. + * @since 1.0.0 + */ + public CompletableFuture enable(McpConfigEnableParams params) { + return caller.invoke("mcp.config.enable", params, Void.class); + } + + /** + * MCP server names to disable for new sessions. + * @since 1.0.0 + */ + public CompletableFuture disable(McpConfigDisableParams params) { + return caller.invoke("mcp.config.disable", params, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerModelsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerModelsApi.java new file mode 100644 index 000000000..e062c6f0a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerModelsApi.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code models} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerModelsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerModelsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional GitHub token used to list models for a specific user instead of the global auth context. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("models.list", java.util.Map.of(), ModelsListResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java new file mode 100644 index 000000000..9de3df51c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerRpc.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * Typed client for server-level RPC methods. + *

+ * Provides strongly-typed access to all server-level API namespaces. + *

+ * Obtain an instance by calling {@code new ServerRpc(caller)}. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerRpc { + + private final RpcCaller caller; + + /** API methods for the {@code models} namespace. */ + public final ServerModelsApi models; + /** API methods for the {@code tools} namespace. */ + public final ServerToolsApi tools; + /** API methods for the {@code account} namespace. */ + public final ServerAccountApi account; + /** API methods for the {@code mcp} namespace. */ + public final ServerMcpApi mcp; + /** API methods for the {@code skills} namespace. */ + public final ServerSkillsApi skills; + /** API methods for the {@code sessionFs} namespace. */ + public final ServerSessionFsApi sessionFs; + /** API methods for the {@code sessions} namespace. */ + public final ServerSessionsApi sessions; + + /** + * Creates a new server RPC client. + * + * @param caller the RPC transport function (e.g., {@code jsonRpcClient::invoke}) + */ + public ServerRpc(RpcCaller caller) { + this.caller = caller; + this.models = new ServerModelsApi(caller); + this.tools = new ServerToolsApi(caller); + this.account = new ServerAccountApi(caller); + this.mcp = new ServerMcpApi(caller); + this.skills = new ServerSkillsApi(caller); + this.sessionFs = new ServerSessionFsApi(caller); + this.sessions = new ServerSessionsApi(caller); + } + + /** + * Optional message to echo back to the caller. + * @since 1.0.0 + */ + public CompletableFuture ping(PingParams params) { + return caller.invoke("ping", params, PingResult.class); + } + + /** + * Optional connection token presented by the SDK client during the handshake. + * @since 1.0.0 + */ + public CompletableFuture connect(ConnectParams params) { + return caller.invoke("connect", params, ConnectResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionFsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionFsApi.java new file mode 100644 index 000000000..25aefcf25 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionFsApi.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code sessionFs} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSessionFsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSessionFsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. + * @since 1.0.0 + */ + public CompletableFuture setProvider(SessionFsSetProviderParams params) { + return caller.invoke("sessionFs.setProvider", params, SessionFsSetProviderResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java new file mode 100644 index 000000000..a78a2fbf5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSessionsApi.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code sessions} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSessionsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSessionsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture fork(SessionsForkParams params) { + return caller.invoke("sessions.fork", params, SessionsForkResult.class); + } + + /** + * Remote session connection parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture connect() { + return caller.invoke("sessions.connect", java.util.Map.of(), SessionsConnectResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkill.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkill.java new file mode 100644 index 000000000..92f9e0ccc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkill.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `ServerSkill` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ServerSkill( + /** Unique identifier for the skill */ + @JsonProperty("name") String name, + /** Description of what the skill does */ + @JsonProperty("description") String description, + /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + @JsonProperty("source") SkillSource source, + /** Whether the skill can be invoked by the user as a slash command */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether the skill is currently enabled (based on global config) */ + @JsonProperty("enabled") Boolean enabled, + /** Absolute path to the skill file */ + @JsonProperty("path") String path, + /** The project path this skill belongs to (only for project/inherited skills) */ + @JsonProperty("projectPath") String projectPath +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsApi.java new file mode 100644 index 000000000..8884edc97 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code skills} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSkillsApi { + + private final RpcCaller caller; + + /** API methods for the {@code skills.config} sub-namespace. */ + public final ServerSkillsConfigApi config; + + /** @param caller the RPC transport function */ + ServerSkillsApi(RpcCaller caller) { + this.caller = caller; + this.config = new ServerSkillsConfigApi(caller); + } + + /** + * Optional project paths and additional skill directories to include in discovery. + * @since 1.0.0 + */ + public CompletableFuture discover(SkillsDiscoverParams params) { + return caller.invoke("skills.discover", params, SkillsDiscoverResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsConfigApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsConfigApi.java new file mode 100644 index 000000000..a0236fd33 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerSkillsConfigApi.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code skills.config} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSkillsConfigApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSkillsConfigApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Skill names to mark as disabled in global configuration, replacing any previous list. + * @since 1.0.0 + */ + public CompletableFuture setDisabledSkills(SkillsConfigSetDisabledSkillsParams params) { + return caller.invoke("skills.config.setDisabledSkills", params, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerToolsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerToolsApi.java new file mode 100644 index 000000000..81bbaece6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ServerToolsApi.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code tools} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerToolsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerToolsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional model identifier whose tool overrides should be applied to the listing. + * @since 1.0.0 + */ + public CompletableFuture list(ToolsListParams params) { + return caller.invoke("tools.list", params, ToolsListResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentApi.java new file mode 100644 index 000000000..ab91fe3ae --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentApi.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code agent} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAgentApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionAgentApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.agent.list", java.util.Map.of("sessionId", this.sessionId), SessionAgentListResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getCurrent() { + return caller.invoke("session.agent.getCurrent", java.util.Map.of("sessionId", this.sessionId), SessionAgentGetCurrentResult.class); + } + + /** + * Name of the custom agent to select for subsequent turns. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture select(SessionAgentSelectParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.agent.select", _p, SessionAgentSelectResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture deselect() { + return caller.invoke("session.agent.deselect", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture reload() { + return caller.invoke("session.agent.reload", java.util.Map.of("sessionId", this.sessionId), SessionAgentReloadResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectParams.java new file mode 100644 index 000000000..d412c83cf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentDeselectParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectResult.java new file mode 100644 index 000000000..81e2e3ebb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentDeselectResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.agent.deselect} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentDeselectResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentParams.java new file mode 100644 index 000000000..24bf532ff --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentGetCurrentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentResult.java new file mode 100644 index 000000000..fec2bc94f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentGetCurrentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The currently selected custom agent, or null when using the default agent. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentGetCurrentResult( + /** Currently selected custom agent, or null if using the default agent */ + @JsonProperty("agent") AgentInfo agent +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListParams.java new file mode 100644 index 000000000..6badf614c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListResult.java new file mode 100644 index 000000000..9b618b248 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Custom agents available to the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentListResult( + /** Available custom agents */ + @JsonProperty("agents") List agents +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadParams.java new file mode 100644 index 000000000..5b30c866a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentReloadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadResult.java new file mode 100644 index 000000000..d058293ea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentReloadResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Custom agents available to the session after reloading definitions from disk. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentReloadResult( + /** Reloaded custom agents */ + @JsonProperty("agents") List agents +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectParams.java new file mode 100644 index 000000000..38532ace0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Name of the custom agent to select for subsequent turns. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentSelectParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the custom agent to select */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectResult.java new file mode 100644 index 000000000..ea19f5648 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAgentSelectResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The newly selected custom agent. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAgentSelectResult( + /** The newly selected custom agent */ + @JsonProperty("agent") AgentInfo agent +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java new file mode 100644 index 000000000..ceb245027 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthApi.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code auth} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAuthApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionAuthApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture getStatus() { + return caller.invoke("session.auth.getStatus", java.util.Map.of("sessionId", this.sessionId), SessionAuthGetStatusResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusParams.java new file mode 100644 index 000000000..4a9988668 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAuthGetStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusResult.java new file mode 100644 index 000000000..6e58fe6c7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionAuthGetStatusResult.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Authentication status and account metadata for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAuthGetStatusResult( + /** Whether the session has resolved authentication */ + @JsonProperty("isAuthenticated") Boolean isAuthenticated, + /** Authentication type */ + @JsonProperty("authType") AuthInfoType authType, + /** Authentication host URL */ + @JsonProperty("host") String host, + /** Authenticated login/username, if available */ + @JsonProperty("login") String login, + /** Human-readable authentication status description */ + @JsonProperty("statusMessage") String statusMessage, + /** Copilot plan tier (e.g., individual_pro, business) */ + @JsonProperty("copilotPlan") String copilotPlan +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java new file mode 100644 index 000000000..df5e6d9f2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsApi.java @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code commands} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCommandsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionCommandsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Optional filters controlling which command sources to include in the listing. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.commands.list", java.util.Map.of("sessionId", this.sessionId), SessionCommandsListResult.class); + } + + /** + * Slash command name and optional raw input string to invoke. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture invoke(SessionCommandsInvokeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.invoke", _p, Void.class); + } + + /** + * Pending command request ID and an optional error if the client handler failed. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture handlePendingCommand(SessionCommandsHandlePendingCommandParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.handlePendingCommand", _p, SessionCommandsHandlePendingCommandResult.class); + } + + /** + * Queued command request ID and the result indicating whether the client handled it. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture respondToQueuedCommand(SessionCommandsRespondToQueuedCommandParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.commands.respondToQueuedCommand", _p, SessionCommandsRespondToQueuedCommandResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandParams.java new file mode 100644 index 000000000..e036870e1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Pending command request ID and an optional error if the client handler failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsHandlePendingCommandParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Request ID from the command invocation event */ + @JsonProperty("requestId") String requestId, + /** Error message if the command handler failed */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandResult.java new file mode 100644 index 000000000..101714028 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsHandlePendingCommandResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending client-handled command was completed successfully. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsHandlePendingCommandResult( + /** Whether the command was handled successfully */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java new file mode 100644 index 000000000..ec35a5bb3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsInvokeParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Slash command name and optional raw input string to invoke. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsInvokeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Command name. Leading slashes are stripped and the name is matched case-insensitively. */ + @JsonProperty("name") String name, + /** Raw input after the command name */ + @JsonProperty("input") String input +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java new file mode 100644 index 000000000..a1fa2728b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional filters controlling which command sources to include in the listing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java new file mode 100644 index 000000000..aae276f6c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Slash commands available in the session, after applying any include/exclude filters. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsListResult( + /** Commands available in this session */ + @JsonProperty("commands") List commands +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java new file mode 100644 index 000000000..b5a2e31f2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Queued command request ID and the result indicating whether the client handled it. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsRespondToQueuedCommandParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Request ID from the queued command event */ + @JsonProperty("requestId") String requestId, + /** Result of the queued command execution */ + @JsonProperty("result") Object result +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java new file mode 100644 index 000000000..eb05c1d9e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionCommandsRespondToQueuedCommandResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the queued-command response was accepted by the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionCommandsRespondToQueuedCommandResult( + /** Whether the response was accepted (false if the requestId was not found or already resolved) */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsApi.java new file mode 100644 index 000000000..cf7e6a505 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsApi.java @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code extensions} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionExtensionsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionExtensionsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.extensions.list", java.util.Map.of("sessionId", this.sessionId), SessionExtensionsListResult.class); + } + + /** + * Source-qualified extension identifier to enable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable(SessionExtensionsEnableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.extensions.enable", _p, Void.class); + } + + /** + * Source-qualified extension identifier to disable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable(SessionExtensionsDisableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.extensions.disable", _p, Void.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture reload() { + return caller.invoke("session.extensions.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableParams.java new file mode 100644 index 000000000..896ee43c8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Source-qualified extension identifier to disable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Source-qualified extension ID to disable */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableResult.java new file mode 100644 index 000000000..136e858fb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsDisableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.extensions.disable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsDisableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableParams.java new file mode 100644 index 000000000..45db74f49 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Source-qualified extension identifier to enable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Source-qualified extension ID to enable */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableResult.java new file mode 100644 index 000000000..1b7d328e0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsEnableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.extensions.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsEnableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListParams.java new file mode 100644 index 000000000..b1c320f68 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListResult.java new file mode 100644 index 000000000..ae3aa777f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Extensions discovered for the session, with their current status. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsListResult( + /** Discovered extensions and their current status */ + @JsonProperty("extensions") List extensions +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadParams.java new file mode 100644 index 000000000..e192ededf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsReloadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadResult.java new file mode 100644 index 000000000..e4a1a2264 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionExtensionsReloadResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.extensions.reload} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionExtensionsReloadResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetApi.java new file mode 100644 index 000000000..3c59b0c4a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code fleet} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionFleetApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionFleetApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Optional user prompt to combine with the fleet orchestration instructions. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture start(SessionFleetStartParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.fleet.start", _p, SessionFleetStartResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartParams.java new file mode 100644 index 000000000..5e687cec5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional user prompt to combine with the fleet orchestration instructions. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFleetStartParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Optional user prompt to combine with fleet instructions */ + @JsonProperty("prompt") String prompt +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartResult.java new file mode 100644 index 000000000..e328b4ec2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFleetStartResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether fleet mode was successfully activated. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFleetStartResult( + /** Whether fleet mode was successfully activated */ + @JsonProperty("started") Boolean started +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsAppendFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsAppendFileParams.java new file mode 100644 index 000000000..84a1807ce --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsAppendFileParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * File path, content to append, and optional mode for the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsAppendFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Content to append */ + @JsonProperty("content") String content, + /** Optional POSIX-style mode for newly created files */ + @JsonProperty("mode") Long mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsError.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsError.java new file mode 100644 index 000000000..a78aa5374 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsError.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Describes a filesystem error. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsError( + /** Error classification */ + @JsonProperty("code") SessionFsErrorCode code, + /** Free-form detail about the error, for logging/diagnostics */ + @JsonProperty("message") String message +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsErrorCode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsErrorCode.java new file mode 100644 index 000000000..099ff1236 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsErrorCode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Error classification + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionFsErrorCode { + /** The {@code ENOENT} variant. */ + ENOENT("ENOENT"), + /** The {@code UNKNOWN} variant. */ + UNKNOWN("UNKNOWN"); + + private final String value; + SessionFsErrorCode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionFsErrorCode fromValue(String value) { + for (SessionFsErrorCode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionFsErrorCode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsParams.java new file mode 100644 index 000000000..f5217f532 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path to test for existence in the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsExistsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsResult.java new file mode 100644 index 000000000..6209fd635 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsExistsResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the requested path exists in the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsExistsResult( + /** Whether the path exists */ + @JsonProperty("exists") Boolean exists +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsMkdirParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsMkdirParams.java new file mode 100644 index 000000000..80e0e95a2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsMkdirParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Directory path to create in the client-provided session filesystem, with options for recursive creation and POSIX mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsMkdirParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Create parent directories as needed */ + @JsonProperty("recursive") Boolean recursive, + /** Optional POSIX-style mode for newly created directories */ + @JsonProperty("mode") Long mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileParams.java new file mode 100644 index 000000000..851c1ac88 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path of the file to read from the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileResult.java new file mode 100644 index 000000000..c3abbde10 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReadFileResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * File content as a UTF-8 string, or a filesystem error if the read failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileResult( + /** File content as UTF-8 string */ + @JsonProperty("content") String content, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirParams.java new file mode 100644 index 000000000..1b18f9df5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Directory path whose entries should be listed from the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReaddirParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirResult.java new file mode 100644 index 000000000..053017d1e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Names of entries in the requested directory, or a filesystem error if the read failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReaddirResult( + /** Entry names in the directory */ + @JsonProperty("entries") List entries, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntry.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntry.java new file mode 100644 index 000000000..7cafa538e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntry.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SessionFsReaddirWithTypesEntry` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReaddirWithTypesEntry( + /** Entry name */ + @JsonProperty("name") String name, + /** Entry type */ + @JsonProperty("type") SessionFsReaddirWithTypesEntryType type +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntryType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntryType.java new file mode 100644 index 000000000..71640ec34 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesEntryType.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Entry type + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionFsReaddirWithTypesEntryType { + /** The {@code file} variant. */ + FILE("file"), + /** The {@code directory} variant. */ + DIRECTORY("directory"); + + private final String value; + SessionFsReaddirWithTypesEntryType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionFsReaddirWithTypesEntryType fromValue(String value) { + for (SessionFsReaddirWithTypesEntryType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionFsReaddirWithTypesEntryType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesParams.java new file mode 100644 index 000000000..b092d2075 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Directory path whose entries (with type information) should be listed from the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReaddirWithTypesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesResult.java new file mode 100644 index 000000000..13f105622 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsReaddirWithTypesResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Entries in the requested directory paired with file/directory type information, or a filesystem error if the read failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReaddirWithTypesResult( + /** Directory entries with type information */ + @JsonProperty("entries") List entries, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRenameParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRenameParams.java new file mode 100644 index 000000000..f1d758cba --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRenameParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Source and destination paths for renaming or moving an entry in the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsRenameParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Source path using SessionFs conventions */ + @JsonProperty("src") String src, + /** Destination path using SessionFs conventions */ + @JsonProperty("dest") String dest +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRmParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRmParams.java new file mode 100644 index 000000000..b73a9d631 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsRmParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path to remove from the client-provided session filesystem, with options for recursive removal and force. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsRmParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Remove directories and their contents recursively */ + @JsonProperty("recursive") Boolean recursive, + /** Ignore errors if the path does not exist */ + @JsonProperty("force") Boolean force +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderCapabilities.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderCapabilities.java new file mode 100644 index 000000000..570182125 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderCapabilities.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional capabilities declared by the provider + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSetProviderCapabilities( + /** Whether the provider supports SQLite query/exists operations */ + @JsonProperty("sqlite") Boolean sqlite +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderConventions.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderConventions.java new file mode 100644 index 000000000..ac669a189 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderConventions.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Path conventions used by this filesystem + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionFsSetProviderConventions { + /** The {@code windows} variant. */ + WINDOWS("windows"), + /** The {@code posix} variant. */ + POSIX("posix"); + + private final String value; + SessionFsSetProviderConventions(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionFsSetProviderConventions fromValue(String value) { + for (SessionFsSetProviderConventions v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionFsSetProviderConventions value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderParams.java new file mode 100644 index 000000000..e03dcfcc8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSetProviderParams( + /** Initial working directory for sessions */ + @JsonProperty("initialCwd") String initialCwd, + /** Path within each session's SessionFs where the runtime stores files for that session */ + @JsonProperty("sessionStatePath") String sessionStatePath, + /** Path conventions used by this filesystem */ + @JsonProperty("conventions") SessionFsSetProviderConventions conventions, + /** Optional capabilities declared by the provider */ + @JsonProperty("capabilities") SessionFsSetProviderCapabilities capabilities +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderResult.java new file mode 100644 index 000000000..621ed7d05 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSetProviderResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the calling client was registered as the session filesystem provider. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSetProviderResult( + /** Whether the provider was set successfully */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsParams.java new file mode 100644 index 000000000..47f2bf045 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSqliteExistsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsResult.java new file mode 100644 index 000000000..0cccf0cec --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteExistsResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the per-session SQLite database already exists. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSqliteExistsResult( + /** Whether the session database already exists */ + @JsonProperty("exists") Boolean exists +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryParams.java new file mode 100644 index 000000000..1f07d8cac --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * SQL query, query type, and optional bind parameters for executing a SQLite query against the per-session database. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSqliteQueryParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** SQL query to execute */ + @JsonProperty("query") String query, + /** How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected) */ + @JsonProperty("queryType") SessionFsSqliteQueryType queryType, + /** Optional named bind parameters */ + @JsonProperty("params") Map params +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java new file mode 100644 index 000000000..df6549a9d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryResult.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Query results including rows, columns, and rows affected, or a filesystem error if execution failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsSqliteQueryResult( + /** For SELECT: array of row objects. For others: empty array. */ + @JsonProperty("rows") List> rows, + /** Column names from the result set */ + @JsonProperty("columns") List columns, + /** Number of rows affected (for INSERT/UPDATE/DELETE) */ + @JsonProperty("rowsAffected") Long rowsAffected, + /** Last inserted row ID (for INSERT) */ + @JsonProperty("lastInsertRowid") Double lastInsertRowid, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryType.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryType.java new file mode 100644 index 000000000..ef0143da2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsSqliteQueryType.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * How to execute the query: 'exec' for DDL/multi-statement (no results), 'query' for SELECT (returns rows), 'run' for INSERT/UPDATE/DELETE (returns rowsAffected) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionFsSqliteQueryType { + /** The {@code exec} variant. */ + EXEC("exec"), + /** The {@code query} variant. */ + QUERY("query"), + /** The {@code run} variant. */ + RUN("run"); + + private final String value; + SessionFsSqliteQueryType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionFsSqliteQueryType fromValue(String value) { + for (SessionFsSqliteQueryType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionFsSqliteQueryType value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatParams.java new file mode 100644 index 000000000..410b168fd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Path whose metadata should be returned from the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsStatParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatResult.java new file mode 100644 index 000000000..2e3b811d9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsStatResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Filesystem metadata for the requested path, or a filesystem error if the stat failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsStatResult( + /** Whether the path is a file */ + @JsonProperty("isFile") Boolean isFile, + /** Whether the path is a directory */ + @JsonProperty("isDirectory") Boolean isDirectory, + /** File size in bytes */ + @JsonProperty("size") Long size, + /** ISO 8601 timestamp of last modification */ + @JsonProperty("mtime") OffsetDateTime mtime, + /** ISO 8601 timestamp of creation */ + @JsonProperty("birthtime") OffsetDateTime birthtime, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsWriteFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsWriteFileParams.java new file mode 100644 index 000000000..ed08b2f7d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionFsWriteFileParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * File path, content to write, and optional mode for the client-provided session filesystem. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsWriteFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Content to write */ + @JsonProperty("content") String content, + /** Optional POSIX-style mode for newly created files */ + @JsonProperty("mode") Long mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java new file mode 100644 index 000000000..9988a4a88 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryApi.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code history} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionHistoryApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionHistoryApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture compact() { + return caller.invoke("session.history.compact", java.util.Map.of("sessionId", this.sessionId), SessionHistoryCompactResult.class); + } + + /** + * Identifier of the event to truncate to; this event and all later events are removed. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture truncate(SessionHistoryTruncateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.history.truncate", _p, SessionHistoryTruncateResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java new file mode 100644 index 000000000..8737d590f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryCompactParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java new file mode 100644 index 000000000..f7a8664b9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryCompactResult.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Compaction outcome with the number of tokens and messages removed and the resulting context window breakdown. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryCompactResult( + /** Whether compaction completed successfully */ + @JsonProperty("success") Boolean success, + /** Number of tokens freed by compaction */ + @JsonProperty("tokensRemoved") Long tokensRemoved, + /** Number of messages removed during compaction */ + @JsonProperty("messagesRemoved") Long messagesRemoved, + /** Post-compaction context window usage breakdown */ + @JsonProperty("contextWindow") HistoryCompactContextWindow contextWindow +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateParams.java new file mode 100644 index 000000000..a56ed6994 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the event to truncate to; this event and all later events are removed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryTruncateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Event ID to truncate to. This event and all events after it are removed from the session. */ + @JsonProperty("eventId") String eventId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateResult.java new file mode 100644 index 000000000..7905c66b9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionHistoryTruncateResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Number of events that were removed by the truncation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionHistoryTruncateResult( + /** Number of events that were removed */ + @JsonProperty("eventsRemoved") Long eventsRemoved +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java new file mode 100644 index 000000000..23c4cf3b6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsApi.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code instructions} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionInstructionsApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionInstructionsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture getSources() { + return caller.invoke("session.instructions.getSources", java.util.Map.of("sessionId", this.sessionId), SessionInstructionsGetSourcesResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesParams.java new file mode 100644 index 000000000..cc1e2fb3b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionInstructionsGetSourcesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesResult.java new file mode 100644 index 000000000..10badb176 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionInstructionsGetSourcesResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Instruction sources loaded for the session, in merge order. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionInstructionsGetSourcesResult( + /** Instruction sources for the session */ + @JsonProperty("sources") List sources +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogLevel.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogLevel.java new file mode 100644 index 000000000..7ec7361a7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogLevel.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionLogLevel { + /** The {@code info} variant. */ + INFO("info"), + /** The {@code warning} variant. */ + WARNING("warning"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + SessionLogLevel(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionLogLevel fromValue(String value) { + for (SessionLogLevel v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionLogLevel value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java new file mode 100644 index 000000000..f593c11c2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogParams.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Message text, optional severity level, persistence flag, and optional follow-up URL. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionLogParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Human-readable message */ + @JsonProperty("message") String message, + /** Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". */ + @JsonProperty("level") SessionLogLevel level, + /** When true, the message is transient and not persisted to the session event log on disk */ + @JsonProperty("ephemeral") Boolean ephemeral, + /** Optional URL the user can open in their browser for more details */ + @JsonProperty("url") String url +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogResult.java new file mode 100644 index 000000000..23d9e8d6a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionLogResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.UUID; +import javax.annotation.processing.Generated; + +/** + * Identifier of the session event that was emitted for the log message. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionLogResult( + /** The unique identifier of the emitted session event */ + @JsonProperty("eventId") UUID eventId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java new file mode 100644 index 000000000..93714e068 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpApi.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** API methods for the {@code mcp.oauth} sub-namespace. */ + public final SessionMcpOauthApi oauth; + + /** @param caller the RPC transport function */ + SessionMcpApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + this.oauth = new SessionMcpOauthApi(caller, sessionId); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.mcp.list", java.util.Map.of("sessionId", this.sessionId), SessionMcpListResult.class); + } + + /** + * Name of the MCP server to enable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable(SessionMcpEnableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.enable", _p, Void.class); + } + + /** + * Name of the MCP server to disable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable(SessionMcpDisableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.disable", _p, Void.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture reload() { + return caller.invoke("session.mcp.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableParams.java new file mode 100644 index 000000000..a9d2b0060 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Name of the MCP server to disable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server to disable */ + @JsonProperty("serverName") String serverName +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableResult.java new file mode 100644 index 000000000..0565cf8d1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpDisableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.mcp.disable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpDisableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableParams.java new file mode 100644 index 000000000..f7d19088a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Name of the MCP server to enable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server to enable */ + @JsonProperty("serverName") String serverName +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableResult.java new file mode 100644 index 000000000..43319c3a9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpEnableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.mcp.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpEnableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListParams.java new file mode 100644 index 000000000..fb04ed4a1 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListResult.java new file mode 100644 index 000000000..d61809d42 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * MCP servers configured for the session, with their connection status. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListResult( + /** Configured MCP servers */ + @JsonProperty("servers") List servers +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthApi.java new file mode 100644 index 000000000..c603af3d7 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthApi.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp.oauth} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpOauthApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMcpOauthApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, and the callback success-page copy. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture login(SessionMcpOauthLoginParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.login", _p, SessionMcpOauthLoginResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginParams.java new file mode 100644 index 000000000..004cd3d62 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginParams.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, and the callback success-page copy. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpOauthLoginParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the remote MCP server to authenticate */ + @JsonProperty("serverName") String serverName, + /** When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. */ + @JsonProperty("forceReauth") Boolean forceReauth, + /** Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only β€” existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. */ + @JsonProperty("clientName") String clientName, + /** Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. */ + @JsonProperty("callbackSuccessMessage") String callbackSuccessMessage +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginResult.java new file mode 100644 index 000000000..9c557f6f5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpOauthLoginResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpOauthLoginResult( + /** URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed β€” the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. */ + @JsonProperty("authorizationUrl") String authorizationUrl +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadParams.java new file mode 100644 index 000000000..705e42b72 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpReloadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadResult.java new file mode 100644 index 000000000..80a2c4c26 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMcpReloadResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.mcp.reload} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpReloadResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMode.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMode.java new file mode 100644 index 000000000..d335fd7cd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The session mode the agent is operating in + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SessionMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"); + + private final String value; + SessionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionMode fromValue(String value) { + for (SessionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionMode value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java new file mode 100644 index 000000000..9e67580dc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeApi.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mode} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModeApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionModeApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture get() { + return caller.invoke("session.mode.get", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + + /** + * Agent interaction mode to apply to the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture set(SessionModeSetParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mode.set", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetParams.java new file mode 100644 index 000000000..c8f660f92 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModeGetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetResult.java new file mode 100644 index 000000000..595dff851 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeGetResult.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.mode.get} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModeGetResult( + /** The current agent mode. */ + @JsonProperty("mode") SessionModeGetResultMode mode +) { + + /** The current agent mode. */ + public enum SessionModeGetResultMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"); + + private final String value; + SessionModeGetResultMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionModeGetResultMode fromValue(String value) { + for (SessionModeGetResultMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionModeGetResultMode value: " + value); + } + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetParams.java new file mode 100644 index 000000000..22618fe35 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Agent interaction mode to apply to the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModeSetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The session mode the agent is operating in */ + @JsonProperty("mode") SessionMode mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetResult.java new file mode 100644 index 000000000..f4609f671 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModeSetResult.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.mode.set} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModeSetResult( + /** The agent mode after switching. */ + @JsonProperty("mode") SessionModeSetResultMode mode +) { + + /** The agent mode after switching. */ + public enum SessionModeSetResultMode { + /** The {@code interactive} variant. */ + INTERACTIVE("interactive"), + /** The {@code plan} variant. */ + PLAN("plan"), + /** The {@code autopilot} variant. */ + AUTOPILOT("autopilot"); + + private final String value; + SessionModeSetResultMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionModeSetResultMode fromValue(String value) { + for (SessionModeSetResultMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionModeSetResultMode value: " + value); + } + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java new file mode 100644 index 000000000..55b3b18c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelApi.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code model} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModelApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionModelApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture getCurrent() { + return caller.invoke("session.model.getCurrent", java.util.Map.of("sessionId", this.sessionId), SessionModelGetCurrentResult.class); + } + + /** + * Target model identifier and optional reasoning effort, summary, and capability overrides. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture switchTo(SessionModelSwitchToParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.switchTo", _p, SessionModelSwitchToResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentParams.java new file mode 100644 index 000000000..1687e9fff --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelGetCurrentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java new file mode 100644 index 000000000..4a5a60525 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelGetCurrentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The currently selected model for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelGetCurrentResult( + /** Currently active model identifier */ + @JsonProperty("modelId") String modelId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToParams.java new file mode 100644 index 000000000..e06d3e68a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToParams.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Target model identifier and optional reasoning effort, summary, and capability overrides. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSwitchToParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Model identifier to switch to */ + @JsonProperty("modelId") String modelId, + /** Reasoning effort level to use for the model. "none" disables reasoning. */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Reasoning summary mode to request for supported model clients */ + @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, + /** Override individual model capabilities resolved by the runtime */ + @JsonProperty("modelCapabilities") ModelCapabilitiesOverride modelCapabilities +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToResult.java new file mode 100644 index 000000000..6c4e7a39c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionModelSwitchToResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The model identifier active on the session after the switch. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSwitchToResult( + /** Currently active model identifier after the switch */ + @JsonProperty("modelId") String modelId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java new file mode 100644 index 000000000..371dc15ab --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameApi.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code name} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionNameApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionNameApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture get() { + return caller.invoke("session.name.get", java.util.Map.of("sessionId", this.sessionId), SessionNameGetResult.class); + } + + /** + * New friendly name to apply to the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture set(SessionNameSetParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.name.set", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetParams.java new file mode 100644 index 000000000..941a19a89 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionNameGetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java new file mode 100644 index 000000000..349aa81a2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameGetResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The session's friendly name, or null when not yet set. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionNameGetResult( + /** The session name (user-set or auto-generated), or null if not yet set */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetParams.java new file mode 100644 index 000000000..6ad1fcd37 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionNameSetParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * New friendly name to apply to the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionNameSetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** New session name (1–100 characters, trimmed of leading/trailing whitespace) */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java new file mode 100644 index 000000000..6a32d2d19 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsApi.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code permissions} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPermissionsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPermissionsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Pending permission request ID and the decision to apply (approve/reject and scope). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture handlePendingPermissionRequest(SessionPermissionsHandlePendingPermissionRequestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.handlePendingPermissionRequest", _p, SessionPermissionsHandlePendingPermissionRequestResult.class); + } + + /** + * Whether to auto-approve all tool permission requests for the rest of the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture setApproveAll(SessionPermissionsSetApproveAllParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.permissions.setApproveAll", _p, SessionPermissionsSetApproveAllResult.class); + } + + /** + * No parameters; clears all session-scoped tool permission approvals. + * @since 1.0.0 + */ + public CompletableFuture resetSessionApprovals() { + return caller.invoke("session.permissions.resetSessionApprovals", java.util.Map.of("sessionId", this.sessionId), SessionPermissionsResetSessionApprovalsResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java new file mode 100644 index 000000000..7991f3248 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Pending permission request ID and the decision to apply (approve/reject and scope). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsHandlePendingPermissionRequestParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Request ID of the pending permission request */ + @JsonProperty("requestId") String requestId, + /** Decision to apply to a pending permission request. */ + @JsonProperty("result") Object result +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestResult.java new file mode 100644 index 000000000..a517e642f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsHandlePendingPermissionRequestResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the permission decision was applied; false when the request was already resolved. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsHandlePendingPermissionRequestResult( + /** Whether the permission request was handled successfully */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsParams.java new file mode 100644 index 000000000..1f125f14d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * No parameters; clears all session-scoped tool permission approvals. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsResetSessionApprovalsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsResult.java new file mode 100644 index 000000000..81f71aea3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsResetSessionApprovalsResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsResetSessionApprovalsResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java new file mode 100644 index 000000000..ac599a2df --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Whether to auto-approve all tool permission requests for the rest of the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsSetApproveAllParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Whether to auto-approve all tool permission requests */ + @JsonProperty("enabled") Boolean enabled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllResult.java new file mode 100644 index 000000000..50301af4a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPermissionsSetApproveAllResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the operation succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPermissionsSetApproveAllResult( + /** Whether the operation succeeded */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java new file mode 100644 index 000000000..8f4bc6192 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanApi.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code plan} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPlanApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPlanApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture read() { + return caller.invoke("session.plan.read", java.util.Map.of("sessionId", this.sessionId), SessionPlanReadResult.class); + } + + /** + * Replacement contents to write to the session plan file. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture update(SessionPlanUpdateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.plan.update", _p, Void.class); + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture delete() { + return caller.invoke("session.plan.delete", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteParams.java new file mode 100644 index 000000000..5e7732fb8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanDeleteParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteResult.java new file mode 100644 index 000000000..666109985 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanDeleteResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.plan.delete} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanDeleteResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadParams.java new file mode 100644 index 000000000..2891852ea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadResult.java new file mode 100644 index 000000000..3b5c1634a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanReadResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Existence, contents, and resolved path of the session plan file. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanReadResult( + /** Whether the plan file exists in the workspace */ + @JsonProperty("exists") Boolean exists, + /** The content of the plan file, or null if it does not exist */ + @JsonProperty("content") String content, + /** Absolute file path of the plan file, or null if workspace is not enabled */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateParams.java new file mode 100644 index 000000000..fea63cf01 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Replacement contents to write to the session plan file. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanUpdateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The new content for the plan file */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateResult.java new file mode 100644 index 000000000..aa6c64aaf --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPlanUpdateResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.plan.update} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPlanUpdateResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsApi.java new file mode 100644 index 000000000..176310e11 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code plugins} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionPluginsApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionPluginsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.plugins.list", java.util.Map.of("sessionId", this.sessionId), SessionPluginsListResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListParams.java new file mode 100644 index 000000000..f5923c0d2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPluginsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListResult.java new file mode 100644 index 000000000..c5acac58d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionPluginsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Plugins installed for the session, with their enabled state and version metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionPluginsListResult( + /** Installed plugins */ + @JsonProperty("plugins") List plugins +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java new file mode 100644 index 000000000..ba3c91dd0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteApi.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code remote} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionRemoteApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionRemoteApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable(SessionRemoteEnableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.remote.enable", _p, SessionRemoteEnableResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable() { + return caller.invoke("session.remote.disable", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java new file mode 100644 index 000000000..c2ebe21e8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteDisableParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java new file mode 100644 index 000000000..b487353cb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Per-session remote mode. "off" disables remote, "export" exports session events to GitHub without enabling remote steering, "on" enables both export and remote steering. */ + @JsonProperty("mode") RemoteSessionMode mode +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java new file mode 100644 index 000000000..b098ebbef --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRemoteEnableResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * GitHub URL for the session and a flag indicating whether remote steering is enabled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionRemoteEnableResult( + /** GitHub frontend URL for this session */ + @JsonProperty("url") String url, + /** Whether remote steering is enabled */ + @JsonProperty("remoteSteerable") Boolean remoteSteerable +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java new file mode 100644 index 000000000..ffd890a76 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionRpc.java @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * Typed client for session-scoped RPC methods. + *

+ * Provides strongly-typed access to all session-level API namespaces. + * The {@code sessionId} is injected automatically into every call. + *

+ * Obtain an instance by calling {@code new SessionRpc(caller, sessionId)}. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionRpc { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** API methods for the {@code auth} namespace. */ + public final SessionAuthApi auth; + /** API methods for the {@code model} namespace. */ + public final SessionModelApi model; + /** API methods for the {@code mode} namespace. */ + public final SessionModeApi mode; + /** API methods for the {@code name} namespace. */ + public final SessionNameApi name; + /** API methods for the {@code plan} namespace. */ + public final SessionPlanApi plan; + /** API methods for the {@code workspaces} namespace. */ + public final SessionWorkspacesApi workspaces; + /** API methods for the {@code instructions} namespace. */ + public final SessionInstructionsApi instructions; + /** API methods for the {@code fleet} namespace. */ + public final SessionFleetApi fleet; + /** API methods for the {@code agent} namespace. */ + public final SessionAgentApi agent; + /** API methods for the {@code tasks} namespace. */ + public final SessionTasksApi tasks; + /** API methods for the {@code skills} namespace. */ + public final SessionSkillsApi skills; + /** API methods for the {@code mcp} namespace. */ + public final SessionMcpApi mcp; + /** API methods for the {@code plugins} namespace. */ + public final SessionPluginsApi plugins; + /** API methods for the {@code extensions} namespace. */ + public final SessionExtensionsApi extensions; + /** API methods for the {@code tools} namespace. */ + public final SessionToolsApi tools; + /** API methods for the {@code commands} namespace. */ + public final SessionCommandsApi commands; + /** API methods for the {@code ui} namespace. */ + public final SessionUiApi ui; + /** API methods for the {@code permissions} namespace. */ + public final SessionPermissionsApi permissions; + /** API methods for the {@code shell} namespace. */ + public final SessionShellApi shell; + /** API methods for the {@code history} namespace. */ + public final SessionHistoryApi history; + /** API methods for the {@code usage} namespace. */ + public final SessionUsageApi usage; + /** API methods for the {@code remote} namespace. */ + public final SessionRemoteApi remote; + + /** + * Creates a new session RPC client. + * + * @param caller the RPC transport function (e.g., {@code jsonRpcClient::invoke}) + * @param sessionId the session ID to inject into every request + */ + public SessionRpc(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + this.auth = new SessionAuthApi(caller, sessionId); + this.model = new SessionModelApi(caller, sessionId); + this.mode = new SessionModeApi(caller, sessionId); + this.name = new SessionNameApi(caller, sessionId); + this.plan = new SessionPlanApi(caller, sessionId); + this.workspaces = new SessionWorkspacesApi(caller, sessionId); + this.instructions = new SessionInstructionsApi(caller, sessionId); + this.fleet = new SessionFleetApi(caller, sessionId); + this.agent = new SessionAgentApi(caller, sessionId); + this.tasks = new SessionTasksApi(caller, sessionId); + this.skills = new SessionSkillsApi(caller, sessionId); + this.mcp = new SessionMcpApi(caller, sessionId); + this.plugins = new SessionPluginsApi(caller, sessionId); + this.extensions = new SessionExtensionsApi(caller, sessionId); + this.tools = new SessionToolsApi(caller, sessionId); + this.commands = new SessionCommandsApi(caller, sessionId); + this.ui = new SessionUiApi(caller, sessionId); + this.permissions = new SessionPermissionsApi(caller, sessionId); + this.shell = new SessionShellApi(caller, sessionId); + this.history = new SessionHistoryApi(caller, sessionId); + this.usage = new SessionUsageApi(caller, sessionId); + this.remote = new SessionRemoteApi(caller, sessionId); + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture suspend() { + return caller.invoke("session.suspend", java.util.Map.of("sessionId", this.sessionId), Void.class); + } + + /** + * Message text, optional severity level, persistence flag, and optional follow-up URL. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture log(SessionLogParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.log", _p, SessionLogResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java new file mode 100644 index 000000000..4a8e6a86c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellApi.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code shell} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionShellApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionShellApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Shell command to run, with optional working directory and timeout in milliseconds. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture exec(SessionShellExecParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.shell.exec", _p, SessionShellExecResult.class); + } + + /** + * Identifier of a process previously returned by "shell.exec" and the signal to send. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture kill(SessionShellKillParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.shell.kill", _p, SessionShellKillResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecParams.java new file mode 100644 index 000000000..82a5815d9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Shell command to run, with optional working directory and timeout in milliseconds. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionShellExecParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Shell command to execute */ + @JsonProperty("command") String command, + /** Working directory (defaults to session working directory) */ + @JsonProperty("cwd") String cwd, + /** Timeout in milliseconds (default: 30000) */ + @JsonProperty("timeout") Long timeout +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecResult.java new file mode 100644 index 000000000..d7790ce70 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellExecResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the spawned process, used to correlate streamed output and exit notifications. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionShellExecResult( + /** Unique identifier for tracking streamed output */ + @JsonProperty("processId") String processId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillParams.java new file mode 100644 index 000000000..c89e21982 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of a process previously returned by "shell.exec" and the signal to send. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionShellKillParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Process identifier returned by shell.exec */ + @JsonProperty("processId") String processId, + /** Signal to send (default: SIGTERM) */ + @JsonProperty("signal") ShellKillSignal signal +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillResult.java new file mode 100644 index 000000000..163c990bb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionShellKillResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the signal was delivered; false if the process was unknown or already exited. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionShellKillResult( + /** Whether the signal was sent successfully */ + @JsonProperty("killed") Boolean killed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java new file mode 100644 index 000000000..a96f410f0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsApi.java @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code skills} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionSkillsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionSkillsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.skills.list", java.util.Map.of("sessionId", this.sessionId), SessionSkillsListResult.class); + } + + /** + * Name of the skill to enable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture enable(SessionSkillsEnableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.skills.enable", _p, Void.class); + } + + /** + * Name of the skill to disable for the session. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture disable(SessionSkillsDisableParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.skills.disable", _p, Void.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture reload() { + return caller.invoke("session.skills.reload", java.util.Map.of("sessionId", this.sessionId), SessionSkillsReloadResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableParams.java new file mode 100644 index 000000000..82f20ecec --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Name of the skill to disable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsDisableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the skill to disable */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableResult.java new file mode 100644 index 000000000..7f3fe40b8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsDisableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.skills.disable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsDisableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableParams.java new file mode 100644 index 000000000..0d42ce06e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Name of the skill to enable for the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsEnableParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the skill to enable */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableResult.java new file mode 100644 index 000000000..1e7ea4c7f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsEnableResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.skills.enable} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsEnableResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListParams.java new file mode 100644 index 000000000..6f8986bfd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListResult.java new file mode 100644 index 000000000..98bafbaff --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Skills available to the session, with their enabled state. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsListResult( + /** Available skills */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadParams.java new file mode 100644 index 000000000..5c2cdbeb3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsReloadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java new file mode 100644 index 000000000..10d4fa7de --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSkillsReloadResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSkillsReloadResult( + /** Warnings emitted while loading skills (e.g. skills that loaded but had issues) */ + @JsonProperty("warnings") List warnings, + /** Errors emitted while loading skills (e.g. skills that failed to load entirely) */ + @JsonProperty("errors") List errors +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java new file mode 100644 index 000000000..103cefc68 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionSuspendParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java new file mode 100644 index 000000000..577d91ee3 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksApi.java @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code tasks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionTasksApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionTasksApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Agent type, prompt, name, and optional description and model override for the new task. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture startAgent(SessionTasksStartAgentParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.startAgent", _p, SessionTasksStartAgentResult.class); + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture list() { + return caller.invoke("session.tasks.list", java.util.Map.of("sessionId", this.sessionId), SessionTasksListResult.class); + } + + /** + * Identifier of the task to promote to background mode. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture promoteToBackground(SessionTasksPromoteToBackgroundParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.promoteToBackground", _p, SessionTasksPromoteToBackgroundResult.class); + } + + /** + * Identifier of the background task to cancel. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture cancel(SessionTasksCancelParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.cancel", _p, SessionTasksCancelResult.class); + } + + /** + * Identifier of the completed or cancelled task to remove from tracking. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture remove(SessionTasksRemoveParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.remove", _p, SessionTasksRemoveResult.class); + } + + /** + * Identifier of the target agent task, message content, and optional sender agent ID. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture sendMessage(SessionTasksSendMessageParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.sendMessage", _p, SessionTasksSendMessageResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java new file mode 100644 index 000000000..e00f9fcf8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the background task to cancel. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java new file mode 100644 index 000000000..1ecae8152 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksCancelResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the background task was successfully cancelled. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksCancelResult( + /** Whether the task was successfully cancelled */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java new file mode 100644 index 000000000..8716e9549 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java new file mode 100644 index 000000000..307cdd6b9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Background tasks currently tracked by the session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksListResult( + /** Currently tracked tasks */ + @JsonProperty("tasks") List tasks +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java new file mode 100644 index 000000000..6dc27fd7c --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the task to promote to background mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java new file mode 100644 index 000000000..9580bc608 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksPromoteToBackgroundResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the task was successfully promoted to background mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksPromoteToBackgroundResult( + /** Whether the task was successfully promoted to background mode */ + @JsonProperty("promoted") Boolean promoted +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java new file mode 100644 index 000000000..69fdfbd41 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the completed or cancelled task to remove from tracking. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task identifier */ + @JsonProperty("id") String id +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java new file mode 100644 index 000000000..44ff4eb75 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksRemoveResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the task was removed. False when the task does not exist or is still running/idle. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRemoveResult( + /** Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). */ + @JsonProperty("removed") Boolean removed +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java new file mode 100644 index 000000000..5b496b080 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier of the target agent task, message content, and optional sender agent ID. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksSendMessageParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Agent task identifier */ + @JsonProperty("id") String id, + /** Message content to send to the agent */ + @JsonProperty("message") String message, + /** Agent ID of the sender, if sent on behalf of another agent */ + @JsonProperty("fromAgentId") String fromAgentId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java new file mode 100644 index 000000000..0f72e5a69 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksSendMessageResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the message was delivered, with an error message when delivery failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksSendMessageResult( + /** Whether the message was successfully delivered or steered */ + @JsonProperty("sent") Boolean sent, + /** Error message if delivery failed */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java new file mode 100644 index 000000000..3ad64c52e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentParams.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Agent type, prompt, name, and optional description and model override for the new task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Type of agent to start (e.g., 'explore', 'task', 'general-purpose') */ + @JsonProperty("agentType") String agentType, + /** Task prompt for the agent */ + @JsonProperty("prompt") String prompt, + /** Short name for the agent, used to generate a human-readable ID */ + @JsonProperty("name") String name, + /** Short description of the task */ + @JsonProperty("description") String description, + /** Optional model override */ + @JsonProperty("model") String model +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java new file mode 100644 index 000000000..96bab97fa --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionTasksStartAgentResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier assigned to the newly started background agent task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksStartAgentResult( + /** Generated agent ID for the background task */ + @JsonProperty("agentId") String agentId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java new file mode 100644 index 000000000..323fdfe51 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsApi.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code tools} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionToolsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionToolsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Pending external tool call request ID, with the tool result or an error describing why it failed. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture handlePendingToolCall(SessionToolsHandlePendingToolCallParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tools.handlePendingToolCall", _p, SessionToolsHandlePendingToolCallResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallParams.java new file mode 100644 index 000000000..3bdde0904 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Pending external tool call request ID, with the tool result or an error describing why it failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsHandlePendingToolCallParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Request ID of the pending tool call */ + @JsonProperty("requestId") String requestId, + /** Tool call result (string or expanded result object) */ + @JsonProperty("result") Object result, + /** Error message if the tool call failed */ + @JsonProperty("error") String error +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallResult.java new file mode 100644 index 000000000..3eae1158d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionToolsHandlePendingToolCallResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the external tool call result was handled successfully. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionToolsHandlePendingToolCallResult( + /** Whether the tool call result was handled successfully */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java new file mode 100644 index 000000000..ef37d580d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiApi.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code ui} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionUiApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionUiApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Prompt message and JSON schema describing the form fields to elicit from the user. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture elicitation(SessionUiElicitationParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.elicitation", _p, SessionUiElicitationResult.class); + } + + /** + * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture handlePendingElicitation(SessionUiHandlePendingElicitationParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handlePendingElicitation", _p, SessionUiHandlePendingElicitationResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationParams.java new file mode 100644 index 000000000..e92aa36bd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Prompt message and JSON schema describing the form fields to elicit from the user. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiElicitationParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Message describing what information is needed from the user */ + @JsonProperty("message") String message, + /** JSON Schema describing the form fields to present to the user */ + @JsonProperty("requestedSchema") UIElicitationSchema requestedSchema +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationResult.java new file mode 100644 index 000000000..4be941e08 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiElicitationResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The elicitation response (accept with form values, decline, or cancel) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiElicitationResult( + /** The user's response: accept (submitted), decline (rejected), or cancel (dismissed) */ + @JsonProperty("action") UIElicitationResponseAction action, + /** The form values submitted by the user (present when action is 'accept') */ + @JsonProperty("content") Map content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationParams.java new file mode 100644 index 000000000..b648fb7a9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingElicitationParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the elicitation.requested event */ + @JsonProperty("requestId") String requestId, + /** The elicitation response (accept with form values, decline, or cancel) */ + @JsonProperty("result") UIElicitationResponse result +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationResult.java new file mode 100644 index 000000000..bf25a1686 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUiHandlePendingElicitationResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUiHandlePendingElicitationResult( + /** Whether the response was accepted. False if the request was already resolved by another client. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageApi.java new file mode 100644 index 000000000..c3db06d6b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code usage} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionUsageApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionUsageApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + public CompletableFuture getMetrics() { + return caller.invoke("session.usage.getMetrics", java.util.Map.of("sessionId", this.sessionId), SessionUsageGetMetricsResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsParams.java new file mode 100644 index 000000000..72cb52de9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUsageGetMetricsParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java new file mode 100644 index 000000000..ee7bf42cd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionUsageGetMetricsResult( + /** Total user-initiated premium request cost across all models (may be fractional due to multipliers) */ + @JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost, + /** Raw count of user-initiated API requests */ + @JsonProperty("totalUserRequests") Long totalUserRequests, + /** Session-wide accumulated nano-AI units cost */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Session-wide per-token-type accumulated token counts */ + @JsonProperty("tokenDetails") Map tokenDetails, + /** Total time spent in model API calls (milliseconds) */ + @JsonProperty("totalApiDurationMs") Double totalApiDurationMs, + /** Session start timestamp (epoch milliseconds) */ + @JsonProperty("sessionStartTime") Long sessionStartTime, + /** Aggregated code change metrics */ + @JsonProperty("codeChanges") UsageMetricsCodeChanges codeChanges, + /** Per-model token and request metrics, keyed by model identifier */ + @JsonProperty("modelMetrics") Map modelMetrics, + /** Currently active model identifier */ + @JsonProperty("currentModel") String currentModel, + /** Input tokens from the most recent main-agent API call */ + @JsonProperty("lastCallInputTokens") Long lastCallInputTokens, + /** Output tokens from the most recent main-agent API call */ + @JsonProperty("lastCallOutputTokens") Long lastCallOutputTokens +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceApi.java new file mode 100644 index 000000000..8fd7d7467 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceApi.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code workspace} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionWorkspaceApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionWorkspaceApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Invokes {@code session.workspace.listFiles}. + * @since 1.0.0 + */ + public CompletableFuture listFiles() { + return caller.invoke("session.workspace.listFiles", java.util.Map.of("sessionId", this.sessionId), SessionWorkspaceListFilesResult.class); + } + + /** + * Invokes {@code session.workspace.readFile}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture readFile(SessionWorkspaceReadFileParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspace.readFile", _p, SessionWorkspaceReadFileResult.class); + } + + /** + * Invokes {@code session.workspace.createFile}. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture createFile(SessionWorkspaceCreateFileParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspace.createFile", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileParams.java new file mode 100644 index 000000000..c25fdd790 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.workspace.createFile} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceCreateFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Relative path within the workspace files directory */ + @JsonProperty("path") String path, + /** File content to write as a UTF-8 string */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileResult.java new file mode 100644 index 000000000..e77cf58c5 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceCreateFileResult.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.workspace.createFile} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceCreateFileResult() { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesParams.java new file mode 100644 index 000000000..0fb6431c9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.workspace.listFiles} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceListFilesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesResult.java new file mode 100644 index 000000000..1b46df541 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceListFilesResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.workspace.listFiles} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceListFilesResult( + /** Relative file paths in the workspace files directory */ + @JsonProperty("files") List files +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileParams.java new file mode 100644 index 000000000..ded74763a --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code session.workspace.readFile} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceReadFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Relative path within the workspace files directory */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileResult.java new file mode 100644 index 000000000..c8705581e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspaceReadFileResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result for the {@code session.workspace.readFile} RPC method. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspaceReadFileResult( + /** File content as a UTF-8 string */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java new file mode 100644 index 000000000..a88463737 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesApi.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code workspaces} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionWorkspacesApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionWorkspacesApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture getWorkspace() { + return caller.invoke("session.workspaces.getWorkspace", java.util.Map.of("sessionId", this.sessionId), SessionWorkspacesGetWorkspaceResult.class); + } + + /** + * Identifies the target session. + * @since 1.0.0 + */ + public CompletableFuture listFiles() { + return caller.invoke("session.workspaces.listFiles", java.util.Map.of("sessionId", this.sessionId), SessionWorkspacesListFilesResult.class); + } + + /** + * Relative path of the workspace file to read. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture readFile(SessionWorkspacesReadFileParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspaces.readFile", _p, SessionWorkspacesReadFileResult.class); + } + + /** + * Relative path and UTF-8 content for the workspace file to create or overwrite. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * @since 1.0.0 + */ + public CompletableFuture createFile(SessionWorkspacesCreateFileParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.workspaces.createFile", _p, Void.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesCreateFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesCreateFileParams.java new file mode 100644 index 000000000..b57681a17 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesCreateFileParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Relative path and UTF-8 content for the workspace file to create or overwrite. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesCreateFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Relative path within the workspace files directory */ + @JsonProperty("path") String path, + /** File content to write as a UTF-8 string */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceParams.java new file mode 100644 index 000000000..9f9628bb6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesGetWorkspaceParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java new file mode 100644 index 000000000..3772d5f93 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesGetWorkspaceResult.java @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.UUID; +import javax.annotation.processing.Generated; + +/** + * Current workspace metadata for the session, or null when not available. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesGetWorkspaceResult( + /** Current workspace metadata, or null if not available */ + @JsonProperty("workspace") SessionWorkspacesGetWorkspaceResultWorkspace workspace +) { + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionWorkspacesGetWorkspaceResultWorkspace( + @JsonProperty("id") UUID id, + @JsonProperty("cwd") String cwd, + @JsonProperty("git_root") String gitRoot, + @JsonProperty("repository") String repository, + @JsonProperty("host_type") SessionWorkspacesGetWorkspaceResultWorkspaceHostType hostType, + @JsonProperty("branch") String branch, + @JsonProperty("name") String name, + @JsonProperty("user_named") Boolean userNamed, + @JsonProperty("summary_count") Long summaryCount, + @JsonProperty("created_at") OffsetDateTime createdAt, + @JsonProperty("updated_at") OffsetDateTime updatedAt, + @JsonProperty("remote_steerable") Boolean remoteSteerable, + @JsonProperty("mc_task_id") String mcTaskId, + @JsonProperty("mc_session_id") String mcSessionId, + @JsonProperty("mc_last_event_id") String mcLastEventId, + @JsonProperty("chronicle_sync_dismissed") Boolean chronicleSyncDismissed + ) { + + public enum SessionWorkspacesGetWorkspaceResultWorkspaceHostType { + /** The {@code github} variant. */ + GITHUB("github"), + /** The {@code ado} variant. */ + ADO("ado"); + + private final String value; + SessionWorkspacesGetWorkspaceResultWorkspaceHostType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SessionWorkspacesGetWorkspaceResultWorkspaceHostType fromValue(String value) { + for (SessionWorkspacesGetWorkspaceResultWorkspaceHostType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SessionWorkspacesGetWorkspaceResultWorkspaceHostType value: " + value); + } + } + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesParams.java new file mode 100644 index 000000000..68b976a60 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesListFilesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesResult.java new file mode 100644 index 000000000..06908175b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesListFilesResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Relative paths of files stored in the session workspace files directory. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesListFilesResult( + /** Relative file paths in the workspace files directory */ + @JsonProperty("files") List files +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileParams.java new file mode 100644 index 000000000..e322ee06d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileParams.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Relative path of the workspace file to read. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesReadFileParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Relative path within the workspace files directory */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileResult.java new file mode 100644 index 000000000..7a0717dbe --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionWorkspacesReadFileResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Contents of the requested workspace file as a UTF-8 string. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionWorkspacesReadFileResult( + /** File content as a UTF-8 string */ + @JsonProperty("content") String content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectParams.java new file mode 100644 index 000000000..a8a9e76f6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Remote session connection parameters. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsConnectParams( + /** Session ID to connect to. */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectResult.java new file mode 100644 index 000000000..b67783328 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsConnectResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Remote session connection result. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsConnectResult( + /** SDK session ID for the connected remote session. */ + @JsonProperty("sessionId") String sessionId, + /** Metadata for a connected remote session. */ + @JsonProperty("metadata") ConnectedRemoteSessionMetadata metadata +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java new file mode 100644 index 000000000..19858ac97 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsForkParams( + /** Source session ID to fork from */ + @JsonProperty("sessionId") String sessionId, + /** Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. */ + @JsonProperty("toEventId") String toEventId, + /** Optional friendly name to assign to the forked session. */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java new file mode 100644 index 000000000..29bcb8c92 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionsForkResult.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Identifier and optional friendly name assigned to the newly forked session. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsForkResult( + /** The new forked session's ID */ + @JsonProperty("sessionId") String sessionId, + /** Friendly name assigned to the forked session, if any. */ + @JsonProperty("name") String name +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShellKillSignal.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShellKillSignal.java new file mode 100644 index 000000000..92700c5c0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ShellKillSignal.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Signal to send (default: SIGTERM) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ShellKillSignal { + /** The {@code SIGTERM} variant. */ + SIGTERM("SIGTERM"), + /** The {@code SIGKILL} variant. */ + SIGKILL("SIGKILL"), + /** The {@code SIGINT} variant. */ + SIGINT("SIGINT"); + + private final String value; + ShellKillSignal(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ShellKillSignal fromValue(String value) { + for (ShellKillSignal v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ShellKillSignal value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java new file mode 100644 index 000000000..cd896add8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Skill.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `Skill` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Skill( + /** Unique identifier for the skill */ + @JsonProperty("name") String name, + /** Description of what the skill does */ + @JsonProperty("description") String description, + /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + @JsonProperty("source") SkillSource source, + /** Whether the skill can be invoked by the user as a slash command */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether the skill is currently enabled */ + @JsonProperty("enabled") Boolean enabled, + /** Absolute path to the skill file */ + @JsonProperty("path") String path +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillSource.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillSource.java new file mode 100644 index 000000000..db5f405a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillSource.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Source location type (e.g., project, personal-copilot, plugin, builtin) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SkillSource { + /** The {@code project} variant. */ + PROJECT("project"), + /** The {@code inherited} variant. */ + INHERITED("inherited"), + /** The {@code personal-copilot} variant. */ + PERSONAL_COPILOT("personal-copilot"), + /** The {@code personal-agents} variant. */ + PERSONAL_AGENTS("personal-agents"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code custom} variant. */ + CUSTOM("custom"), + /** The {@code builtin} variant. */ + BUILTIN("builtin"); + + private final String value; + SkillSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SkillSource fromValue(String value) { + for (SkillSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SkillSource value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsConfigSetDisabledSkillsParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsConfigSetDisabledSkillsParams.java new file mode 100644 index 000000000..f704129dd --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsConfigSetDisabledSkillsParams.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Skill names to mark as disabled in global configuration, replacing any previous list. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillsConfigSetDisabledSkillsParams( + /** List of skill names to disable */ + @JsonProperty("disabledSkills") List disabledSkills +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverParams.java new file mode 100644 index 000000000..be1d1921f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional project paths and additional skill directories to include in discovery. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillsDiscoverParams( + /** Optional list of project directory paths to scan for project-scoped skills */ + @JsonProperty("projectPaths") List projectPaths, + /** Optional list of additional skill directory paths to include */ + @JsonProperty("skillDirectories") List skillDirectories +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverResult.java new file mode 100644 index 000000000..c80a73837 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SkillsDiscoverResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Skills discovered across global and project sources. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillsDiscoverResult( + /** All discovered skills across all sources */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java new file mode 100644 index 000000000..686018c51 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInfo.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Schema for the `SlashCommandInfo` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SlashCommandInfo( + /** Canonical command name without a leading slash */ + @JsonProperty("name") String name, + /** Canonical aliases without leading slashes */ + @JsonProperty("aliases") List aliases, + /** Human-readable command description */ + @JsonProperty("description") String description, + /** Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command */ + @JsonProperty("kind") SlashCommandKind kind, + /** Optional unstructured input hint */ + @JsonProperty("input") SlashCommandInput input, + /** Whether the command may run while an agent turn is active */ + @JsonProperty("allowDuringAgentExecution") Boolean allowDuringAgentExecution, + /** Whether the command is experimental */ + @JsonProperty("experimental") Boolean experimental +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java new file mode 100644 index 000000000..186dec5a8 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInput.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional unstructured input hint + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SlashCommandInput( + /** Hint to display when command input has not been provided */ + @JsonProperty("hint") String hint, + /** When true, the command requires non-empty input; clients should render the input hint as required */ + @JsonProperty("required") Boolean required, + /** Optional completion hint for the input (e.g. 'directory' for filesystem path completion) */ + @JsonProperty("completion") SlashCommandInputCompletion completion, + /** When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace */ + @JsonProperty("preserveMultilineInput") Boolean preserveMultilineInput +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java new file mode 100644 index 000000000..c192fa9c0 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandInputCompletion.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Optional completion hint for the input (e.g. 'directory' for filesystem path completion) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SlashCommandInputCompletion { + /** The {@code directory} variant. */ + DIRECTORY("directory"); + + private final String value; + SlashCommandInputCompletion(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SlashCommandInputCompletion fromValue(String value) { + for (SlashCommandInputCompletion v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SlashCommandInputCompletion value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java new file mode 100644 index 000000000..1f08c4efc --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/SlashCommandKind.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SlashCommandKind { + /** The {@code builtin} variant. */ + BUILTIN("builtin"), + /** The {@code skill} variant. */ + SKILL("skill"), + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + SlashCommandKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SlashCommandKind fromValue(String value) { + for (SlashCommandKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SlashCommandKind value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Tool.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Tool.java new file mode 100644 index 000000000..5954c2c03 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/Tool.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Schema for the `Tool` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Tool( + /** Tool identifier (e.g., "bash", "grep", "str_replace_editor") */ + @JsonProperty("name") String name, + /** Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP tools) */ + @JsonProperty("namespacedName") String namespacedName, + /** Description of what the tool does */ + @JsonProperty("description") String description, + /** JSON Schema for the tool's input parameters */ + @JsonProperty("parameters") Map parameters, + /** Optional instructions for how to use this tool effectively */ + @JsonProperty("instructions") String instructions +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListParams.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListParams.java new file mode 100644 index 000000000..3072c46eb --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListParams.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional model identifier whose tool overrides should be applied to the listing. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolsListParams( + /** Optional model ID β€” when provided, the returned tool list reflects model-specific overrides */ + @JsonProperty("model") String model +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListResult.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListResult.java new file mode 100644 index 000000000..30e3b0962 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/ToolsListResult.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Built-in tools available for the requested model, with their parameters and instructions. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolsListResult( + /** List of available built-in tools with metadata */ + @JsonProperty("tools") List tools +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponse.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponse.java new file mode 100644 index 000000000..058a68c0d --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponse.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * The elicitation response (accept with form values, decline, or cancel) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIElicitationResponse( + /** The user's response: accept (submitted), decline (rejected), or cancel (dismissed) */ + @JsonProperty("action") UIElicitationResponseAction action, + /** The form values submitted by the user (present when action is 'accept') */ + @JsonProperty("content") Map content +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponseAction.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponseAction.java new file mode 100644 index 000000000..e4811ef95 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationResponseAction.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UIElicitationResponseAction { + /** The {@code accept} variant. */ + ACCEPT("accept"), + /** The {@code decline} variant. */ + DECLINE("decline"), + /** The {@code cancel} variant. */ + CANCEL("cancel"); + + private final String value; + UIElicitationResponseAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UIElicitationResponseAction fromValue(String value) { + for (UIElicitationResponseAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UIElicitationResponseAction value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationSchema.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationSchema.java new file mode 100644 index 000000000..171f5c688 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UIElicitationSchema.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * JSON Schema describing the form fields to present to the user + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIElicitationSchema( + /** Schema type indicator (always 'object') */ + @JsonProperty("type") String type, + /** Form field definitions, keyed by field name */ + @JsonProperty("properties") Map properties, + /** List of required field names */ + @JsonProperty("required") List required +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java new file mode 100644 index 000000000..442c88da2 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsCodeChanges.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Aggregated code change metrics + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsCodeChanges( + /** Total lines of code added */ + @JsonProperty("linesAdded") Long linesAdded, + /** Total lines of code removed */ + @JsonProperty("linesRemoved") Long linesRemoved, + /** Number of distinct files modified */ + @JsonProperty("filesModifiedCount") Long filesModifiedCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java new file mode 100644 index 000000000..15a133323 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Schema for the `UsageMetricsModelMetric` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetric( + /** Request count and cost metrics for this model */ + @JsonProperty("requests") UsageMetricsModelMetricRequests requests, + /** Token usage metrics for this model */ + @JsonProperty("usage") UsageMetricsModelMetricUsage usage, + /** Accumulated nano-AI units cost for this model */ + @JsonProperty("totalNanoAiu") Long totalNanoAiu, + /** Token count details per type */ + @JsonProperty("tokenDetails") Map tokenDetails +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricRequests.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricRequests.java new file mode 100644 index 000000000..ac18ded85 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricRequests.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Request count and cost metrics for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricRequests( + /** Number of API requests made with this model */ + @JsonProperty("count") Long count, + /** User-initiated premium request cost (with multiplier applied) */ + @JsonProperty("cost") Double cost +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java new file mode 100644 index 000000000..1a64c76e9 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `UsageMetricsModelMetricTokenDetail` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricUsage.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricUsage.java new file mode 100644 index 000000000..f7c556a0f --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricUsage.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Token usage metrics for this model + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsModelMetricUsage( + /** Total input tokens consumed */ + @JsonProperty("inputTokens") Long inputTokens, + /** Total output tokens produced */ + @JsonProperty("outputTokens") Long outputTokens, + /** Total tokens read from prompt cache */ + @JsonProperty("cacheReadTokens") Long cacheReadTokens, + /** Total tokens written to prompt cache */ + @JsonProperty("cacheWriteTokens") Long cacheWriteTokens, + /** Total output tokens used for reasoning */ + @JsonProperty("reasoningTokens") Long reasoningTokens +) { +} diff --git a/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java new file mode 100644 index 000000000..1175c7b8b --- /dev/null +++ b/java/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.sdk.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Schema for the `UsageMetricsTokenDetail` type. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UsageMetricsTokenDetail( + /** Accumulated token count for this token type */ + @JsonProperty("tokenCount") Long tokenCount +) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/CliServerManager.java b/java/src/main/java/com/github/copilot/sdk/CliServerManager.java new file mode 100644 index 000000000..bd4effe5a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CliServerManager.java @@ -0,0 +1,337 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.github.copilot.sdk.json.CopilotClientOptions; + +/** + * Manages the lifecycle of the Copilot CLI server process. + *

+ * This class handles spawning the CLI server process, building command lines, + * detecting the listening port, and establishing connections. + */ +final class CliServerManager { + + private static final Logger LOG = Logger.getLogger(CliServerManager.class.getName()); + private static final int STDERR_READER_JOIN_TIMEOUT_MS = 5000; + + private final CopilotClientOptions options; + private final StringBuilder stderrBuffer = new StringBuilder(); + private volatile Thread stderrThread; + private String connectionToken; + + CliServerManager(CopilotClientOptions options) { + this.options = options; + } + + /** + * Sets the connection token to pass to the CLI process via environment + * variable. + * + * @param connectionToken + * the token, or {@code null} if not applicable + */ + void setConnectionToken(String connectionToken) { + this.connectionToken = connectionToken; + } + + /** + * Starts the CLI server process. + * + * @return information about the started process including detected port + * @throws IOException + * if the process cannot be started + * @throws InterruptedException + * if interrupted while waiting for port detection + */ + ProcessInfo startCliServer() throws IOException, InterruptedException { + clearStderrBuffer(); + + String cliPath = options.getCliPath() != null ? options.getCliPath() : "copilot"; + var args = new ArrayList(); + + if (options.getCliArgs() != null) { + args.addAll(Arrays.asList(options.getCliArgs())); + } + + args.add("--server"); + args.add("--no-auto-update"); + args.add("--log-level"); + args.add(options.getLogLevel()); + + if (options.isUseStdio()) { + args.add("--stdio"); + } else if (options.getPort() > 0) { + args.add("--port"); + args.add(String.valueOf(options.getPort())); + } + + // Add auth-related flags + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + args.add("--auth-token-env"); + args.add("COPILOT_SDK_AUTH_TOKEN"); + } + + // Default UseLoggedInUser to false when GitHubToken is provided + boolean useLoggedInUser = options.getUseLoggedInUser() + .orElse(options.getGitHubToken() == null || options.getGitHubToken().isEmpty()); + if (!useLoggedInUser) { + args.add("--no-auto-login"); + } + + if (options.getSessionIdleTimeoutSeconds().isPresent() + && options.getSessionIdleTimeoutSeconds().getAsInt() > 0) { + args.add("--session-idle-timeout"); + args.add(String.valueOf(options.getSessionIdleTimeoutSeconds().getAsInt())); + } + + if (options.isRemote()) { + args.add("--remote"); + } + + List command = resolveCliCommand(cliPath, args); + + var pb = new ProcessBuilder(command); + pb.redirectErrorStream(false); + + // Note: On Windows, console window visibility depends on how the parent Java + // process was launched. GUI applications started with 'javaw' will not create + // visible console windows for subprocesses. Console applications started with + // 'java' will share their console with subprocesses. Java's ProcessBuilder + // doesn't provide explicit CREATE_NO_WINDOW flags like native Windows APIs, + // but the default behavior is appropriate for most use cases. + + if (options.getCwd() != null) { + pb.directory(new File(options.getCwd())); + } + + if (options.getEnvironment() != null) { + pb.environment().clear(); + pb.environment().putAll(options.getEnvironment()); + } + pb.environment().remove("NODE_DEBUG"); + + // Set auth token in environment if provided + if (options.getGitHubToken() != null && !options.getGitHubToken().isEmpty()) { + pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken()); + } + + // Set Copilot home directory if configured + if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) { + pb.environment().put("COPILOT_HOME", options.getCopilotHome()); + } + + // Set connection token for TCP mode + if (connectionToken != null && !connectionToken.isEmpty()) { + pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken); + } + + // Set telemetry environment variables if configured + if (options.getTelemetry() != null) { + var telemetry = options.getTelemetry(); + pb.environment().put("COPILOT_OTEL_ENABLED", "true"); + if (telemetry.getOtlpEndpoint() != null) { + pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint()); + } + if (telemetry.getFilePath() != null) { + pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath()); + } + if (telemetry.getExporterType() != null) { + pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType()); + } + if (telemetry.getSourceName() != null) { + pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName()); + } + if (telemetry.getCaptureContent().isPresent()) { + pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", + telemetry.getCaptureContent().get() ? "true" : "false"); + } + } + + Process process = pb.start(); + + // Forward stderr to logger in background + startStderrReader(process); + + Integer detectedPort = null; + if (!options.isUseStdio()) { + detectedPort = waitForPortAnnouncement(process); + } + + return new ProcessInfo(process, detectedPort); + } + + /** + * Connects to a running Copilot server. + * + * @param process + * the CLI process (null if connecting to external server) + * @param tcpHost + * the host to connect to (null for stdio mode) + * @param tcpPort + * the port to connect to (null for stdio mode) + * @return the JSON-RPC client connected to the server + * @throws IOException + * if connection fails + */ + JsonRpcClient connectToServer(Process process, String tcpHost, Integer tcpPort) throws IOException { + if (tcpHost != null && tcpPort != null) { + // TCP mode: external server or child process with explicit port + Socket socket = new Socket(tcpHost, tcpPort); + return JsonRpcClient.fromSocket(socket); + } else if (process != null) { + // Stdio mode: child process + return JsonRpcClient.fromProcess(process); + } else { + throw new IllegalStateException("Cannot connect: no process for stdio and no host:port for TCP"); + } + } + + private void startStderrReader(Process process) { + var thread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (stderrBuffer) { + stderrBuffer.append(line).append('\n'); + } + LOG.fine("[CLI] " + line); + } + } catch (IOException e) { + LOG.log(Level.FINE, "Error reading stderr", e); + } + }, "cli-stderr-reader"); + thread.setDaemon(true); + thread.start(); + this.stderrThread = thread; + } + + private Integer waitForPortAnnouncement(Process process) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + Pattern portPattern = Pattern.compile("listening on port (\\d+)", Pattern.CASE_INSENSITIVE); + long deadline = System.currentTimeMillis() + 30000; + + while (System.currentTimeMillis() < deadline) { + String line = reader.readLine(); + if (line == null) { + awaitStderrReader(); + String stderr = getStderrOutput(); + throw new IOException(formatCliExitedMessage("CLI process exited unexpectedly.", stderr)); + } + + Matcher matcher = portPattern.matcher(line); + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } + } + + process.destroyForcibly(); + throw new IOException("Timeout waiting for CLI to announce port"); + } + } + + String getStderrOutput() { + synchronized (stderrBuffer) { + return stderrBuffer.toString().trim(); + } + } + + private void awaitStderrReader() { + Thread t = this.stderrThread; + if (t != null) { + try { + t.join(STDERR_READER_JOIN_TIMEOUT_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private void clearStderrBuffer() { + synchronized (stderrBuffer) { + stderrBuffer.setLength(0); + } + } + + static String formatCliExitedMessage(String message, String stderrOutput) { + if (stderrOutput == null || stderrOutput.isEmpty()) { + return message; + } + return message + "\nstderr: " + stderrOutput; + } + + private List resolveCliCommand(String cliPath, List args) { + boolean isJsFile = cliPath.toLowerCase().endsWith(".js"); + + if (isJsFile) { + var result = new ArrayList(); + result.add("node"); + result.add(cliPath); + result.addAll(args); + return result; + } + + // On Windows, use cmd /c to resolve the executable + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win") && !new File(cliPath).isAbsolute()) { + var result = new ArrayList(); + result.add("cmd"); + result.add("/c"); + result.add(cliPath); + result.addAll(args); + return result; + } + + var result = new ArrayList(); + result.add(cliPath); + result.addAll(args); + return result; + } + + static URI parseCliUrl(String url) { + // If it's just a port number, treat as localhost + try { + int port = Integer.parseInt(url); + return URI.create("http://localhost:" + port); + } catch (NumberFormatException e) { + // Not a port number, continue + } + + // Add scheme if missing + if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) { + url = "https://" + url; + } + + return URI.create(url); + } + + /** + * Information about a started CLI server process. + * + * @param process + * the CLI process + * @param port + * the detected TCP port (null for stdio mode) + */ + record ProcessInfo(Process process, Integer port) { + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/ConnectionState.java b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java new file mode 100644 index 000000000..d35528006 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/ConnectionState.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +/** + * Represents the connection state of a {@link CopilotClient}. + *

+ * The connection state indicates the current status of the client's connection + * to the Copilot CLI server. + * + * @see CopilotClient#getState() + * @since 1.0.0 + */ +public enum ConnectionState { + /** + * The client is not connected to the server. + */ + DISCONNECTED, + + /** + * The client is in the process of connecting to the server. + */ + CONNECTING, + + /** + * The client is connected and ready to accept requests. + */ + CONNECTED, + + /** + * The client encountered an error during connection or operation. + */ + ERROR +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotClient.java b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java new file mode 100644 index 000000000..4d0770319 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CopilotClient.java @@ -0,0 +1,933 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CreateSessionResponse; +import com.github.copilot.sdk.generated.rpc.ConnectParams; +import com.github.copilot.sdk.generated.rpc.ServerRpc; +import com.github.copilot.sdk.json.DeleteSessionResponse; +import com.github.copilot.sdk.json.GetAuthStatusResponse; +import com.github.copilot.sdk.json.GetLastSessionIdResponse; +import com.github.copilot.sdk.json.GetSessionMetadataResponse; +import com.github.copilot.sdk.json.GetModelsResponse; +import com.github.copilot.sdk.json.GetStatusResponse; +import com.github.copilot.sdk.json.ListSessionsResponse; +import com.github.copilot.sdk.json.ModelInfo; +import com.github.copilot.sdk.json.PingResponse; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionResponse; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionLifecycleHandler; +import com.github.copilot.sdk.json.SessionListFilter; +import com.github.copilot.sdk.json.SessionMetadata; + +/** + * Provides a client for interacting with the Copilot CLI server. + *

+ * The CopilotClient manages the connection to the Copilot CLI server and + * provides methods to create and manage conversation sessions. It can either + * spawn a CLI server process or connect to an existing server. + *

+ * Example usage: + * + *

{@code
+ * try (var client = new CopilotClient()) {
+ * 	client.start().get();
+ *
+ * 	var session = client
+ * 			.createSession(
+ * 					new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5"))
+ * 			.get();
+ *
+ * 	session.on(AssistantMessageEvent.class, msg -> {
+ * 		System.out.println(msg.getData().content());
+ * 	});
+ *
+ * 	session.send(new MessageOptions().setPrompt("Hello!")).get();
+ * }
+ * }
+ * + * @since 1.0.0 + */ +public final class CopilotClient implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(CopilotClient.class.getName()); + + /** + * Timeout, in seconds, used by {@link #close()} when waiting for graceful + * shutdown via {@link #stop()}. + */ + public static final int AUTOCLOSEABLE_TIMEOUT_SECONDS = 10; + private static final int FORCE_KILL_TIMEOUT_SECONDS = 10; + private final CopilotClientOptions options; + private final CliServerManager serverManager; + private final LifecycleEventManager lifecycleManager = new LifecycleEventManager(); + private final Map sessions = new ConcurrentHashMap<>(); + private volatile CompletableFuture connectionFuture; + private volatile boolean disposed = false; + private final String optionsHost; + private final Integer optionsPort; + private final String effectiveConnectionToken; + private volatile List modelsCache; + private final Object modelsCacheLock = new Object(); + + /** + * Creates a new CopilotClient with default options. + */ + public CopilotClient() { + this(new CopilotClientOptions()); + } + + /** + * Creates a new CopilotClient with the specified options. + * + * @param options + * Options for creating the client + * @throws IllegalArgumentException + * if mutually exclusive options are provided + */ + public CopilotClient(CopilotClientOptions options) { + this.options = options != null ? options : new CopilotClientOptions(); + + // When cliUrl is set, auto-correct useStdio since we're connecting via TCP + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { + this.options.setUseStdio(false); + } + + // Validate mutually exclusive options: cliUrl and cliPath cannot both be set + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() + && this.options.getCliPath() != null) { + throw new IllegalArgumentException("CliUrl is mutually exclusive with CliPath"); + } + + // Validate auth options with external server + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty() + && (this.options.getGitHubToken() != null || this.options.getUseLoggedInUser().isPresent())) { + throw new IllegalArgumentException( + "GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)"); + } + + // Validate tcpConnectionToken + if (this.options.getTcpConnectionToken() != null) { + if (this.options.getTcpConnectionToken().isEmpty()) { + throw new IllegalArgumentException("TcpConnectionToken must be a non-empty string"); + } + if (this.options.isUseStdio()) { + throw new IllegalArgumentException("TcpConnectionToken cannot be used with UseStdio = true"); + } + } + + // Compute effective connection token: use provided, or auto-generate for + // SDK-spawned TCP mode, or null for stdio/external server + boolean sdkSpawnsCli = !this.options.isUseStdio() + && (this.options.getCliUrl() == null || this.options.getCliUrl().isEmpty()); + this.effectiveConnectionToken = this.options.getTcpConnectionToken() != null + ? this.options.getTcpConnectionToken() + : (sdkSpawnsCli ? java.util.UUID.randomUUID().toString() : null); + + // Parse CliUrl if provided + if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { + URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); + this.optionsHost = uri.getHost(); + this.optionsPort = uri.getPort(); + } else { + this.optionsHost = null; + this.optionsPort = null; + } + + this.serverManager = new CliServerManager(this.options); + this.serverManager.setConnectionToken(this.effectiveConnectionToken); + } + + /** + * Starts the Copilot client and connects to the server. + * + * @return A future that completes when the connection is established + */ + public CompletableFuture start() { + if (connectionFuture == null) { + synchronized (this) { + if (connectionFuture == null) { + connectionFuture = startCore(); + } + } + } + return connectionFuture.thenApply(c -> null); + } + + private CompletableFuture startCore() { + LOG.fine("Starting Copilot client"); + + Executor exec = options.getExecutor(); + try { + return exec != null + ? CompletableFuture.supplyAsync(this::startCoreBody, exec) + : CompletableFuture.supplyAsync(this::startCoreBody); + } catch (RejectedExecutionException e) { + return CompletableFuture.failedFuture(e); + } + } + + private Connection startCoreBody() { + Process process = null; + long startNanos = System.nanoTime(); + try { + JsonRpcClient rpc; + + if (optionsHost != null && optionsPort != null) { + // External server (TCP) + rpc = serverManager.connectToServer(null, optionsHost, optionsPort); + } else { + // Child process (stdio or TCP) + CliServerManager.ProcessInfo processInfo = serverManager.startCliServer(); + process = processInfo.process(); + rpc = serverManager.connectToServer(process, processInfo.port() != null ? "localhost" : null, + processInfo.port()); + } + + LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.start transport setup complete. Elapsed={Elapsed}", + startNanos); + + Connection connection = new Connection(rpc, process, new ServerRpc(rpc::invoke)); + + // Register handlers for server-to-client calls + RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch, + options.getExecutor()); + dispatcher.registerHandlers(rpc); + + // Verify protocol version + verifyProtocolVersion(connection); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.start protocol verification complete. Elapsed={Elapsed}", startNanos); + + LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.start complete. Elapsed={Elapsed}", startNanos); + return connection; + } catch (Exception e) { + if (!(e instanceof java.util.concurrent.CancellationException)) { + LoggingHelpers.logTiming(LOG, Level.WARNING, e, "CopilotClient.start failed. Elapsed={Elapsed}", + startNanos); + } + // Clean up the spawned process if connection setup failed + if (process != null) { + cleanupCliProcess(process); + } + String stderr = serverManager.getStderrOutput(); + if (!stderr.isEmpty()) { + throw new CompletionException(new IOException( + CliServerManager.formatCliExitedMessage("CLI process exited unexpectedly.", stderr), e)); + } + throw new CompletionException(e); + } + } + + private static final int MIN_PROTOCOL_VERSION = 2; + private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601; + + private void verifyProtocolVersion(Connection connection) throws Exception { + int expectedVersion = SdkProtocolVersion.get(); + Integer serverVersion; + + try { + // Try the new 'connect' RPC which supports connection tokens + var connectParams = new ConnectParams(effectiveConnectionToken); + var connectResponse = connection.rpc + .invoke("connect", connectParams, com.github.copilot.sdk.generated.rpc.ConnectResult.class) + .get(30, TimeUnit.SECONDS); + serverVersion = connectResponse.protocolVersion() != null + ? connectResponse.protocolVersion().intValue() + : null; + } catch (Exception e) { + // Unwrap CompletionException/ExecutionException to check inner cause + Throwable cause = e; + while (cause instanceof java.util.concurrent.ExecutionException || cause instanceof CompletionException) { + cause = cause.getCause(); + } + if (cause instanceof JsonRpcException rpcEx && isUnsupportedConnectMethod(rpcEx)) { + // Legacy server without 'connect'; fall back to 'ping'. + // A token, if any, is silently dropped β€” the legacy server can't enforce one. + var params = new HashMap(); + params.put("message", null); + PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, + TimeUnit.SECONDS); + serverVersion = pingResponse.protocolVersion(); + } else { + throw e; + } + } + + if (serverVersion == null) { + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server does not report a protocol version. " + + "Please update your server to ensure compatibility."); + } + + if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) { + throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION + + "-" + expectedVersion + ", but server reports version " + serverVersion + ". " + + "Please update your SDK or server to ensure compatibility."); + } + } + + private static boolean isUnsupportedConnectMethod(JsonRpcException ex) { + return ex.getCode() == METHOD_NOT_FOUND_ERROR_CODE || "Unhandled method connect".equals(ex.getMessage()); + } + + /** + * Disconnects from the Copilot server and closes all active sessions. + *

+ * This method performs graceful cleanup: + *

    + *
  1. Closes all active sessions (releases in-memory resources)
  2. + *
  3. Closes the JSON-RPC connection
  4. + *
  5. Terminates the CLI server process (if spawned by this client)
  6. + *
+ *

+ * Note: session data on disk is preserved, so sessions can be resumed later. To + * permanently remove session data before stopping, call + * {@link #deleteSession(String)} for each session first. + * + * @return A future that completes when the client is stopped + */ + public CompletableFuture stop() { + var closeFutures = new ArrayList>(); + Executor exec = options.getExecutor(); + + for (CopilotSession session : new ArrayList<>(sessions.values())) { + Runnable closeTask = () -> { + try { + session.close(); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error closing session " + session.getSessionId(), e); + } + }; + CompletableFuture future; + try { + future = exec != null + ? CompletableFuture.runAsync(closeTask, exec) + : CompletableFuture.runAsync(closeTask); + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected session close task; closing inline", e); + closeTask.run(); + future = CompletableFuture.completedFuture(null); + } + closeFutures.add(future); + } + sessions.clear(); + + return CompletableFuture.allOf(closeFutures.toArray(new CompletableFuture[0])) + .thenCompose(v -> cleanupConnection()); + } + + /** + * Forces an immediate stop of the client without graceful cleanup. + * + * @return A future that completes when the client is stopped + */ + public CompletableFuture forceStop() { + disposed = true; + sessions.clear(); + return cleanupConnection(); + } + + private CompletableFuture cleanupConnection() { + CompletableFuture future = connectionFuture; + connectionFuture = null; + + // Clear models cache + modelsCache = null; + + if (future == null) { + return CompletableFuture.completedFuture(null); + } + + return future.thenAccept(connection -> { + try { + connection.rpc.close(); + } catch (Exception e) { + LOG.log(Level.FINE, "Error closing RPC", e); + } + + if (connection.process != null) { + cleanupCliProcess(connection.process); + } + }).exceptionally(ex -> { + LOG.log(Level.FINE, "Ignoring failed Copilot client startup during cleanup", ex); + return null; + }); + } + + private static void cleanupCliProcess(Process process) { + try { + if (process.isAlive()) { + Process destroyedProcess = process.destroyForcibly(); + if (!destroyedProcess.waitFor(FORCE_KILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + LOG.fine("Process did not terminate within force kill timeout"); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.log(Level.FINE, "Interrupted while killing process", e); + } catch (Exception e) { + LOG.log(Level.FINE, "Error killing process", e); + } + } + + /** + * Creates a new Copilot session with the specified configuration. + *

+ * The session maintains conversation state and can be used to send messages and + * receive responses. Remember to close the session when done. + *

+ * A permission handler is required when creating a session. Use + * {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve + * all permission requests, or provide a custom handler to control permissions + * selectively. + * + *

+ * Example: + * + *

{@code
+     * var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+     * }
+ * + * @param config + * configuration for the session, including the required + * {@link SessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)} + * handler + * @return a future that resolves with the created CopilotSession + * @throws IllegalArgumentException + * if {@code config} is {@code null} or does not have a permission + * handler set + * @see SessionConfig + * @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL + */ + public CompletableFuture createSession(SessionConfig config) { + if (config == null || config.getOnPermissionRequest() == null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("An onPermissionRequest handler is required when creating a session. " + + "For example, to allow all permissions, use: " + + "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); + } + return ensureConnected().thenCompose(connection -> { + long totalNanos = System.nanoTime(); + // Pre-generate session ID so the session can be registered before the RPC call, + // ensuring no events emitted by the CLI during creation are lost. + String sessionId = config.getSessionId() != null + ? config.getSessionId() + : java.util.UUID.randomUUID().toString(); + + long setupNanos = System.nanoTime(); + var session = new CopilotSession(sessionId, connection.rpc); + if (options.getExecutor() != null) { + session.setExecutor(options.getExecutor()); + } + SessionRequestBuilder.configureSession(session, config); + sessions.put(sessionId, session); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession local setup complete. Elapsed={Elapsed}, SessionId=" + sessionId, + setupNanos); + + // Extract transform callbacks from the system message config. + // Callbacks are registered with the session; a wire-safe copy of the + // system message (with transform sections replaced by action="transform") + // is used in the RPC request. + var extracted = SessionRequestBuilder.extractTransformCallbacks(config.getSystemMessage()); + if (extracted.transformCallbacks() != null) { + session.registerTransformCallbacks(extracted.transformCallbacks()); + } + + var request = SessionRequestBuilder.buildCreateRequest(config, sessionId); + if (extracted.wireSystemMessage() != config.getSystemMessage()) { + request.setSystemMessage(extracted.wireSystemMessage()); + } + + long rpcNanos = System.nanoTime(); + return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession session creation request completed. Elapsed={Elapsed}, SessionId=" + + sessionId, + rpcNanos); + session.setWorkspacePath(response.workspacePath()); + session.setCapabilities(response.capabilities()); + // If the server returned a different sessionId (e.g. a v2 CLI that ignores + // the client-supplied ID), re-key the sessions map. + String returnedId = response.sessionId(); + if (returnedId != null && !returnedId.equals(sessionId)) { + sessions.remove(sessionId); + session.setActiveSessionId(returnedId); + sessions.put(returnedId, session); + } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); + return session; + }).exceptionally(ex -> { + sessions.remove(sessionId); + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotClient.createSession failed. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); + }); + }); + } + + /** + * Resumes an existing Copilot session. + *

+ * This restores a previously saved session, allowing you to continue a + * conversation. The session's history is preserved. + *

+ * A permission handler is required when resuming a session. Use + * {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve + * all permission requests, or provide a custom handler to control permissions + * selectively. + * + * @param sessionId + * the ID of the session to resume + * @param config + * configuration for the resumed session, including the required + * {@link ResumeSessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)} + * handler + * @return a future that resolves with the resumed CopilotSession + * @throws IllegalArgumentException + * if {@code config} is {@code null} or does not have a permission + * handler set + * @see #listSessions() + * @see #getLastSessionId() + * @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL + */ + public CompletableFuture resumeSession(String sessionId, ResumeSessionConfig config) { + if (config == null || config.getOnPermissionRequest() == null) { + return CompletableFuture.failedFuture( + new IllegalArgumentException("An onPermissionRequest handler is required when resuming a session. " + + "For example, to allow all permissions, use: " + + "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)")); + } + return ensureConnected().thenCompose(connection -> { + long totalNanos = System.nanoTime(); + // Register the session before the RPC call to avoid missing early events. + long setupNanos = System.nanoTime(); + var session = new CopilotSession(sessionId, connection.rpc); + if (options.getExecutor() != null) { + session.setExecutor(options.getExecutor()); + } + SessionRequestBuilder.configureSession(session, config); + sessions.put(sessionId, session); + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession local setup complete. Elapsed={Elapsed}, SessionId=" + sessionId, + setupNanos); + + // Extract transform callbacks from the system message config. + var extracted = SessionRequestBuilder.extractTransformCallbacks(config.getSystemMessage()); + if (extracted.transformCallbacks() != null) { + session.registerTransformCallbacks(extracted.transformCallbacks()); + } + + var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); + if (extracted.wireSystemMessage() != config.getSystemMessage()) { + request.setSystemMessage(extracted.wireSystemMessage()); + } + + long rpcNanos = System.nanoTime(); + return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession session resume request completed. Elapsed={Elapsed}, SessionId=" + + sessionId, + rpcNanos); + session.setWorkspacePath(response.workspacePath()); + session.setCapabilities(response.capabilities()); + // If the server returned a different sessionId than what was requested, re-key. + String returnedId = response.sessionId(); + if (returnedId != null && !returnedId.equals(sessionId)) { + sessions.remove(sessionId); + session.setActiveSessionId(returnedId); + sessions.put(returnedId, session); + } + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotClient.resumeSession complete. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); + return session; + }).exceptionally(ex -> { + sessions.remove(sessionId); + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotClient.resumeSession failed. Elapsed={Elapsed}, SessionId=" + sessionId, totalNanos); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); + }); + }); + } + + /** + * Gets the current connection state. + * + * @return the current connection state + * @see ConnectionState + */ + public ConnectionState getState() { + if (connectionFuture == null) + return ConnectionState.DISCONNECTED; + if (connectionFuture.isCompletedExceptionally()) + return ConnectionState.ERROR; + if (!connectionFuture.isDone()) + return ConnectionState.CONNECTING; + return ConnectionState.CONNECTED; + } + + /** + * Returns the typed RPC client for server-level methods. + *

+ * Provides strongly-typed access to all server-level API namespaces such as + * {@code models}, {@code tools}, {@code account}, and {@code mcp}. + *

+ * Example usage: + * + *

{@code
+     * client.start().get();
+     * var models = client.getRpc().models.list().get();
+     * }
+ * + * @return the server-level typed RPC client + * @throws IllegalStateException + * if the client is not connected; call {@link #start()} first + * @since 1.0.0 + */ + public ServerRpc getRpc() { + CompletableFuture future = connectionFuture; + if (future == null || !future.isDone() || future.isCompletedExceptionally()) { + throw new IllegalStateException("Client not connected; call start() first"); + } + return future.join().serverRpc(); + } + + /** + * Pings the server to check connectivity. + *

+ * This can be used to verify that the server is responsive and to check the + * protocol version. + * + * @param message + * an optional message to echo back + * @return a future that resolves with the ping response + * @see PingResponse + */ + public CompletableFuture ping(String message) { + return ensureConnected().thenCompose(connection -> connection.rpc.invoke("ping", + Map.of("message", message != null ? message : ""), PingResponse.class)); + } + + /** + * Gets CLI status including version and protocol information. + * + * @return a future that resolves with the status response containing version + * and protocol version + * @see GetStatusResponse + */ + public CompletableFuture getStatus() { + return ensureConnected() + .thenCompose(connection -> connection.rpc.invoke("status.get", Map.of(), GetStatusResponse.class)); + } + + /** + * Gets current authentication status. + * + * @return a future that resolves with the authentication status + * @see GetAuthStatusResponse + */ + public CompletableFuture getAuthStatus() { + return ensureConnected().thenCompose( + connection -> connection.rpc.invoke("auth.getStatus", Map.of(), GetAuthStatusResponse.class)); + } + + /** + * Lists available models with their metadata. + *

+ * Results are cached after the first successful call to avoid rate limiting. + * The cache is cleared when the client disconnects. + *

+ * If an {@code onListModels} handler was provided in + * {@link com.github.copilot.sdk.json.CopilotClientOptions}, it is called + * instead of querying the CLI server. This is useful in BYOK mode. + * + * @return a future that resolves with a list of available models + * @see ModelInfo + */ + public CompletableFuture> listModels() { + // Check cache first + List cached = modelsCache; + if (cached != null) { + return CompletableFuture.completedFuture(new ArrayList<>(cached)); + } + + // If a custom handler is configured, use it instead of querying the CLI server + var onListModels = options.getOnListModels(); + if (onListModels != null) { + synchronized (modelsCacheLock) { + if (modelsCache != null) { + return CompletableFuture.completedFuture(new ArrayList<>(modelsCache)); + } + } + return onListModels.get().thenApply(models -> { + synchronized (modelsCacheLock) { + modelsCache = models; + } + return new ArrayList<>(models); + }); + } + + return ensureConnected().thenCompose(connection -> { + // Double-check cache inside lock + synchronized (modelsCacheLock) { + if (modelsCache != null) { + return CompletableFuture.completedFuture(new ArrayList<>(modelsCache)); + } + } + + return connection.rpc.invoke("models.list", Map.of(), GetModelsResponse.class).thenApply(response -> { + List models = response.getModels(); + synchronized (modelsCacheLock) { + modelsCache = models; + } + return new ArrayList<>(models); // Return a copy to prevent cache mutation + }); + }); + } + + /** + * Gets the ID of the most recently used session. + *

+ * This is useful for resuming the last conversation without needing to list all + * sessions. + * + * @return a future that resolves with the last session ID, or {@code null} if + * no sessions exist + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) + */ + public CompletableFuture getLastSessionId() { + return ensureConnected().thenCompose( + connection -> connection.rpc.invoke("session.getLastId", Map.of(), GetLastSessionIdResponse.class) + .thenApply(GetLastSessionIdResponse::sessionId)); + } + + /** + * Permanently deletes a session and all its data from disk, including + * conversation history, planning state, and artifacts. + *

+ * Unlike {@link CopilotSession#close()}, which only releases in-memory + * resources and preserves session data for later resumption, this method is + * irreversible. The session cannot be resumed after deletion. + * + * @param sessionId + * the ID of the session to delete + * @return a future that completes when the session is deleted + * @throws RuntimeException + * if the deletion fails + */ + public CompletableFuture deleteSession(String sessionId) { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.delete", Map.of("sessionId", sessionId), DeleteSessionResponse.class) + .thenAccept(response -> { + if (!response.success()) { + throw new RuntimeException("Failed to delete session " + sessionId + ": " + response.error()); + } + sessions.remove(sessionId); + })); + } + + /** + * Lists all available sessions. + *

+ * Returns metadata about all sessions that can be resumed, including their IDs, + * start times, and summaries. + * + * @return a future that resolves with a list of session metadata + * @see SessionMetadata + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) + */ + public CompletableFuture> listSessions() { + return listSessions(null); + } + + /** + * Lists all available sessions with optional filtering. + *

+ * Returns metadata about all sessions that can be resumed, including their IDs, + * start times, summaries, and context information. Use the filter parameter to + * narrow down sessions by working directory, git repository, or branch. + * + *

Example Usage

+ * + *
{@code
+     * // List all sessions
+     * var allSessions = client.listSessions().get();
+     *
+     * // Filter by repository
+     * var filter = new SessionListFilter().setRepository("owner/repo");
+     * var repoSessions = client.listSessions(filter).get();
+     * }
+ * + * @param filter + * optional filter to narrow down sessions by context fields, or + * {@code null} to list all sessions + * @return a future that resolves with a list of session metadata + * @see SessionMetadata + * @see SessionListFilter + * @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig) + */ + public CompletableFuture> listSessions(SessionListFilter filter) { + return ensureConnected().thenCompose(connection -> { + Map params = filter != null ? Map.of("filter", filter) : Map.of(); + return connection.rpc.invoke("session.list", params, ListSessionsResponse.class) + .thenApply(ListSessionsResponse::sessions); + }); + } + + /** + * Gets metadata for a specific session by ID. + *

+ * This provides an efficient O(1) lookup of a single session's metadata instead + * of listing all sessions. + * + *

Example Usage

+ * + *
{@code
+     * var metadata = client.getSessionMetadata("session-123").get();
+     * if (metadata != null) {
+     * 	System.out.println("Session started at: " + metadata.getStartTime());
+     * }
+     * }
+ * + * @param sessionId + * the ID of the session to look up + * @return a future that resolves with the {@link SessionMetadata}, or + * {@code null} if the session was not found + * @see SessionMetadata + * @since 1.0.0 + */ + public CompletableFuture getSessionMetadata(String sessionId) { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.getMetadata", Map.of("sessionId", sessionId), GetSessionMetadataResponse.class) + .thenApply(GetSessionMetadataResponse::session)); + } + + /** + * Gets the ID of the session currently displayed in the TUI. + *

+ * This is only available when connecting to a server running in TUI+server mode + * (--ui-server). + * + * @return a future that resolves with the session ID, or null if no foreground + * session is set + */ + public CompletableFuture getForegroundSessionId() { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.getForeground", Map.of(), + com.github.copilot.sdk.json.GetForegroundSessionResponse.class) + .thenApply(com.github.copilot.sdk.json.GetForegroundSessionResponse::sessionId)); + } + + /** + * Requests the TUI to switch to displaying the specified session. + *

+ * This is only available when connecting to a server running in TUI+server mode + * (--ui-server). + * + * @param sessionId + * the ID of the session to display in the TUI + * @return a future that completes when the operation is done + * @throws RuntimeException + * if the operation fails + */ + public CompletableFuture setForegroundSessionId(String sessionId) { + return ensureConnected().thenCompose(connection -> connection.rpc + .invoke("session.setForeground", new com.github.copilot.sdk.json.SetForegroundSessionRequest(sessionId), + com.github.copilot.sdk.json.SetForegroundSessionResponse.class) + .thenAccept(response -> { + if (!response.success()) { + throw new RuntimeException( + response.error() != null ? response.error() : "Failed to set foreground session"); + } + })); + } + + /** + * Subscribes to all session lifecycle events. + *

+ * Lifecycle events are emitted when sessions are created, deleted, updated, or + * change foreground/background state (in TUI+server mode). + * + * @param handler + * a callback that receives lifecycle events + * @return an AutoCloseable that, when closed, unsubscribes the handler + */ + public AutoCloseable onLifecycle(SessionLifecycleHandler handler) { + return lifecycleManager.subscribe(handler); + } + + /** + * Subscribes to a specific session lifecycle event type. + * + * @param eventType + * the event type to listen for (use + * {@link com.github.copilot.sdk.json.SessionLifecycleEventTypes} + * constants) + * @param handler + * a callback that receives events of the specified type + * @return an AutoCloseable that, when closed, unsubscribes the handler + */ + public AutoCloseable onLifecycle(String eventType, SessionLifecycleHandler handler) { + return lifecycleManager.subscribe(eventType, handler); + } + + private CompletableFuture ensureConnected() { + if (connectionFuture == null && !options.isAutoStart()) { + throw new IllegalStateException("Client not connected. Call start() first."); + } + + start(); + return connectionFuture; + } + + /** + * Closes this client using graceful shutdown semantics. + *

+ * This method is intended for {@code try-with-resources} usage and blocks while + * waiting for {@link #stop()} to complete, up to + * {@link #AUTOCLOSEABLE_TIMEOUT_SECONDS} seconds. If shutdown fails or times + * out, the error is logged at {@link Level#FINE} and the method returns. + *

+ * This method is idempotent. + * + * @see #stop() + * @see #forceStop() + * @see #AUTOCLOSEABLE_TIMEOUT_SECONDS + */ + @Override + public void close() { + if (disposed) + return; + disposed = true; + try { + stop().get(AUTOCLOSEABLE_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception e) { + LOG.log(Level.FINE, "Error during close", e); + } + } + + private static record Connection(JsonRpcClient rpc, Process process, ServerRpc serverRpc) { + }; + +} diff --git a/java/src/main/java/com/github/copilot/sdk/CopilotSession.java b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java new file mode 100644 index 000000000..5fb8733a2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -0,0 +1,1964 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.rpc.SessionCommandsHandlePendingCommandParams; +import com.github.copilot.sdk.generated.rpc.SessionLogParams; +import com.github.copilot.sdk.generated.rpc.SessionLogLevel; +import com.github.copilot.sdk.generated.rpc.ModelCapabilitiesOverride; +import com.github.copilot.sdk.generated.rpc.ModelCapabilitiesOverrideLimits; +import com.github.copilot.sdk.generated.rpc.ModelCapabilitiesOverrideSupports; +import com.github.copilot.sdk.generated.rpc.SessionModelSwitchToParams; +import com.github.copilot.sdk.generated.rpc.SessionPermissionsHandlePendingPermissionRequestParams; +import com.github.copilot.sdk.generated.rpc.SessionRpc; +import com.github.copilot.sdk.generated.rpc.SessionToolsHandlePendingToolCallParams; +import com.github.copilot.sdk.generated.rpc.SessionUiElicitationParams; +import com.github.copilot.sdk.generated.rpc.SessionUiHandlePendingElicitationParams; +import com.github.copilot.sdk.generated.rpc.UIElicitationResponse; +import com.github.copilot.sdk.generated.rpc.UIElicitationResponseAction; +import com.github.copilot.sdk.generated.rpc.UIElicitationSchema; +import com.github.copilot.sdk.generated.CapabilitiesChangedEvent; +import com.github.copilot.sdk.generated.CommandExecuteEvent; +import com.github.copilot.sdk.generated.ElicitationRequestedEvent; +import com.github.copilot.sdk.generated.ExternalToolRequestedEvent; +import com.github.copilot.sdk.generated.PermissionRequestedEvent; +import com.github.copilot.sdk.generated.SessionErrorEvent; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.AgentInfo; +import com.github.copilot.sdk.json.AutoModeSwitchHandler; +import com.github.copilot.sdk.json.AutoModeSwitchInvocation; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; +import com.github.copilot.sdk.json.CommandContext; +import com.github.copilot.sdk.json.CommandDefinition; +import com.github.copilot.sdk.json.CommandHandler; +import com.github.copilot.sdk.json.ElicitationContext; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationParams; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ExitPlanModeHandler; +import com.github.copilot.sdk.json.ExitPlanModeInvocation; +import com.github.copilot.sdk.json.ExitPlanModeRequest; +import com.github.copilot.sdk.json.ExitPlanModeResult; +import com.github.copilot.sdk.json.ElicitationSchema; +import com.github.copilot.sdk.json.GetMessagesResponse; +import com.github.copilot.sdk.json.HookInvocation; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PermissionInvocation; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.PostToolUseHookInput; +import com.github.copilot.sdk.json.PreToolUseHookInput; +import com.github.copilot.sdk.json.SendMessageRequest; +import com.github.copilot.sdk.json.SendMessageResponse; +import com.github.copilot.sdk.json.SessionCapabilities; +import com.github.copilot.sdk.json.SessionEndHookInput; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.SessionStartHookInput; +import com.github.copilot.sdk.json.SessionUiApi; +import com.github.copilot.sdk.json.SessionUiCapabilities; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; +import com.github.copilot.sdk.json.UserInputHandler; +import com.github.copilot.sdk.json.UserInputInvocation; +import com.github.copilot.sdk.json.UserInputRequest; +import com.github.copilot.sdk.json.UserInputResponse; +import com.github.copilot.sdk.json.UserPromptSubmittedHookInput; + +/** + * Represents a single conversation session with the Copilot CLI. + *

+ * A session maintains conversation state, handles events, and manages tool + * execution. Sessions are created via {@link CopilotClient#createSession} or + * resumed via {@link CopilotClient#resumeSession}. + *

+ * {@code CopilotSession} implements {@link AutoCloseable}. Use the + * try-with-resources pattern for automatic cleanup, or call {@link #close()} + * explicitly. Closing a session releases in-memory resources but preserves + * session data on disk β€” the conversation can be resumed later via + * {@link CopilotClient#resumeSession}. To permanently delete session data, use + * {@link CopilotClient#deleteSession}. + * + *

Example Usage

+ * + *
{@code
+ * // Create a session with a permission handler (required)
+ * var session = client
+ * 		.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5"))
+ * 		.get();
+ *
+ * // Register type-safe event handlers
+ * session.on(AssistantMessageEvent.class, msg -> {
+ * 	System.out.println(msg.getData().content());
+ * });
+ * session.on(SessionIdleEvent.class, idle -> {
+ * 	System.out.println("Session is idle");
+ * });
+ *
+ * // Send messages
+ * session.sendAndWait(new MessageOptions().setPrompt("Hello!")).get();
+ *
+ * // Clean up
+ * session.close();
+ * }
+ * + * @see CopilotClient#createSession(com.github.copilot.sdk.json.SessionConfig) + * @see CopilotClient#resumeSession(String, + * com.github.copilot.sdk.json.ResumeSessionConfig) + * @see SessionEvent + * @since 1.0.0 + */ +public final class CopilotSession implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName()); + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + /** + * The current active session ID. Initialized to the pre-generated value and may + * be updated after session.create / session.resume if the server returns a + * different ID (e.g. when working against a v2 CLI that ignores the + * client-supplied sessionId). + */ + private volatile String sessionId; + private volatile String workspacePath; + private volatile SessionCapabilities capabilities = new SessionCapabilities(); + private final SessionUiApi ui; + private final JsonRpcClient rpc; + private volatile SessionRpc sessionRpc; + private final Set> eventHandlers = ConcurrentHashMap.newKeySet(); + private final Map toolHandlers = new ConcurrentHashMap<>(); + private final Map commandHandlers = new ConcurrentHashMap<>(); + private final AtomicReference permissionHandler = new AtomicReference<>(); + private final AtomicReference userInputHandler = new AtomicReference<>(); + private final AtomicReference elicitationHandler = new AtomicReference<>(); + private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); + private final AtomicReference autoModeSwitchHandler = new AtomicReference<>(); + private final AtomicReference hooksHandler = new AtomicReference<>(); + private volatile EventErrorHandler eventErrorHandler; + private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; + private volatile Map>> transformCallbacks; + private final ScheduledExecutorService timeoutScheduler; + private volatile Executor executor; + + /** Tracks whether this session instance has been terminated via close(). */ + private volatile boolean isTerminated = false; + + /** + * Creates a new session with the given ID and RPC client. + *

+ * This constructor is package-private. Sessions should be created via + * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}. + * + * @param sessionId + * the unique session identifier + * @param rpc + * the JSON-RPC client for communication + */ + CopilotSession(String sessionId, JsonRpcClient rpc) { + this(sessionId, rpc, null); + } + + /** + * Creates a new session with the given ID, RPC client, and workspace path. + *

+ * This constructor is package-private. Sessions should be created via + * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}. + * + * @param sessionId + * the unique session identifier + * @param rpc + * the JSON-RPC client for communication + * @param workspacePath + * the workspace path if infinite sessions are enabled + */ + CopilotSession(String sessionId, JsonRpcClient rpc, String workspacePath) { + this.sessionId = sessionId; + this.rpc = rpc; + this.workspacePath = workspacePath; + this.ui = new SessionUiApiImpl(); + var executor = new ScheduledThreadPoolExecutor(1, r -> { + var t = new Thread(r, "sendAndWait-timeout"); + t.setDaemon(true); + return t; + }); + executor.setRemoveOnCancelPolicy(true); + this.timeoutScheduler = executor; + } + + /** + * Sets the executor for internal async operations. Package-private; called by + * CopilotClient after construction. + */ + void setExecutor(Executor executor) { + this.executor = executor; + } + + /** + * Gets the unique identifier for this session. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Updates the active session ID. Package-private; called by CopilotClient if + * the server returns a different session ID than the pre-generated one (e.g. + * when a v2 CLI ignores the client-supplied sessionId). + * + * @param sessionId + * the server-confirmed session ID + */ + void setActiveSessionId(String sessionId) { + this.sessionId = sessionId; + this.sessionRpc = null; // Reset so getRpc() lazily re-creates with the new sessionId + } + + /** + * Gets the path to the session workspace directory when infinite sessions are + * enabled. + *

+ * The workspace directory contains checkpoints/, plan.md, and files/ + * subdirectories. + * + * @return the workspace path, or {@code null} if infinite sessions are disabled + */ + public String getWorkspacePath() { + return workspacePath; + } + + /** + * Sets the workspace path. Package-private; called by CopilotClient after + * session.create or session.resume RPC response. + * + * @param workspacePath + * the workspace path + */ + void setWorkspacePath(String workspacePath) { + this.workspacePath = workspacePath; + } + + /** + * Gets the capabilities reported by the host for this session. + *

+ * Capabilities are populated from the session create/resume response and + * updated in real time via {@code capabilities.changed} events. + * + * @return the session capabilities (never {@code null}) + */ + public SessionCapabilities getCapabilities() { + return capabilities; + } + + /** + * Gets the UI API for eliciting information from the user during this session. + *

+ * All methods on this API throw {@link IllegalStateException} if the host does + * not report elicitation support via {@link #getCapabilities()}. + * + * @return the UI API + */ + public SessionUiApi getUi() { + return ui; + } + + /** + * Returns the typed RPC client for this session. + *

+ * Provides strongly-typed access to all session-level API namespaces. The + * {@code sessionId} is injected automatically into every call. + *

+ * Example usage: + * + *

{@code
+     * var agents = session.getRpc().agent.list().get();
+     * }
+ * + * @return the session-scoped typed RPC client (never {@code null}) + * @throws IllegalStateException + * if the session is not connected + * @since 1.0.0 + */ + public SessionRpc getRpc() { + if (rpc == null) { + throw new IllegalStateException("Session is not connected β€” RPC client is unavailable"); + } + SessionRpc current = sessionRpc; + if (current == null) { + synchronized (this) { + current = sessionRpc; + if (current == null) { + sessionRpc = current = new SessionRpc(rpc::invoke, sessionId); + } + } + } + return current; + } + + /** + * Sets a custom error handler for exceptions thrown by event handlers. + *

+ * When an event handler registered via {@link #on(Consumer)} or + * {@link #on(Class, Consumer)} throws an exception during event dispatch, the + * error handler is invoked with the event and exception. The error is always + * logged at {@link Level#WARNING} regardless of whether a custom handler is + * set. + * + *

+ * Whether dispatch continues or stops after an error is controlled by the + * {@link EventErrorPolicy} set via {@link #setEventErrorPolicy}. The error + * handler is always invoked regardless of the policy. + * + *

+ * If the error handler itself throws an exception, that exception is caught and + * logged at {@link Level#SEVERE}, and dispatch is stopped regardless of the + * configured policy. + * + *

+ * Example: + * + *

{@code
+     * session.setEventErrorHandler((event, exception) -> {
+     * 	metrics.increment("handler.errors");
+     * 	logger.error("Handler failed on {}: {}", event.getType(), exception.getMessage());
+     * });
+     * }
+ * + * @param handler + * the error handler, or {@code null} to use only the default logging + * behavior + * @throws IllegalStateException + * if this session has been terminated + * @see EventErrorHandler + * @see #setEventErrorPolicy(EventErrorPolicy) + * @since 1.0.8 + */ + public void setEventErrorHandler(EventErrorHandler handler) { + ensureNotTerminated(); + this.eventErrorHandler = handler; + } + + /** + * Sets the error propagation policy for event dispatch. + *

+ * Controls whether remaining event listeners continue to execute when a + * preceding listener throws an exception. Errors are always logged at + * {@link Level#WARNING} regardless of the policy. + * + *

    + *
  • {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS} (default) β€” log the + * error and stop dispatch after the first error
  • + *
  • {@link EventErrorPolicy#SUPPRESS_AND_LOG_ERRORS} β€” log the error and + * continue dispatching to all remaining listeners
  • + *
+ * + *

+ * The configured {@link EventErrorHandler} (if any) is always invoked + * regardless of the policy. + * + *

+ * Example: + * + *

{@code
+     * // Opt-in to suppress errors (continue dispatching despite errors)
+     * session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
+     * session.setEventErrorHandler((event, ex) -> logger.error("Handler failed, continuing: {}", ex.getMessage(), ex));
+     * }
+ * + * @param policy + * the error policy (default is + * {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS}) + * @throws IllegalStateException + * if this session has been terminated + * @see EventErrorPolicy + * @see #setEventErrorHandler(EventErrorHandler) + * @since 1.0.8 + */ + public void setEventErrorPolicy(EventErrorPolicy policy) { + ensureNotTerminated(); + if (policy == null) { + throw new NullPointerException("policy must not be null"); + } + this.eventErrorPolicy = policy; + } + + /** + * Sends a simple text message to the Copilot session. + *

+ * This is a convenience method equivalent to + * {@code send(new MessageOptions().setPrompt(prompt))}. + * + * @param prompt + * the message text to send + * @return a future that resolves with the message ID assigned by the server + * @throws IllegalStateException + * if this session has been terminated + * @see #send(MessageOptions) + */ + public CompletableFuture send(String prompt) { + ensureNotTerminated(); + return send(new MessageOptions().setPrompt(prompt)); + } + + /** + * Sends a simple text message and waits until the session becomes idle. + *

+ * This is a convenience method equivalent to + * {@code sendAndWait(new MessageOptions().setPrompt(prompt))}. + * + * @param prompt + * the message text to send + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received + * @throws IllegalStateException + * if this session has been terminated + * @see #sendAndWait(MessageOptions) + */ + public CompletableFuture sendAndWait(String prompt) { + ensureNotTerminated(); + return sendAndWait(new MessageOptions().setPrompt(prompt)); + } + + /** + * Sends a message to the Copilot session. + *

+ * This method sends a message asynchronously and returns immediately. Use + * {@link #sendAndWait(MessageOptions)} to wait for the response. + * + * @param options + * the message options containing the prompt and attachments + * @return a future that resolves with the message ID assigned by the server + * @throws IllegalStateException + * if this session has been terminated + * @see #sendAndWait(MessageOptions) + * @see #send(String) + */ + public CompletableFuture send(MessageOptions options) { + ensureNotTerminated(); + var request = new SendMessageRequest(); + request.setSessionId(sessionId); + request.setPrompt(options.getPrompt()); + request.setAttachments(options.getAttachments()); + request.setMode(options.getMode()); + request.setRequestHeaders(options.getRequestHeaders()); + + return rpc.invoke("session.send", request, SendMessageResponse.class).thenApply(SendMessageResponse::messageId); + } + + /** + * Sends a message and waits until the session becomes idle. + *

+ * This method blocks until the assistant finishes processing the message or + * until the timeout expires. It's suitable for simple request/response + * interactions where you don't need to process streaming events. + *

+ * The returned future can be cancelled via + * {@link java.util.concurrent.Future#cancel(boolean)}. If cancelled externally, + * the future completes with {@link java.util.concurrent.CancellationException}. + * If the timeout expires first, the future completes exceptionally with a + * {@link TimeoutException}. + * + * @param options + * the message options containing the prompt and attachments + * @param timeoutMs + * timeout in milliseconds (0 or negative for no timeout) + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received. The future + * completes exceptionally with a TimeoutException if the timeout + * expires, or with CancellationException if cancelled externally. + * @throws IllegalStateException + * if this session has been terminated + * @see #sendAndWait(MessageOptions) + * @see #send(MessageOptions) + */ + public CompletableFuture sendAndWait(MessageOptions options, long timeoutMs) { + ensureNotTerminated(); + long totalNanos = System.nanoTime(); + var future = new CompletableFuture(); + var lastAssistantMessage = new AtomicReference(); + var firstAssistantMessageLogged = new java.util.concurrent.atomic.AtomicBoolean(false); + + Consumer handler = evt -> { + if (evt instanceof AssistantMessageEvent msg) { + lastAssistantMessage.set(msg); + if (firstAssistantMessageLogged.compareAndSet(false, true)) { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotSession.sendAndWait first assistant message. Elapsed={Elapsed}, SessionId=" + + sessionId, + totalNanos); + } + } else if (evt instanceof SessionIdleEvent) { + LoggingHelpers.logTiming(LOG, Level.FINE, + "CopilotSession.sendAndWait idle received. Elapsed={Elapsed}, SessionId=" + sessionId, + totalNanos); + future.complete(lastAssistantMessage.get()); + } else if (evt instanceof SessionErrorEvent errorEvent) { + String message = errorEvent.getData() != null ? errorEvent.getData().message() : "session error"; + future.completeExceptionally(new RuntimeException("Session error: " + message)); + } + }; + + Closeable subscription = on(handler); + + send(options).exceptionally(ex -> { + try { + subscription.close(); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error closing subscription", e); + } + future.completeExceptionally(ex); + return null; + }); + + var result = new CompletableFuture(); + + // Schedule timeout on the shared session-level scheduler. + // Per Javadoc, timeoutMs <= 0 means "no timeout". + ScheduledFuture timeoutTask = null; + if (timeoutMs > 0) { + try { + timeoutTask = timeoutScheduler.schedule(() -> { + if (!future.isDone()) { + future.completeExceptionally( + new TimeoutException("sendAndWait timed out after " + timeoutMs + "ms")); + } + }, timeoutMs, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException e) { + try { + subscription.close(); + } catch (IOException closeEx) { + e.addSuppressed(closeEx); + } + result.completeExceptionally(e); + return result; + } + } + + // When inner future completes, run cleanup and propagate to result. + // Use whenCompleteAsync so that result.complete(r) is not called + // synchronously on the event-dispatch thread while dispatchEvent() is + // still iterating over handlers. Without async dispatch, a caller that + // registered its own session.on() listener before calling sendAndWait() + // could see its listener invoked *after* result.get() returned, because + // sendAndWait's internal handler would complete the future mid-loop. By + // submitting the completion to timeoutScheduler we allow the current + // dispatch loop to finish calling all other handlers first. + final ScheduledFuture taskToCancel = timeoutTask; + future.whenCompleteAsync((r, ex) -> { + try { + subscription.close(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error closing subscription", e); + } + if (taskToCancel != null) { + taskToCancel.cancel(false); + } + if (!result.isDone()) { + if (ex != null) { + if (ex instanceof TimeoutException) { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotSession.sendAndWait failed. Elapsed={Elapsed}, SessionId=" + sessionId + + ", CompletedBy=timeout", + totalNanos); + } else if (!(ex instanceof java.util.concurrent.CancellationException)) { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "CopilotSession.sendAndWait failed. Elapsed={Elapsed}, SessionId=" + sessionId + + ", CompletedBy=error", + totalNanos); + } + result.completeExceptionally(ex); + } else { + LoggingHelpers.logTiming( + LOG, Level.FINE, "CopilotSession.sendAndWait complete. Elapsed={Elapsed}, SessionId=" + + sessionId + ", CompletedBy=idle, AssistantMessageReceived=" + (r != null), + totalNanos); + result.complete(r); + } + } + }, timeoutScheduler); + + // When result is cancelled externally, cancel inner future to trigger cleanup + result.whenComplete((v, ex) -> { + if (result.isCancelled() && !future.isDone()) { + future.cancel(true); + } + }); + + return result; + } + + /** + * Sends a message and waits until the session becomes idle with default 60 + * second timeout. + * + * @param options + * the message options containing the prompt and attachments + * @return a future that resolves with the final assistant message event, or + * {@code null} if no assistant message was received + * @throws IllegalStateException + * if this session has been terminated + * @see #sendAndWait(MessageOptions, long) + */ + public CompletableFuture sendAndWait(MessageOptions options) { + ensureNotTerminated(); + return sendAndWait(options, 60000); + } + + /** + * Registers a callback for all session events. + *

+ * The handler will be invoked for every event in this session, including + * assistant messages, tool calls, and session state changes. For type-safe + * handling of specific event types, prefer {@link #on(Class, Consumer)} + * instead. + * + *

+ * Exception handling: If a handler throws an exception, the error is + * routed to the configured {@link EventErrorHandler} (if set). Whether + * remaining handlers execute depends on the configured + * {@link EventErrorPolicy}. + * + *

+ * Example: + * + *

{@code
+     * // Collect all events
+     * var events = new ArrayList();
+     * session.on(events::add);
+     * }
+ * + * @param handler + * a callback to be invoked when a session event occurs + * @return a Closeable that, when closed, unsubscribes the handler + * @throws IllegalStateException + * if this session has been terminated + * @see #on(Class, Consumer) + * @see SessionEvent + * @see #setEventErrorPolicy(EventErrorPolicy) + */ + public Closeable on(Consumer handler) { + ensureNotTerminated(); + eventHandlers.add(handler); + return () -> eventHandlers.remove(handler); + } + + /** + * Registers an event handler for a specific event type. + *

+ * This provides a type-safe way to handle specific events without needing + * {@code instanceof} checks. The handler will only be called for events + * matching the specified type. + * + *

+ * Exception handling: If a handler throws an exception, the error is + * routed to the configured {@link EventErrorHandler} (if set). Whether + * remaining handlers execute depends on the configured + * {@link EventErrorPolicy}. + * + *

+ * Example Usage + *

+ * + *
{@code
+     * // Handle assistant messages
+     * session.on(AssistantMessageEvent.class, msg -> {
+     * 	System.out.println(msg.getData().content());
+     * });
+     *
+     * // Handle session idle
+     * session.on(SessionIdleEvent.class, idle -> {
+     * 	done.complete(null);
+     * });
+     *
+     * // Handle streaming deltas
+     * session.on(AssistantMessageDeltaEvent.class, delta -> {
+     * 	System.out.print(delta.getData().deltaContent());
+     * });
+     * }
+ * + * @param + * the event type + * @param eventType + * the class of the event to listen for + * @param handler + * a callback invoked when events of this type occur + * @return a Closeable that unsubscribes the handler when closed + * @throws IllegalStateException + * if this session has been terminated + * @see #on(Consumer) + * @see SessionEvent + */ + public Closeable on(Class eventType, Consumer handler) { + ensureNotTerminated(); + Consumer wrapper = event -> { + if (eventType.isInstance(event)) { + handler.accept(eventType.cast(event)); + } + }; + eventHandlers.add(wrapper); + return () -> eventHandlers.remove(wrapper); + } + + /** + * Dispatches an event to all registered handlers. + *

+ * This is called internally when events are received from the server. Each + * handler is invoked in its own try/catch block. Errors are always logged at + * {@link Level#WARNING}. Whether dispatch continues after a handler error + * depends on the configured {@link EventErrorPolicy}: + *

    + *
  • {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS} (default) β€” dispatch + * stops after the first error
  • + *
  • {@link EventErrorPolicy#SUPPRESS_AND_LOG_ERRORS} β€” remaining handlers + * still execute
  • + *
+ *

+ * The configured {@link EventErrorHandler} is always invoked (if set), + * regardless of the policy. If the error handler itself throws, dispatch stops + * regardless of policy and the error is logged at {@link Level#SEVERE}. + * + * @param event + * the event to dispatch + * @see #setEventErrorHandler(EventErrorHandler) + * @see #setEventErrorPolicy(EventErrorPolicy) + */ + void dispatchEvent(SessionEvent event) { + // Handle broadcast request events (protocol v3) before dispatching to user + // handlers. These are fire-and-forget: the response is sent asynchronously. + handleBroadcastEventAsync(event); + + for (Consumer handler : eventHandlers) { + try { + handler.accept(event); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error in event handler", e); + EventErrorHandler errorHandler = this.eventErrorHandler; + if (errorHandler != null) { + try { + errorHandler.handleError(event, e); + } catch (Exception errorHandlerException) { + LOG.log(Level.SEVERE, "Error in event error handler", errorHandlerException); + break; // error handler itself failed β€” stop regardless of policy + } + } + if (eventErrorPolicy == EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS) { + break; + } + } + } + } + + /** + * Handles broadcast request events by executing local handlers and responding + * via RPC (protocol v3). + *

+ * Fire-and-forget: the response is sent asynchronously. + * + * @param event + * the event to handle + */ + private void handleBroadcastEventAsync(SessionEvent event) { + if (event instanceof ExternalToolRequestedEvent toolEvent) { + var data = toolEvent.getData(); + if (data == null || data.requestId() == null || data.toolName() == null) { + return; + } + ToolDefinition tool = getTool(data.toolName()); + if (tool == null) { + return; // This client doesn't handle this tool; another client will + } + executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), tool); + + } else if (event instanceof PermissionRequestedEvent permEvent) { + var data = permEvent.getData(); + if (data == null || data.requestId() == null || data.permissionRequest() == null) { + return; + } + if (Boolean.TRUE.equals(data.resolvedByHook())) { + return; // Already resolved by a permissionRequest hook; no client action needed. + } + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + return; // This client doesn't handle permissions; another client will + } + executePermissionAndRespondAsync(data.requestId(), + MAPPER.convertValue(data.permissionRequest(), PermissionRequest.class), handler); + } else if (event instanceof CommandExecuteEvent cmdEvent) { + var data = cmdEvent.getData(); + if (data == null || data.requestId() == null || data.commandName() == null) { + return; + } + executeCommandAndRespondAsync(data.requestId(), data.commandName(), data.command(), data.args()); + } else if (event instanceof ElicitationRequestedEvent elicitEvent) { + var data = elicitEvent.getData(); + if (data == null || data.requestId() == null) { + return; + } + ElicitationHandler handler = elicitationHandler.get(); + if (handler != null) { + ElicitationSchema schema = null; + if (data.requestedSchema() != null) { + schema = new ElicitationSchema().setType(data.requestedSchema().type()) + .setProperties(data.requestedSchema().properties()) + .setRequired(data.requestedSchema().required()); + } + var context = new ElicitationContext().setSessionId(sessionId).setMessage(data.message()) + .setRequestedSchema(schema).setMode(data.mode() != null ? data.mode().getValue() : null) + .setElicitationSource(data.elicitationSource()).setUrl(data.url()); + handleElicitationRequestAsync(context, data.requestId()); + } + } else if (event instanceof CapabilitiesChangedEvent capEvent) { + var data = capEvent.getData(); + if (data != null) { + var newCapabilities = new SessionCapabilities(); + if (data.ui() != null) { + newCapabilities.setUi(new SessionUiCapabilities().setElicitation(data.ui().elicitation())); + } else { + newCapabilities.setUi(capabilities.getUi()); + } + capabilities = newCapabilities; + } + } + } + + /** + * Executes a tool handler and sends the result back via + * {@code session.tools.handlePendingToolCall}. + */ + private void executeToolAndRespondAsync(String requestId, String toolName, String toolCallId, Object arguments, + ToolDefinition tool) { + Runnable task = () -> { + try { + JsonNode argumentsNode = arguments instanceof JsonNode jn + ? jn + : (arguments != null ? MAPPER.valueToTree(arguments) : null); + var invocation = new com.github.copilot.sdk.json.ToolInvocation().setSessionId(sessionId) + .setToolCallId(toolCallId).setToolName(toolName).setArguments(argumentsNode); + + tool.handler().invoke(invocation).thenAccept(result -> { + try { + ToolResultObject toolResult; + if (result instanceof ToolResultObject tr) { + toolResult = tr; + } else { + toolResult = ToolResultObject + .success(result instanceof String s ? s : MAPPER.writeValueAsString(result)); + } + getRpc().tools.handlePendingToolCall( + new SessionToolsHandlePendingToolCallParams(sessionId, requestId, toolResult, null)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending tool result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + getRpc().tools.handlePendingToolCall(new SessionToolsHandlePendingToolCallParams(sessionId, + requestId, null, ex.getMessage() != null ? ex.getMessage() : ex.toString())); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending tool error for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing tool for requestId=" + requestId, e); + try { + getRpc().tools.handlePendingToolCall(new SessionToolsHandlePendingToolCallParams(sessionId, + requestId, null, e.getMessage() != null ? e.getMessage() : e.toString())); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending tool error for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected tool task for requestId=" + requestId + "; running inline", e); + task.run(); + } + } + + /** + * Builds a {@link SessionUiHandlePendingElicitationParams} carrying a + * {@code cancel} action, used when an elicitation handler throws or the handler + * future completes exceptionally. + */ + private SessionUiHandlePendingElicitationParams buildElicitationCancelParams(String requestId) { + var cancelResult = new UIElicitationResponse(UIElicitationResponseAction.CANCEL, null); + return new SessionUiHandlePendingElicitationParams(sessionId, requestId, cancelResult); + } + + /** + * Executes a permission handler and sends the result back via + * {@code session.permissions.handlePendingPermissionRequest}. + */ + private void executePermissionAndRespondAsync(String requestId, PermissionRequest permissionRequest, + PermissionHandler handler) { + Runnable task = () -> { + try { + var invocation = new PermissionInvocation(); + invocation.setSessionId(sessionId); + handler.handle(permissionRequest, invocation).thenAccept(result -> { + try { + PermissionRequestResultKind kind = new PermissionRequestResultKind(result.getKind()); + if (PermissionRequestResultKind.NO_RESULT.equals(kind)) { + // Handler explicitly abstains β€” leave the request unanswered + // so another client can handle it. + return; + } + getRpc().permissions.handlePendingPermissionRequest( + new SessionPermissionsHandlePendingPermissionRequestParams(sessionId, requestId, + result)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending permission result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + PermissionRequestResult denied = new PermissionRequestResult(); + denied.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + getRpc().permissions.handlePendingPermissionRequest( + new SessionPermissionsHandlePendingPermissionRequestParams(sessionId, requestId, + denied)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending permission denied for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing permission handler for requestId=" + requestId, e); + try { + PermissionRequestResult denied = new PermissionRequestResult(); + denied.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + getRpc().permissions.handlePendingPermissionRequest( + new SessionPermissionsHandlePendingPermissionRequestParams(sessionId, requestId, denied)); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending permission denied for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected perm task for requestId=" + requestId + "; running inline", e); + task.run(); + } + } + + /** + * Registers custom tool handlers for this session. + *

+ * Called internally when creating or resuming a session with tools. + * + * @param tools + * the list of tool definitions with handlers + */ + void registerTools(List tools) { + toolHandlers.clear(); + if (tools != null) { + for (ToolDefinition tool : tools) { + toolHandlers.put(tool.name(), tool); + } + } + } + + /** + * Executes a command handler and sends the result back via + * {@code session.commands.handlePendingCommand}. + */ + private void executeCommandAndRespondAsync(String requestId, String commandName, String command, String args) { + CommandHandler handler = commandHandlers.get(commandName); + Runnable task = () -> { + if (handler == null) { + try { + getRpc().commands.handlePendingCommand(new SessionCommandsHandlePendingCommandParams(sessionId, + requestId, "Unknown command: " + commandName)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, e); + } + return; + } + try { + var ctx = new CommandContext().setSessionId(sessionId).setCommand(command).setCommandName(commandName) + .setArgs(args); + handler.handle(ctx).thenRun(() -> { + try { + getRpc().commands.handlePendingCommand( + new SessionCommandsHandlePendingCommandParams(sessionId, requestId, null)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + String msg = ex.getMessage() != null ? ex.getMessage() : ex.toString(); + getRpc().commands.handlePendingCommand( + new SessionCommandsHandlePendingCommandParams(sessionId, requestId, msg)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing command for requestId=" + requestId, e); + try { + String msg = e.getMessage() != null ? e.getMessage() : e.toString(); + getRpc().commands.handlePendingCommand( + new SessionCommandsHandlePendingCommandParams(sessionId, requestId, msg)); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending command error for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected command task for requestId=" + requestId + "; running inline", e); + task.run(); + } + } + + /** + * Dispatches an elicitation request to the registered handler and responds via + * {@code session.ui.handlePendingElicitation}. Auto-cancels on handler errors. + */ + private void handleElicitationRequestAsync(ElicitationContext context, String requestId) { + ElicitationHandler handler = elicitationHandler.get(); + if (handler == null) { + return; + } + Runnable task = () -> { + try { + handler.handle(context).thenAccept(result -> { + try { + String actionStr = result.getAction() != null + ? result.getAction().getValue() + : ElicitationResultAction.CANCEL.getValue(); + var parsedAction = UIElicitationResponseAction.fromValue(actionStr); + var elicitationResult = new UIElicitationResponse(parsedAction, result.getContent()); + getRpc().ui.handlePendingElicitation( + new SessionUiHandlePendingElicitationParams(sessionId, requestId, elicitationResult)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending elicitation result for requestId=" + requestId, e); + } + }).exceptionally(ex -> { + try { + getRpc().ui.handlePendingElicitation(buildElicitationCancelParams(requestId)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending elicitation cancel for requestId=" + requestId, e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing elicitation handler for requestId=" + requestId, e); + try { + getRpc().ui.handlePendingElicitation(buildElicitationCancelParams(requestId)); + } catch (Exception sendEx) { + LOG.log(Level.WARNING, "Error sending elicitation cancel for requestId=" + requestId, sendEx); + } + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected elicitation task for requestId=" + requestId + "; running inline", + e); + task.run(); + } + } + + /** + * Throws if the host does not support elicitation. + */ + private void assertElicitation() { + SessionCapabilities caps = capabilities; + if (caps == null || caps.getUi() == null || !caps.getUi().getElicitation().orElse(false)) { + throw new IllegalStateException("Elicitation is not supported by the host. " + + "Check session.getCapabilities().getUi().getElicitation().orElse(false) before calling UI methods."); + } + } + + /** + * Implements {@link SessionUiApi} backed by the session's RPC connection. + */ + private final class SessionUiApiImpl implements SessionUiApi { + + @Override + public CompletableFuture elicitation(ElicitationParams params) { + assertElicitation(); + return getRpc().ui.elicitation(new SessionUiElicitationParams(sessionId, params.getMessage(), + new UIElicitationSchema(params.getRequestedSchema().getType(), + params.getRequestedSchema().getProperties(), params.getRequestedSchema().getRequired()))) + .thenApply(resp -> { + var result = new ElicitationResult(); + if (resp.action() != null) { + for (ElicitationResultAction a : ElicitationResultAction.values()) { + if (a.getValue().equalsIgnoreCase(resp.action().getValue())) { + result.setAction(a); + break; + } + } + } + if (result.getAction() == null) { + result.setAction(ElicitationResultAction.CANCEL); + } + result.setContent(resp.content()); + return result; + }); + } + + @Override + public CompletableFuture confirm(String message) { + assertElicitation(); + var field = Map.of("type", "boolean", "default", (Object) true); + return getRpc().ui.elicitation(new SessionUiElicitationParams(sessionId, message, + new UIElicitationSchema("object", Map.of("confirmed", (Object) field), List.of("confirmed")))) + .thenApply(resp -> { + if (resp.action() == UIElicitationResponseAction.ACCEPT && resp.content() != null) { + Object val = resp.content().get("confirmed"); + if (val instanceof Boolean b) { + return b; + } + if (val instanceof com.fasterxml.jackson.databind.node.BooleanNode bn) { + return bn.booleanValue(); + } + if (val instanceof String s) { + return Boolean.parseBoolean(s); + } + } + return false; + }); + } + + @Override + public CompletableFuture select(String message, String[] options) { + assertElicitation(); + var field = Map.of("type", (Object) "string", "enum", (Object) options); + return getRpc().ui.elicitation(new SessionUiElicitationParams(sessionId, message, + new UIElicitationSchema("object", Map.of("selection", (Object) field), List.of("selection")))) + .thenApply(resp -> { + if (resp.action() == UIElicitationResponseAction.ACCEPT && resp.content() != null) { + Object val = resp.content().get("selection"); + return val != null ? val.toString() : null; + } + return null; + }); + } + + @Override + public CompletableFuture input(String message, InputOptions options) { + assertElicitation(); + var field = new java.util.LinkedHashMap(); + field.put("type", "string"); + if (options != null) { + if (options.getTitle() != null) + field.put("title", options.getTitle()); + if (options.getDescription() != null) + field.put("description", options.getDescription()); + if (options.getMinLength().isPresent()) + field.put("minLength", options.getMinLength().getAsInt()); + if (options.getMaxLength().isPresent()) + field.put("maxLength", options.getMaxLength().getAsInt()); + if (options.getFormat() != null) + field.put("format", options.getFormat()); + if (options.getDefaultValue() != null) + field.put("default", options.getDefaultValue()); + } + return getRpc().ui + .elicitation(new SessionUiElicitationParams(sessionId, message, + new UIElicitationSchema("object", Map.of("value", (Object) field), List.of("value")))) + .thenApply(resp -> { + if (resp.action() == UIElicitationResponseAction.ACCEPT && resp.content() != null) { + Object val = resp.content().get("value"); + return val != null ? val.toString() : null; + } + return null; + }); + } + } + + /** + * Retrieves a registered tool by name. + * + * @param name + * the tool name + * @return the tool definition, or {@code null} if not found + */ + ToolDefinition getTool(String name) { + return toolHandlers.get(name); + } + + /** + * Registers a handler for permission requests. + *

+ * Called internally when creating or resuming a session with permission + * handling. + * + * @param handler + * the permission handler + */ + void registerPermissionHandler(PermissionHandler handler) { + permissionHandler.set(handler); + } + + /** + * Handles a permission request from the Copilot CLI. + *

+ * Called internally when the server requests permission for an operation. + * + * @param permissionRequestData + * the JSON data for the permission request + * @return a future that resolves with the permission result + */ + CompletableFuture handlePermissionRequest(JsonNode permissionRequestData) { + PermissionHandler handler = permissionHandler.get(); + if (handler == null) { + PermissionRequestResult result = new PermissionRequestResult(); + result.setKind(PermissionRequestResultKind.USER_NOT_AVAILABLE); + return CompletableFuture.completedFuture(result); + } + + try { + PermissionRequest request = MAPPER.treeToValue(permissionRequestData, PermissionRequest.class); + var invocation = new PermissionInvocation(); + invocation.setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "Permission handler threw an exception", ex); + PermissionRequestResult result = new PermissionRequestResult(); + result.setKind(PermissionRequestResultKind.USER_NOT_AVAILABLE); + return result; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process permission request", e); + PermissionRequestResult result = new PermissionRequestResult(); + result.setKind(PermissionRequestResultKind.USER_NOT_AVAILABLE); + return CompletableFuture.completedFuture(result); + } + } + + /** + * Registers a handler for user input requests. + *

+ * Called internally when creating or resuming a session with user input + * handling. + * + * @param handler + * the user input handler + */ + void registerUserInputHandler(UserInputHandler handler) { + userInputHandler.set(handler); + } + + /** + * Registers command handlers for this session. + *

+ * Called internally when creating or resuming a session with commands. + * + * @param commands + * the command definitions to register + */ + void registerCommands(java.util.List commands) { + commandHandlers.clear(); + if (commands != null) { + for (CommandDefinition cmd : commands) { + if (cmd.getName() != null && cmd.getHandler() != null) { + commandHandlers.put(cmd.getName(), cmd.getHandler()); + } + } + } + } + + /** + * Registers an elicitation handler for this session. + *

+ * Called internally when creating or resuming a session with an elicitation + * handler. + * + * @param handler + * the handler to invoke when an elicitation request is received + */ + void registerElicitationHandler(ElicitationHandler handler) { + elicitationHandler.set(handler); + } + + /** + * Registers an exit-plan-mode handler for this session. + *

+ * Called internally when creating or resuming a session with an exit-plan-mode + * handler. + * + * @param handler + * the handler to invoke when an exit-plan-mode request is received + */ + void registerExitPlanModeHandler(ExitPlanModeHandler handler) { + exitPlanModeHandler.set(handler); + } + + /** + * Registers an auto-mode-switch handler for this session. + *

+ * Called internally when creating or resuming a session with an + * auto-mode-switch handler. + * + * @param handler + * the handler to invoke when an auto-mode-switch request is received + */ + void registerAutoModeSwitchHandler(AutoModeSwitchHandler handler) { + autoModeSwitchHandler.set(handler); + } + + /** + * Sets the capabilities reported by the host for this session. + *

+ * Called internally after session create/resume response. + * + * @param sessionCapabilities + * the capabilities to set, or {@code null} for empty capabilities + */ + void setCapabilities(SessionCapabilities sessionCapabilities) { + this.capabilities = sessionCapabilities != null ? sessionCapabilities : new SessionCapabilities(); + } + + /** + * Handles a user input request from the Copilot CLI. + *

+ * Called internally when the server requests user input. + * + * @param request + * the user input request + * @return a future that resolves with the user input response + */ + CompletableFuture handleUserInputRequest(UserInputRequest request) { + UserInputHandler handler = userInputHandler.get(); + if (handler == null) { + return CompletableFuture.failedFuture(new IllegalStateException("No user input handler registered")); + } + + try { + var invocation = new UserInputInvocation().setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "User input handler threw an exception", ex); + throw new RuntimeException("User input handler error", ex); + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process user input request", e); + return CompletableFuture.failedFuture(e); + } + } + + /** + * Handles an exit-plan-mode request from the Copilot CLI. + *

+ * Called internally when the server sends an {@code exitPlanMode.request}. + * + * @param request + * the exit-plan-mode request + * @return a future that resolves with the user's decision + */ + CompletableFuture handleExitPlanModeRequest(ExitPlanModeRequest request) { + ExitPlanModeHandler handler = exitPlanModeHandler.get(); + if (handler == null) { + return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true)); + } + + try { + var invocation = new ExitPlanModeInvocation().setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "Exit plan mode handler threw an exception", ex); + throw new RuntimeException("Exit plan mode handler error", ex); + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process exit plan mode request", e); + return CompletableFuture.failedFuture(e); + } + } + + /** + * Handles an auto-mode-switch request from the Copilot CLI. + *

+ * Called internally when the server sends an {@code autoModeSwitch.request}. + * + * @param request + * the auto-mode-switch request + * @return a future that resolves with the user's decision + */ + CompletableFuture handleAutoModeSwitchRequest(AutoModeSwitchRequest request) { + AutoModeSwitchHandler handler = autoModeSwitchHandler.get(); + if (handler == null) { + return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO); + } + + try { + var invocation = new AutoModeSwitchInvocation().setSessionId(sessionId); + return handler.handle(request, invocation).exceptionally(ex -> { + LOG.log(Level.SEVERE, "Auto mode switch handler threw an exception", ex); + throw new RuntimeException("Auto mode switch handler error", ex); + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process auto mode switch request", e); + return CompletableFuture.failedFuture(e); + } + } + + /** + * Registers hook handlers for this session. + *

+ * Called internally when creating or resuming a session with hooks. + * + * @param hooks + * the hooks configuration + */ + void registerHooks(SessionHooks hooks) { + hooksHandler.set(hooks); + } + + /** + * Registers transform callbacks for system message sections. + *

+ * Called internally when creating or resuming a session with + * {@link com.github.copilot.sdk.SystemMessageMode#CUSTOMIZE} and transform + * callbacks. + * + * @param callbacks + * the transform callbacks keyed by section identifier; {@code null} + * clears any previously registered callbacks + */ + void registerTransformCallbacks( + Map>> callbacks) { + this.transformCallbacks = callbacks; + } + + /** + * Handles a {@code systemMessage.transform} RPC call from the Copilot CLI. + *

+ * The CLI sends section content; the SDK invokes the registered transform + * callbacks and returns the transformed sections. + * + * @param sections + * JSON node containing sections keyed by section identifier + * @return a future resolving with a map of transformed sections + */ + CompletableFuture> handleSystemMessageTransform(JsonNode sections) { + var callbacks = this.transformCallbacks; + var result = new java.util.LinkedHashMap(); + var futures = new ArrayList>(); + + if (sections != null && sections.isObject()) { + sections.fields().forEachRemaining(entry -> { + String sectionId = entry.getKey(); + String content = entry.getValue().has("content") ? entry.getValue().get("content").asText("") : ""; + + java.util.function.Function> cb = callbacks != null + ? callbacks.get(sectionId) + : null; + + if (cb != null) { + CompletableFuture f = cb.apply(content).exceptionally(ex -> content) + .thenAccept(transformed -> { + synchronized (result) { + result.put(sectionId, Map.of("content", transformed != null ? transformed : "")); + } + }); + futures.add(f); + } else { + result.put(sectionId, Map.of("content", content)); + } + }); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> { + Map response = new java.util.LinkedHashMap<>(); + response.put("sections", result); + return response; + }); + } + + /** + * Handles a hook invocation from the Copilot CLI. + *

+ * Called internally when the server invokes a hook. + * + * @param hookType + * the type of hook to invoke + * @param input + * the hook input data + * @return a future that resolves with the hook output + */ + CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { + SessionHooks hooks = hooksHandler.get(); + if (hooks == null) { + return CompletableFuture.completedFuture(null); + } + + var invocation = new HookInvocation().setSessionId(sessionId); + + try { + switch (hookType) { + case "preToolUse" : + if (hooks.getOnPreToolUse() != null) { + PreToolUseHookInput preInput = MAPPER.treeToValue(input, PreToolUseHookInput.class); + return hooks.getOnPreToolUse().handle(preInput, invocation) + .thenApply(output -> (Object) output); + } + break; + case "postToolUse" : + if (hooks.getOnPostToolUse() != null) { + PostToolUseHookInput postInput = MAPPER.treeToValue(input, PostToolUseHookInput.class); + return hooks.getOnPostToolUse().handle(postInput, invocation) + .thenApply(output -> (Object) output); + } + break; + case "userPromptSubmitted" : + if (hooks.getOnUserPromptSubmitted() != null) { + UserPromptSubmittedHookInput promptInput = MAPPER.treeToValue(input, + UserPromptSubmittedHookInput.class); + return hooks.getOnUserPromptSubmitted().handle(promptInput, invocation) + .thenApply(output -> (Object) output); + } + break; + case "sessionStart" : + if (hooks.getOnSessionStart() != null) { + SessionStartHookInput startInput = MAPPER.treeToValue(input, SessionStartHookInput.class); + return hooks.getOnSessionStart().handle(startInput, invocation) + .thenApply(output -> (Object) output); + } + break; + case "sessionEnd" : + if (hooks.getOnSessionEnd() != null) { + SessionEndHookInput endInput = MAPPER.treeToValue(input, SessionEndHookInput.class); + return hooks.getOnSessionEnd().handle(endInput, invocation) + .thenApply(output -> (Object) output); + } + break; + default : + LOG.fine("Unhandled hook type: " + hookType); + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to process hook invocation", e); + return CompletableFuture.failedFuture(e); + } + + return CompletableFuture.completedFuture(null); + } + + /** + * Gets the complete list of messages and events in the session. + *

+ * This retrieves the full conversation history, including all user messages, + * assistant responses, tool invocations, and other session events. + * + * @return a future that resolves with a list of all session events + * @throws IllegalStateException + * if this session has been terminated + * @see SessionEvent + */ + public CompletableFuture> getMessages() { + ensureNotTerminated(); + return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class) + .thenApply(response -> { + var events = new ArrayList(); + if (response.events() != null) { + for (JsonNode eventNode : response.events()) { + try { + SessionEvent event = MAPPER.treeToValue(eventNode, SessionEvent.class); + if (event != null) { + events.add(event); + } + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to parse event", e); + } + } + } + return events; + }); + } + + /** + * Aborts the currently processing message in this session. + *

+ * Use this to cancel a long-running operation or stop the assistant from + * continuing to generate a response. + * + * @return a future that completes when the abort is acknowledged + * @throws IllegalStateException + * if this session has been terminated + */ + public CompletableFuture abort() { + ensureNotTerminated(); + return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class); + } + + /** + * Changes the model for this session with an optional reasoning effort level. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + *

{@code
+     * session.setModel("gpt-4.1").get();
+     * session.setModel("claude-sonnet-4.6", "high").get();
+     * }
+ * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @param reasoningEffort + * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, + * {@code "high"}, {@code "xhigh"}); {@code null} to use default + * @return a future that completes when the model switch is acknowledged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.2.0 + */ + public CompletableFuture setModel(String model, String reasoningEffort) { + ensureNotTerminated(); + return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, null)) + .thenApply(r -> null); + } + + /** + * Changes the model for this session with optional reasoning effort and + * capability overrides. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + *

{@code
+     * session.setModel("claude-sonnet-4.5", null,
+     * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(false)))
+     * 		.get();
+     * }
+ * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @param reasoningEffort + * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, + * {@code "high"}, {@code "xhigh"}); {@code null} to use default + * @param modelCapabilities + * per-property overrides for model capabilities; {@code null} to use + * runtime defaults + * @return a future that completes when the model switch is acknowledged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.3.0 + */ + public CompletableFuture setModel(String model, String reasoningEffort, + com.github.copilot.sdk.json.ModelCapabilitiesOverride modelCapabilities) { + ensureNotTerminated(); + ModelCapabilitiesOverride generatedCapabilities = null; + if (modelCapabilities != null) { + ModelCapabilitiesOverrideSupports supports = null; + if (modelCapabilities.getSupports() != null) { + var s = modelCapabilities.getSupports(); + supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), + s.getReasoningEffort().orElse(null)); + } + ModelCapabilitiesOverrideLimits limits = null; + if (modelCapabilities.getLimits() != null) { + limits = new ObjectMapper().convertValue(modelCapabilities.getLimits(), + ModelCapabilitiesOverrideLimits.class); + } + generatedCapabilities = new ModelCapabilitiesOverride(supports, limits); + } + return getRpc().model + .switchTo( + new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, generatedCapabilities)) + .thenApply(r -> null); + } + + /** + * Changes the model for this session. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. + * + *

{@code
+     * session.setModel("gpt-4.1").get();
+     * }
+ * + * @param model + * the model ID to switch to (e.g., {@code "gpt-4.1"}) + * @return a future that completes when the model switch is acknowledged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture setModel(String model) { + return setModel(model, null); + } + + /** + * Logs a message to the session timeline. + *

+ * The message appears in the session event stream and is visible to SDK + * consumers. Non-ephemeral messages are also persisted to the session event log + * on disk. + * + *

Example Usage

+ * + *
{@code
+     * session.log("Build completed successfully").get();
+     * session.log("Disk space low", "warning", null).get();
+     * session.log("Temporary status", null, true).get();
+     * session.log("Details at link", "info", null, "https://example.com").get();
+     * }
+ * + * @param message + * the message to log + * @param level + * the log severity level ({@code "info"}, {@code "warning"}, + * {@code "error"}), or {@code null} to use the default + * ({@code "info"}) + * @param ephemeral + * when {@code true}, the message is transient and not persisted to + * disk; {@code null} uses default behavior + * @param url + * optional URL to associate with the log entry; {@code null} to omit + * @return a future that completes when the message is logged + * @throws IllegalStateException + * if this session has been terminated + * @since 1.2.0 + */ + public CompletableFuture log(String message, String level, Boolean ephemeral, String url) { + ensureNotTerminated(); + SessionLogLevel rpcLevel = null; + if (level != null) { + try { + rpcLevel = SessionLogLevel.fromValue(level); + } catch (IllegalArgumentException e) { + rpcLevel = SessionLogLevel.INFO; + } + } + return getRpc().log(new SessionLogParams(sessionId, message, rpcLevel, ephemeral, url)).thenApply(r -> null); + } + + /** + * Logs a message to the session timeline. + *

+ * The message appears in the session event stream and is visible to SDK + * consumers. Non-ephemeral messages are also persisted to the session event log + * on disk. + * + *

Example Usage

+ * + *
{@code
+     * session.log("Build completed successfully").get();
+     * session.log("Disk space low", "warning", null).get();
+     * session.log("Temporary status", null, true).get();
+     * }
+ * + * @param message + * the message to log + * @param level + * the log severity level ({@code "info"}, {@code "warning"}, + * {@code "error"}), or {@code null} to use the default + * ({@code "info"}) + * @param ephemeral + * when {@code true}, the message is transient and not persisted to + * disk; {@code null} uses default behavior + * @return a future that completes when the message is logged + * @throws IllegalStateException + * if this session has been terminated + */ + public CompletableFuture log(String message, String level, Boolean ephemeral) { + return log(message, level, ephemeral, null); + } + + /** + * Logs an informational message to the session timeline. + * + * @param message + * the message to log + * @return a future that completes when the message is logged + * @throws IllegalStateException + * if this session has been terminated + */ + public CompletableFuture log(String message) { + return log(message, null, null); + } + + /** + * Lists the custom agents available for selection in this session. + * + * @return a future that resolves with the list of available agents + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture> listAgents() { + ensureNotTerminated(); + return rpc.invoke("session.agent.list", Map.of("sessionId", sessionId), AgentListResponse.class) + .thenApply(response -> response.agents() != null + ? Collections.unmodifiableList(response.agents()) + : Collections.emptyList()); + } + + /** + * Gets the currently selected custom agent for this session, or {@code null} if + * no custom agent is selected. + * + * @return a future that resolves with the current agent, or {@code null} if + * using the default agent + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture getCurrentAgent() { + ensureNotTerminated(); + return rpc.invoke("session.agent.getCurrent", Map.of("sessionId", sessionId), AgentGetCurrentResponse.class) + .thenApply(AgentGetCurrentResponse::agent); + } + + /** + * Selects a custom agent for this session. + * + * @param agentName + * the name/identifier of the agent to select + * @return a future that resolves with the selected agent information + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture selectAgent(String agentName) { + ensureNotTerminated(); + return rpc.invoke("session.agent.select", Map.of("sessionId", sessionId, "name", agentName), + AgentSelectResponse.class).thenApply(AgentSelectResponse::agent); + } + + /** + * Deselects the currently selected custom agent, returning to the default + * agent. + * + * @return a future that completes when the agent is deselected + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture deselectAgent() { + ensureNotTerminated(); + return rpc.invoke("session.agent.deselect", Map.of("sessionId", sessionId), Void.class); + } + + /** + * Compacts the session context to reduce token usage. + *

+ * This triggers an immediate session compaction, summarizing the conversation + * history to free up context window space. + * + * @return a future that completes when compaction finishes + * @throws IllegalStateException + * if this session has been terminated + * @since 1.0.11 + */ + public CompletableFuture compact() { + ensureNotTerminated(); + return rpc.invoke("session.compaction.compact", Map.of("sessionId", sessionId), Void.class); + } + + /** + * Verifies that this session has not yet been terminated. + * + * @throws IllegalStateException + * if close() has already been invoked + */ + private void ensureNotTerminated() { + if (isTerminated) { + throw new IllegalStateException("Session is closed"); + } + } + + /** + * Disposes the session and releases all associated resources. + *

+ * This destroys the session on the server, clears all event handlers, and + * releases tool and permission handlers. After calling this method, the session + * cannot be used again. Subsequent calls to this method have no effect. + */ + @Override + public void close() { + synchronized (this) { + if (isTerminated) { + return; // Already terminated - no-op + } + isTerminated = true; + } + + timeoutScheduler.shutdownNow(); + + try { + rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS); + } catch (Exception e) { + LOG.log(Level.FINE, "Error destroying session", e); + } + + eventHandlers.clear(); + toolHandlers.clear(); + commandHandlers.clear(); + permissionHandler.set(null); + userInputHandler.set(null); + elicitationHandler.set(null); + exitPlanModeHandler.set(null); + autoModeSwitchHandler.set(null); + hooksHandler.set(null); + } + + // ===== Internal response types for agent API ===== + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentListResponse(@JsonProperty("agents") List agents) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentGetCurrentResponse(@JsonProperty("agent") AgentInfo agent) { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record AgentSelectResponse(@JsonProperty("agent") AgentInfo agent) { + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/EventErrorHandler.java b/java/src/main/java/com/github/copilot/sdk/EventErrorHandler.java new file mode 100644 index 000000000..2ff77d971 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/EventErrorHandler.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import com.github.copilot.sdk.generated.SessionEvent; + +/** + * A handler for errors thrown by event handlers during event dispatch. + *

+ * When an event handler registered via + * {@link CopilotSession#on(java.util.function.Consumer)} or + * {@link CopilotSession#on(Class, java.util.function.Consumer)} throws an + * exception, the {@code EventErrorHandler} is invoked with the event that was + * being dispatched and the exception that was thrown. + * + *

+ * Errors are always logged at {@link java.util.logging.Level#WARNING} + * regardless of whether an error handler is set. The error handler provides + * additional custom handling such as metrics, alerts, or integration with + * external error-reporting systems: + * + *

{@code
+ * session.setEventErrorHandler((event, exception) -> {
+ * 	metrics.increment("handler.errors");
+ * 	logger.error("Handler failed on {}: {}", event.getType(), exception.getMessage());
+ * });
+ * }
+ * + *

+ * Whether dispatch continues or stops after an error is controlled by the + * {@link EventErrorPolicy} set via + * {@link CopilotSession#setEventErrorPolicy(EventErrorPolicy)}. The error + * handler is always invoked regardless of the policy. + * + *

+ * If the error handler itself throws an exception, that exception is caught and + * logged at {@link java.util.logging.Level#SEVERE}, and dispatch is stopped + * regardless of the configured policy. + * + * @see CopilotSession#setEventErrorHandler(EventErrorHandler) + * @see EventErrorPolicy + * @since 1.0.8 + */ +@FunctionalInterface +public interface EventErrorHandler { + + /** + * Called when an event handler throws an exception during event dispatch. + * + * @param event + * the event that was being dispatched when the error occurred + * @param exception + * the exception thrown by the event handler + */ + void handleError(SessionEvent event, Exception exception); +} diff --git a/java/src/main/java/com/github/copilot/sdk/EventErrorPolicy.java b/java/src/main/java/com/github/copilot/sdk/EventErrorPolicy.java new file mode 100644 index 000000000..b7c3dca21 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/EventErrorPolicy.java @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +/** + * Controls how event dispatch behaves when an event handler throws an + * exception. + *

+ * This policy is set via + * {@link CopilotSession#setEventErrorPolicy(EventErrorPolicy)} and determines + * whether remaining event listeners continue to execute after a preceding + * listener throws an exception. Errors are always logged at + * {@link java.util.logging.Level#WARNING} regardless of the policy. + * + *

+ * The configured {@link EventErrorHandler} (if any) is always invoked + * regardless of the policy β€” the policy only controls whether dispatch + * continues after the error has been logged and the error handler has been + * called. + * + *

+ * The naming follows the convention used by Spring Framework's + * {@code TaskUtils.LOG_AND_SUPPRESS_ERROR_HANDLER} and + * {@code TaskUtils.LOG_AND_PROPAGATE_ERROR_HANDLER}. + * + *

+ * Example: + * + *

{@code
+ * // Default: propagate errors (stop dispatch on first error, log the error)
+ * session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS);
+ *
+ * // Opt-in to suppress errors (continue dispatching, log each error)
+ * session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
+ * }
+ * + * @see CopilotSession#setEventErrorPolicy(EventErrorPolicy) + * @see EventErrorHandler + * @since 1.0.8 + */ +public enum EventErrorPolicy { + + /** + * Suppress errors: log the error and continue dispatching to remaining + * listeners. + *

+ * When a handler throws an exception, the error is logged at + * {@link java.util.logging.Level#WARNING} and remaining handlers still execute. + * The configured {@link EventErrorHandler} is called for each error. This is + * analogous to Spring's {@code LOG_AND_SUPPRESS_ERROR_HANDLER} behavior. + */ + SUPPRESS_AND_LOG_ERRORS, + + /** + * Propagate errors: log the error and stop dispatch on first listener error + * (default). + *

+ * When a handler throws an exception, the error is logged at + * {@link java.util.logging.Level#WARNING} and no further handlers are invoked. + * The configured {@link EventErrorHandler} is still called before dispatch + * stops. This is analogous to Spring's {@code LOG_AND_PROPAGATE_ERROR_HANDLER} + * behavior. + */ + PROPAGATE_AND_LOG_ERRORS +} diff --git a/java/src/main/java/com/github/copilot/sdk/ExtractedTransforms.java b/java/src/main/java/com/github/copilot/sdk/ExtractedTransforms.java new file mode 100644 index 000000000..717d873d3 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/ExtractedTransforms.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import com.github.copilot.sdk.json.SystemMessageConfig; + +/** + * Result of extracting transform callbacks from a {@link SystemMessageConfig}. + *

+ * Holds a wire-safe copy of the system message config (with transform callbacks + * replaced by {@code action="transform"}) alongside the extracted callbacks + * that must be registered with the session. + * + * @param wireSystemMessage + * the system message config safe for JSON serialization; may be + * {@code null} when the input config was {@code null} + * @param transformCallbacks + * transform callbacks keyed by section identifier; {@code null} when + * no transforms were present + * @see SessionRequestBuilder#extractTransformCallbacks(SystemMessageConfig) + */ +record ExtractedTransforms(SystemMessageConfig wireSystemMessage, + Map>> transformCallbacks) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java new file mode 100644 index 000000000..73db478ca --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcClient.java @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.copilot.sdk.json.JsonRpcError; +import com.github.copilot.sdk.json.JsonRpcRequest; +import com.github.copilot.sdk.json.JsonRpcResponse; + +/** + * JSON-RPC 2.0 client implementation for communicating with the Copilot CLI. + * + * @since 1.0.0 + */ +class JsonRpcClient implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(JsonRpcClient.class.getName()); + private static final ObjectMapper MAPPER = createObjectMapper(); + + private final InputStream inputStream; + private final OutputStream outputStream; + private final Socket socket; + private final Process process; + private final AtomicLong requestIdCounter = new AtomicLong(0); + private final Map> pendingRequests = new ConcurrentHashMap<>(); + private final Map> notificationHandlers = new ConcurrentHashMap<>(); + private final ExecutorService readerExecutor; + private volatile boolean running = true; + + private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process) { + this.inputStream = inputStream; + this.outputStream = outputStream; + this.socket = socket; + this.process = process; + this.readerExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "jsonrpc-reader"); + t.setDaemon(true); + return t; + }); + startReader(); + } + + static ObjectMapper createObjectMapper() { + var mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); + return mapper; + } + + public static ObjectMapper getObjectMapper() { + return MAPPER; + } + + /** + * Creates a JSON-RPC client using stdio with a process. + */ + public static JsonRpcClient fromProcess(Process process) { + return new JsonRpcClient(process.getInputStream(), process.getOutputStream(), null, process); + } + + /** + * Creates a JSON-RPC client using TCP socket. + */ + public static JsonRpcClient fromSocket(Socket socket) throws IOException { + return new JsonRpcClient(socket.getInputStream(), socket.getOutputStream(), socket, null); + } + + /** + * Registers a handler for JSON-RPC method calls (requests/notifications from + * server). + */ + public void registerMethodHandler(String method, BiConsumer handler) { + notificationHandlers.put(method, handler); + } + + /** + * Sends a JSON-RPC request and waits for the response. + */ + public CompletableFuture invoke(String method, Object params, Class responseType) { + long timingNanos = System.nanoTime(); + long id = requestIdCounter.incrementAndGet(); + var future = new CompletableFuture(); + pendingRequests.put(id, future); + + var request = new JsonRpcRequest(); + request.setJsonrpc("2.0"); + request.setId(id); + request.setMethod(method); + request.setParams(params); + + try { + sendMessage(request); + } catch (IOException e) { + pendingRequests.remove(id); + future.completeExceptionally(e); + } + + return future.thenApply(result -> { + try { + T value = null; + if (responseType != Void.class && responseType != void.class) { + value = MAPPER.treeToValue(result, responseType); + } + LoggingHelpers.logTiming(LOG, Level.FINE, + "JsonRpc.invoke JSON-RPC request finished. Elapsed={Elapsed}, Method=" + method + ", RequestId=" + + id + ", Status=Succeeded", + timingNanos); + return value; + } catch (JsonProcessingException e) { + throw new CompletionException(e); + } + }).exceptionally(ex -> { + LoggingHelpers.logTiming(LOG, Level.WARNING, ex, + "JsonRpc.invoke JSON-RPC request finished. Elapsed={Elapsed}, Method=" + method + ", RequestId=" + + id + ", Status=Failed", + timingNanos); + throw ex instanceof RuntimeException re ? re : new RuntimeException(ex); + }); + } + + /** + * Sends a JSON-RPC notification (no response expected). + */ + public void notify(String method, Object params) throws IOException { + var notification = new JsonRpcRequest(); + notification.setJsonrpc("2.0"); + notification.setMethod(method); + notification.setParams(params); + sendMessage(notification); + } + + /** + * Sends a JSON-RPC response to a server request. + */ + public void sendResponse(Object id, Object result) throws IOException { + var response = new JsonRpcResponse(); + response.setJsonrpc("2.0"); + response.setId(id); + response.setResult(result); + sendMessage(response); + } + + /** + * Sends a JSON-RPC error response to a server request. + */ + public void sendErrorResponse(Object id, int code, String message) throws IOException { + var response = new JsonRpcResponse(); + response.setJsonrpc("2.0"); + response.setId(id); + var error = new JsonRpcError(); + error.setCode(code); + error.setMessage(message); + response.setError(error); + sendMessage(response); + } + + private synchronized void sendMessage(Object message) throws IOException { + String json = MAPPER.writeValueAsString(message); + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String header = "Content-Length: " + content.length + "\r\n\r\n"; + + outputStream.write(header.getBytes(StandardCharsets.UTF_8)); + outputStream.write(content); + outputStream.flush(); + + LOG.fine("Sent: " + json); + } + + private void startReader() { + readerExecutor.submit(() -> { + try { + // We need to read bytes because Content-Length specifies bytes, not characters. + // Using BufferedReader would cause issues with multi-byte UTF-8 characters. + var bis = new BufferedInputStream(inputStream); + + while (running) { + // Read headers line by line + int contentLength = -1; + var headerLine = new StringBuilder(); + boolean lastWasCR = false; + boolean inHeaders = true; + + while (inHeaders) { + int b = bis.read(); + if (b == -1) { + return; + } + + if (b == '\r') { + lastWasCR = true; + } else if (b == '\n') { + String line = headerLine.toString(); + headerLine.setLength(0); + lastWasCR = false; + + if (line.isEmpty()) { + // End of headers (blank line) + inHeaders = false; + } else if (line.toLowerCase().startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring(15).trim()); + } + } else { + if (lastWasCR) { + headerLine.append('\r'); + lastWasCR = false; + } + headerLine.append((char) b); + } + } + + if (contentLength <= 0) { + continue; + } + + // Read content as bytes (Content-Length specifies bytes, not characters) + byte[] buffer = new byte[contentLength]; + int read = 0; + while (read < contentLength) { + int result = bis.read(buffer, read, contentLength - read); + if (result == -1) { + return; + } + read += result; + } + + String content = new String(buffer, StandardCharsets.UTF_8); + LOG.fine("Received: " + content); + + handleMessage(content); + } + } catch (Exception e) { + if (running) { + LOG.log(Level.SEVERE, "Error in JSON-RPC reader", e); + } + } + }); + } + + private void handleMessage(String content) { + try { + JsonNode node = MAPPER.readTree(content); + + // Check if this is a response to our request + if (node.has("id") && !node.get("id").isNull() && (node.has("result") || node.has("error"))) { + long id = node.get("id").asLong(); + CompletableFuture future = pendingRequests.remove(id); + if (future != null) { + if (node.has("error")) { + JsonNode errorNode = node.get("error"); + String errorMessage = errorNode.has("message") + ? errorNode.get("message").asText() + : "Unknown error"; + int errorCode = errorNode.has("code") ? errorNode.get("code").asInt() : -1; + future.completeExceptionally(new JsonRpcException(errorCode, errorMessage)); + } else { + future.complete(node.get("result")); + } + } + } + // Check if this is a request from server (has method and id) + else if (node.has("method")) { + String method = node.get("method").asText(); + JsonNode params = node.get("params"); + Object id = node.has("id") && !node.get("id").isNull() ? node.get("id") : null; + + LOG.fine("Received method: " + method); + + BiConsumer handler = notificationHandlers.get(method); + if (handler != null) { + try { + // Create a context that includes the request ID for responses + handler.accept(id != null ? id.toString() : null, params); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling method " + method, e); + if (id != null) { + try { + sendErrorResponse(id, -32603, e.getMessage()); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + } + } else { + LOG.fine("No handler for method: " + method); + if (id != null) { + try { + sendErrorResponse(id, -32601, "Method not found: " + method); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + } + } + } catch (JsonProcessingException e) { + LOG.log(Level.SEVERE, "Error parsing JSON-RPC message", e); + } + } + + @Override + public void close() { + running = false; + readerExecutor.shutdownNow(); + + // Cancel all pending requests + pendingRequests.forEach((id, future) -> future.completeExceptionally(new IOException("Client closed"))); + pendingRequests.clear(); + + try { + if (socket != null) { + socket.close(); + } + } catch (IOException e) { + LOG.log(Level.FINE, "Error closing socket", e); + } + + if (process != null) { + process.destroy(); + } + } + + public boolean isConnected() { + if (socket != null) { + return socket.isConnected() && !socket.isClosed(); + } + if (process != null) { + return process.isAlive(); + } + return false; + } + + public Process getProcess() { + return process; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java new file mode 100644 index 000000000..dbc3ab77b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/JsonRpcException.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +/** + * Exception thrown when a JSON-RPC error occurs during communication with the + * Copilot CLI server. + *

+ * This exception wraps error responses from the JSON-RPC protocol, including + * the error code and message returned by the server. + * + * @since 1.0.0 + */ +final class JsonRpcException extends RuntimeException { + + private final int code; + + /** + * Creates a new JSON-RPC exception. + * + * @param code + * the JSON-RPC error code + * @param message + * the error message from the server + */ + public JsonRpcException(int code, String message) { + super(message); + this.code = code; + } + + /** + * Returns the JSON-RPC error code. + *

+ * Standard JSON-RPC error codes include: + *

    + *
  • -32700: Parse error
  • + *
  • -32600: Invalid request
  • + *
  • -32601: Method not found
  • + *
  • -32602: Invalid params
  • + *
  • -32603: Internal error
  • + *
+ * + * @return the error code + */ + public int getCode() { + return code; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/LifecycleEventManager.java b/java/src/main/java/com/github/copilot/sdk/LifecycleEventManager.java new file mode 100644 index 000000000..21c00e233 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/LifecycleEventManager.java @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.SessionLifecycleHandler; + +/** + * Manages lifecycle event subscriptions and dispatching. + *

+ * This class handles registration/unregistration of lifecycle event handlers + * and dispatches events to the appropriate handlers. + */ +final class LifecycleEventManager { + + private static final Logger LOG = Logger.getLogger(LifecycleEventManager.class.getName()); + + private final List wildcardHandlers = new ArrayList<>(); + private final Map> typedHandlers = new ConcurrentHashMap<>(); + private final Object handlersLock = new Object(); + + /** + * Subscribes to all session lifecycle events. + * + * @param handler + * a callback that receives lifecycle events + * @return an AutoCloseable that, when closed, unsubscribes the handler + */ + AutoCloseable subscribe(SessionLifecycleHandler handler) { + synchronized (handlersLock) { + wildcardHandlers.add(handler); + } + return () -> { + synchronized (handlersLock) { + wildcardHandlers.remove(handler); + } + }; + } + + /** + * Subscribes to a specific session lifecycle event type. + * + * @param eventType + * the event type to listen for + * @param handler + * a callback that receives events of the specified type + * @return an AutoCloseable that, when closed, unsubscribes the handler + */ + AutoCloseable subscribe(String eventType, SessionLifecycleHandler handler) { + synchronized (handlersLock) { + typedHandlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler); + } + return () -> { + synchronized (handlersLock) { + List handlers = typedHandlers.get(eventType); + if (handlers != null) { + handlers.remove(handler); + } + } + }; + } + + /** + * Dispatches a lifecycle event to all registered handlers. + * + * @param event + * the lifecycle event to dispatch + */ + void dispatch(SessionLifecycleEvent event) { + List typed; + List wildcard; + + synchronized (handlersLock) { + List handlers = typedHandlers.get(event.getType()); + typed = handlers != null ? new ArrayList<>(handlers) : new ArrayList<>(); + wildcard = new ArrayList<>(wildcardHandlers); + } + + for (SessionLifecycleHandler handler : typed) { + try { + handler.onLifecycleEvent(event); + } catch (Exception e) { + LOG.log(Level.WARNING, "Lifecycle handler error", e); + } + } + + for (SessionLifecycleHandler handler : wildcard) { + try { + handler.onLifecycleEvent(event); + } catch (Exception e) { + LOG.log(Level.WARNING, "Lifecycle handler error", e); + } + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/LoggingHelpers.java b/java/src/main/java/com/github/copilot/sdk/LoggingHelpers.java new file mode 100644 index 000000000..654494ef1 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/LoggingHelpers.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Internal helper for timing-based diagnostic logging. + */ +final class LoggingHelpers { + + private LoggingHelpers() { + // Utility class + } + + /** + * Formats elapsed time as a human-readable duration string. + * + * @param startNanos + * the start time from {@link System#nanoTime()} + * @return formatted duration (e.g. "PT0.123S") + */ + static String formatElapsed(long startNanos) { + long elapsedNanos = System.nanoTime() - startNanos; + long millis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos); + return String.format("PT%d.%03dS", millis / 1000, millis % 1000); + } + + /** + * Logs a timing message at the given level if the logger accepts it. + * + * @param logger + * the logger to use + * @param level + * the log level + * @param message + * the message template + * @param startNanos + * the start time from {@link System#nanoTime()} + */ + static void logTiming(Logger logger, Level level, String message, long startNanos) { + if (!logger.isLoggable(level)) { + return; + } + logger.log(level, message.replace("{Elapsed}", formatElapsed(startNanos))); + } + + /** + * Logs a timing message at the given level with an exception. + * + * @param logger + * the logger to use + * @param level + * the log level + * @param exception + * the exception, may be {@code null} + * @param message + * the message template + * @param startNanos + * the start time from {@link System#nanoTime()} + */ + static void logTiming(Logger logger, Level level, Throwable exception, String message, long startNanos) { + if (!logger.isLoggable(level)) { + return; + } + String formatted = message.replace("{Elapsed}", formatElapsed(startNanos)); + if (exception != null) { + logger.log(level, formatted, exception); + } else { + logger.log(level, formatted); + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/java/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java new file mode 100644 index 000000000..1d76d8b88 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -0,0 +1,514 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.ExitPlanModeRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.SessionLifecycleEventMetadata; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolInvocation; +import com.github.copilot.sdk.json.ToolResultObject; +import com.github.copilot.sdk.json.UserInputRequest; + +/** + * Dispatches incoming JSON-RPC method calls to the appropriate handlers. + *

+ * This class handles all server-to-client RPC calls including: + *

    + *
  • Session events
  • + *
  • Tool calls
  • + *
  • Permission requests
  • + *
  • User input requests
  • + *
  • Hooks invocations
  • + *
  • Lifecycle events
  • + *
+ */ +final class RpcHandlerDispatcher { + + private static final Logger LOG = Logger.getLogger(RpcHandlerDispatcher.class.getName()); + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final Map sessions; + private final LifecycleEventDispatcher lifecycleDispatcher; + private final Executor executor; + + /** + * Creates a dispatcher with session registry and lifecycle dispatcher. + * + * @param sessions + * the session registry to look up sessions by ID + * @param lifecycleDispatcher + * callback for dispatching lifecycle events + * @param executor + * the executor for async dispatch, or {@code null} for default + */ + RpcHandlerDispatcher(Map sessions, LifecycleEventDispatcher lifecycleDispatcher, + Executor executor) { + this.sessions = sessions; + this.lifecycleDispatcher = lifecycleDispatcher; + this.executor = executor; + } + + /** + * Registers all RPC method handlers with the given JSON-RPC client. + * + * @param rpc + * the JSON-RPC client to register handlers with + */ + void registerHandlers(JsonRpcClient rpc) { + rpc.registerMethodHandler("session.event", (requestId, params) -> handleSessionEvent(params)); + rpc.registerMethodHandler("session.lifecycle", (requestId, params) -> handleLifecycleEvent(params)); + rpc.registerMethodHandler("tool.call", (requestId, params) -> handleToolCall(rpc, requestId, params)); + rpc.registerMethodHandler("permission.request", + (requestId, params) -> handlePermissionRequest(rpc, requestId, params)); + rpc.registerMethodHandler("userInput.request", + (requestId, params) -> handleUserInputRequest(rpc, requestId, params)); + rpc.registerMethodHandler("exitPlanMode.request", + (requestId, params) -> handleExitPlanModeRequest(rpc, requestId, params)); + rpc.registerMethodHandler("autoModeSwitch.request", + (requestId, params) -> handleAutoModeSwitchRequest(rpc, requestId, params)); + rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params)); + rpc.registerMethodHandler("systemMessage.transform", + (requestId, params) -> handleSystemMessageTransform(rpc, requestId, params)); + } + + private void handleSessionEvent(JsonNode params) { + try { + String sessionId = params.get("sessionId").asText(); + JsonNode eventNode = params.get("event"); + LOG.fine("Received session.event: " + eventNode); + + CopilotSession session = sessions.get(sessionId); + if (session != null && eventNode != null) { + SessionEvent event = MAPPER.treeToValue(eventNode, SessionEvent.class); + if (event != null) { + session.dispatchEvent(event); + } + } + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling session event", e); + } + } + + private void handleLifecycleEvent(JsonNode params) { + try { + String type = params.has("type") ? params.get("type").asText() : ""; + String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : ""; + + SessionLifecycleEvent event = new SessionLifecycleEvent(); + event.setType(type); + event.setSessionId(sessionId); + + if (params.has("metadata") && !params.get("metadata").isNull()) { + SessionLifecycleEventMetadata metadata = MAPPER.treeToValue(params.get("metadata"), + SessionLifecycleEventMetadata.class); + event.setMetadata(metadata); + } + + lifecycleDispatcher.dispatch(event); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling session lifecycle event", e); + } + } + + private void handleToolCall(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "tool.call"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + String toolCallId = params.get("toolCallId").asText(); + String toolName = params.get("toolName").asText(); + JsonNode arguments = params.get("arguments"); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + ToolDefinition tool = session.getTool(toolName); + if (tool == null || tool.handler() == null) { + var result = ToolResultObject.failure("Tool '" + toolName + "' is not supported.", + "tool '" + toolName + "' not supported"); + rpc.sendResponse(requestIdLong, Map.of("result", result)); + return; + } + + var invocation = new ToolInvocation().setSessionId(sessionId).setToolCallId(toolCallId) + .setToolName(toolName).setArguments(arguments); + + tool.handler().invoke(invocation).thenAccept(result -> { + try { + ToolResultObject toolResult; + if (result instanceof ToolResultObject tr) { + toolResult = tr; + } else { + toolResult = ToolResultObject + .success(result instanceof String s ? s : MAPPER.writeValueAsString(result)); + } + rpc.sendResponse(requestIdLong, Map.of("result", toolResult)); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error sending tool result", e); + } + }).exceptionally(ex -> { + try { + var result = ToolResultObject.failure( + "Invoking this tool produced an error. Detailed information is not available.", + ex.getMessage()); + rpc.sendResponse(requestIdLong, Map.of("result", result)); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error sending tool error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling tool call", e); + try { + rpc.sendErrorResponse(requestIdLong, -32603, e.getMessage()); + } catch (IOException ioe) { + LOG.log(Level.SEVERE, "Failed to send error response", ioe); + } + } + }); + } + + private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "permission.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + JsonNode permissionRequest = params.get("permissionRequest"); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + var result = new PermissionRequestResult() + .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + rpc.sendResponse(requestIdLong, Map.of("result", result)); + return; + } + + session.handlePermissionRequest(permissionRequest).thenAccept(result -> { + try { + if (PermissionRequestResultKind.NO_RESULT.getValue().equalsIgnoreCase(result.getKind())) { + // Protocol v2 does not support NO_RESULT β€” the server + // expects exactly one response per request, so abstaining + // would leave it hanging. + throw new IllegalStateException( + "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."); + } + rpc.sendResponse(requestIdLong, Map.of("result", result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending permission result", e); + } + }).exceptionally(ex -> { + try { + var result = new PermissionRequestResult() + .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + rpc.sendResponse(requestIdLong, Map.of("result", result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending permission denied", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling permission request", e); + } + }); + } + + private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + LOG.fine("Received userInput.request: " + params); + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "userInput.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + String question = params.get("question").asText(); + LOG.fine("Processing userInput for session " + sessionId + ", question: " + question); + JsonNode choicesNode = params.get("choices"); + JsonNode allowFreeformNode = params.get("allowFreeform"); + + CopilotSession session = sessions.get(sessionId); + LOG.fine("Found session: " + (session != null)); + if (session == null) { + LOG.fine("Session not found, sending error"); + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + var request = new UserInputRequest().setQuestion(question); + if (choicesNode != null && choicesNode.isArray()) { + var choices = new ArrayList(); + for (JsonNode choice : choicesNode) { + choices.add(choice.asText()); + } + request.setChoices(choices); + } + if (allowFreeformNode != null) { + request.setAllowFreeform(allowFreeformNode.asBoolean()); + } + + session.handleUserInputRequest(request).thenAccept(response -> { + try { + // Ensure answer is never null - CLI requires a non-null string + String answer = response.getAnswer() != null ? response.getAnswer() : ""; + LOG.fine("Sending userInput response: answer=" + answer + ", wasFreeform=" + + response.isWasFreeform()); + rpc.sendResponse(requestIdLong, + Map.of("answer", answer, "wasFreeform", response.isWasFreeform())); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending user input response", e); + } + }).exceptionally(ex -> { + LOG.log(Level.WARNING, "User input handler exception", ex); + try { + rpc.sendErrorResponse(requestIdLong, -32603, "User input handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending user input error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling user input request", e); + } + }); + } + + private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "exitPlanMode.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + var request = new ExitPlanModeRequest(); + if (params.has("summary")) { + request.setSummary(params.get("summary").asText()); + } + if (params.has("planContent") && !params.get("planContent").isNull()) { + request.setPlanContent(params.get("planContent").asText()); + } + if (params.has("actions") && params.get("actions").isArray()) { + var actions = new ArrayList(); + for (JsonNode action : params.get("actions")) { + actions.add(action.asText()); + } + request.setActions(actions); + } + if (params.has("recommendedAction") && !params.get("recommendedAction").isNull()) { + request.setRecommendedAction(params.get("recommendedAction").asText()); + } + + session.handleExitPlanModeRequest(request).thenAccept(result -> { + try { + rpc.sendResponse(requestIdLong, MAPPER.valueToTree(result)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, + "Exit plan mode handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling exit plan mode request", e); + } + }); + } + + private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "autoModeSwitch.request"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + var request = new AutoModeSwitchRequest(); + if (params.has("errorCode") && !params.get("errorCode").isNull()) { + request.setErrorCode(params.get("errorCode").asText()); + } + if (params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()) { + request.setRetryAfterSeconds(params.get("retryAfterSeconds").asDouble()); + } + + session.handleAutoModeSwitchRequest(request).thenAccept(response -> { + try { + rpc.sendResponse(requestIdLong, Map.of("response", response)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, + "Auto mode switch handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling auto mode switch request", e); + } + }); + } + + private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "hooks.invoke"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.get("sessionId").asText(); + String hookType = params.get("hookType").asText(); + JsonNode input = params.get("input"); + + CopilotSession session = sessions.get(sessionId); + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + session.handleHooksInvoke(hookType, input).thenAccept(output -> { + try { + rpc.sendResponse(requestIdLong, Collections.singletonMap("output", output)); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending hooks response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, "Hooks handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending hooks error", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling hooks invoke", e); + } + }); + } + + /** + * Functional interface for dispatching lifecycle events. + */ + @FunctionalInterface + interface LifecycleEventDispatcher { + + void dispatch(SessionLifecycleEvent event); + } + + private void handleSystemMessageTransform(JsonRpcClient rpc, String requestId, JsonNode params) { + runAsync(() -> { + final long requestIdLong = parseRequestId(requestId, "systemMessage.transform"); + if (requestIdLong == -1) { + return; + } + try { + String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : null; + JsonNode sections = params.get("sections"); + + CopilotSession session = sessionId != null ? sessions.get(sessionId) : null; + if (session == null) { + rpc.sendErrorResponse(requestIdLong, -32602, "Unknown session " + sessionId); + return; + } + + session.handleSystemMessageTransform(sections).thenAccept(result -> { + try { + rpc.sendResponse(requestIdLong, result); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending systemMessage.transform response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(requestIdLong, -32603, "Transform error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending transform error response", e); + } + return null; + }); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error handling systemMessage.transform", e); + } + }); + } + + /** + * Parses a JSON-RPC request ID string into a {@code long}. + * + * @param requestId + * the request ID string received from the JSON-RPC layer + * @param methodName + * the RPC method name, used in the log message on failure + * @return the parsed request ID, or {@code -1} if the string is not a valid + * long + */ + private static long parseRequestId(String requestId, String methodName) { + try { + return Long.parseLong(requestId); + } catch (NumberFormatException nfe) { + LOG.log(Level.SEVERE, "Invalid requestId for " + methodName + ": " + requestId, nfe); + return -1; + } + } + + private void runAsync(Runnable task) { + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, "Executor rejected handler task; running inline", e); + task.run(); + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java new file mode 100644 index 000000000..3b00a88ae --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/SdkProtocolVersion.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// Code generated by update-protocol-version.ts. DO NOT EDIT. + +package com.github.copilot.sdk; + +/** + * Provides the SDK protocol version. This must match the version expected by + * the copilot-agent-runtime server. + * + * @since 1.0.0 + */ +public enum SdkProtocolVersion { + + LATEST(3); + + private int versionNumber; + + private SdkProtocolVersion(int versionNumber) { + this.versionNumber = versionNumber; + } + + public int getVersionNumber() { + return this.versionNumber; + } + + /** + * Gets the SDK protocol version. + * + * @return the protocol version + */ + public static int get() { + return LATEST.getVersionNumber(); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java new file mode 100644 index 000000000..0cdc4f942 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -0,0 +1,336 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.CommandWireDefinition; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.SectionOverride; +import com.github.copilot.sdk.json.SectionOverrideAction; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; + +/** + * Builds JSON-RPC request objects from session configuration. + *

+ * This class handles the conversion of SDK configuration objects + * ({@link SessionConfig}, {@link ResumeSessionConfig}) to JSON-RPC request + * objects for session creation and resumption. + */ +final class SessionRequestBuilder { + + private SessionRequestBuilder() { + // Utility class + } + + /** + * Extracts transform callbacks from a {@link SystemMessageConfig} and returns a + * wire-safe copy of the config alongside the extracted callbacks. + *

+ * When the system message mode is {@link SystemMessageMode#CUSTOMIZE} and some + * sections have {@link SectionOverride#getTransform() transform} callbacks set, + * this method: + *

    + *
  1. Removes the callbacks from the wire config (they must not be + * serialized).
  2. + *
  3. Replaces each transform section with + * {@link SectionOverrideAction#TRANSFORM} in the wire config.
  4. + *
  5. Returns the callbacks so they can be registered with the session.
  6. + *
+ * + * @param systemMessage + * the system message config, may be {@code null} + * @return an {@link ExtractedTransforms} containing the wire-safe config and + * any extracted callbacks + */ + static ExtractedTransforms extractTransformCallbacks(SystemMessageConfig systemMessage) { + if (systemMessage == null || systemMessage.getMode() != SystemMessageMode.CUSTOMIZE + || systemMessage.getSections() == null) { + return new ExtractedTransforms(systemMessage, null); + } + + Map>> callbacks = new HashMap<>(); + Map wireSections = new HashMap<>(); + + for (Map.Entry entry : systemMessage.getSections().entrySet()) { + String sectionId = entry.getKey(); + SectionOverride override = entry.getValue(); + + if (override.getTransform() != null) { + callbacks.put(sectionId, override.getTransform()); + wireSections.put(sectionId, new SectionOverride().setAction(SectionOverrideAction.TRANSFORM)); + } else { + wireSections.put(sectionId, override); + } + } + + if (callbacks.isEmpty()) { + return new ExtractedTransforms(systemMessage, null); + } + + // Build a wire-safe copy of the system message with callbacks removed + var wireConfig = new SystemMessageConfig().setMode(systemMessage.getMode()) + .setContent(systemMessage.getContent()).setSections(wireSections); + + return new ExtractedTransforms(wireConfig, callbacks); + } + + /** + * Builds a CreateSessionRequest from the given configuration. + * + * @param config + * the session configuration (may be null) + * @param sessionId + * the pre-generated session ID to use + * @return the built request object + */ + static CreateSessionRequest buildCreateRequest(SessionConfig config, String sessionId) { + var request = new CreateSessionRequest(); + // Always request permission callbacks to enable deny-by-default behavior + request.setRequestPermission(true); + // Always send envValueMode=direct for MCP servers + request.setEnvValueMode("direct"); + request.setSessionId(sessionId); + if (config == null) { + return request; + } + + request.setModel(config.getModel()); + request.setClientName(config.getClientName()); + request.setReasoningEffort(config.getReasoningEffort()); + request.setTools(config.getTools()); + request.setSystemMessage(config.getSystemMessage()); + request.setAvailableTools(config.getAvailableTools()); + request.setExcludedTools(config.getExcludedTools()); + request.setProvider(config.getProvider()); + config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + if (config.getOnUserInputRequest() != null) { + request.setRequestUserInput(true); + } + if (config.getHooks() != null && config.getHooks().hasHooks()) { + request.setHooks(true); + } + request.setWorkingDirectory(config.getWorkingDirectory()); + if (config.isStreaming()) { + request.setStreaming(true); + } + config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); + request.setMcpServers(config.getMcpServers()); + request.setCustomAgents(config.getCustomAgents()); + request.setDefaultAgent(config.getDefaultAgent()); + request.setAgent(config.getAgent()); + request.setInfiniteSessions(config.getInfiniteSessions()); + request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); + request.setDisabledSkills(config.getDisabledSkills()); + request.setConfigDir(config.getConfigDir()); + config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); + request.setModelCapabilities(config.getModelCapabilities()); + + if (config.getCommands() != null && !config.getCommands().isEmpty()) { + var wireCommands = config.getCommands().stream() + .map(c -> new CommandWireDefinition(c.getName(), c.getDescription())) + .collect(java.util.stream.Collectors.toList()); + request.setCommands(wireCommands); + } + if (config.getOnElicitationRequest() != null) { + request.setRequestElicitation(true); + } + if (config.getOnExitPlanMode() != null) { + request.setRequestExitPlanMode(true); + } + if (config.getOnAutoModeSwitch() != null) { + request.setRequestAutoModeSwitch(true); + } + request.setGitHubToken(config.getGitHubToken()); + request.setRemoteSession(config.getRemoteSession()); + request.setCloud(config.getCloud()); + + return request; + } + + /** + * Builds a CreateSessionRequest from the given configuration. + * + * @param config + * the session configuration (may be null) + * @return the built request object + * @deprecated Use {@link #buildCreateRequest(SessionConfig, String)} instead. + */ + @Deprecated + static CreateSessionRequest buildCreateRequest(SessionConfig config) { + String sessionId = (config != null && config.getSessionId() != null) + ? config.getSessionId() + : java.util.UUID.randomUUID().toString(); + return buildCreateRequest(config, sessionId); + } + + /** + * Builds a ResumeSessionRequest from the given session ID and configuration. + * + * @param sessionId + * the ID of the session to resume + * @param config + * the resume configuration (may be null) + * @return the built request object + */ + static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionConfig config) { + var request = new ResumeSessionRequest(); + request.setSessionId(sessionId); + // Always request permission callbacks to enable deny-by-default behavior + request.setRequestPermission(true); + // Always send envValueMode=direct for MCP servers + request.setEnvValueMode("direct"); + + if (config == null) { + return request; + } + + request.setModel(config.getModel()); + request.setClientName(config.getClientName()); + request.setReasoningEffort(config.getReasoningEffort()); + request.setTools(config.getTools()); + request.setSystemMessage(config.getSystemMessage()); + request.setAvailableTools(config.getAvailableTools()); + request.setExcludedTools(config.getExcludedTools()); + request.setProvider(config.getProvider()); + config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); + if (config.getOnUserInputRequest() != null) { + request.setRequestUserInput(true); + } + if (config.getHooks() != null && config.getHooks().hasHooks()) { + request.setHooks(true); + } + request.setWorkingDirectory(config.getWorkingDirectory()); + request.setConfigDir(config.getConfigDir()); + config.getEnableConfigDiscovery().ifPresent(request::setEnableConfigDiscovery); + if (config.isDisableResume()) { + request.setDisableResume(true); + } + if (config.isStreaming()) { + request.setStreaming(true); + } + config.getIncludeSubAgentStreamingEvents().ifPresent(request::setIncludeSubAgentStreamingEvents); + request.setMcpServers(config.getMcpServers()); + request.setCustomAgents(config.getCustomAgents()); + request.setDefaultAgent(config.getDefaultAgent()); + request.setAgent(config.getAgent()); + request.setSkillDirectories(config.getSkillDirectories()); + request.setInstructionDirectories(config.getInstructionDirectories()); + request.setDisabledSkills(config.getDisabledSkills()); + request.setInfiniteSessions(config.getInfiniteSessions()); + request.setModelCapabilities(config.getModelCapabilities()); + + if (config.getCommands() != null && !config.getCommands().isEmpty()) { + var wireCommands = config.getCommands().stream() + .map(c -> new CommandWireDefinition(c.getName(), c.getDescription())) + .collect(java.util.stream.Collectors.toList()); + request.setCommands(wireCommands); + } + if (config.getOnElicitationRequest() != null) { + request.setRequestElicitation(true); + } + if (config.getOnExitPlanMode() != null) { + request.setRequestExitPlanMode(true); + } + if (config.getOnAutoModeSwitch() != null) { + request.setRequestAutoModeSwitch(true); + } + request.setGitHubToken(config.getGitHubToken()); + request.setRemoteSession(config.getRemoteSession()); + + return request; + } + + /** + * Configures a session with handlers from the given config. + * + * @param session + * the session to configure + * @param config + * the session configuration + */ + static void configureSession(CopilotSession session, SessionConfig config) { + if (config == null) { + return; + } + + if (config.getTools() != null) { + session.registerTools(config.getTools()); + } + if (config.getOnPermissionRequest() != null) { + session.registerPermissionHandler(config.getOnPermissionRequest()); + } + if (config.getOnUserInputRequest() != null) { + session.registerUserInputHandler(config.getOnUserInputRequest()); + } + if (config.getHooks() != null) { + session.registerHooks(config.getHooks()); + } + if (config.getCommands() != null) { + session.registerCommands(config.getCommands()); + } + if (config.getOnElicitationRequest() != null) { + session.registerElicitationHandler(config.getOnElicitationRequest()); + } + if (config.getOnExitPlanMode() != null) { + session.registerExitPlanModeHandler(config.getOnExitPlanMode()); + } + if (config.getOnAutoModeSwitch() != null) { + session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch()); + } + if (config.getOnEvent() != null) { + session.on(config.getOnEvent()); + } + } + + /** + * Configures a resumed session with handlers from the given config. + * + * @param session + * the session to configure + * @param config + * the resume session configuration + */ + static void configureSession(CopilotSession session, ResumeSessionConfig config) { + if (config == null) { + return; + } + + if (config.getTools() != null) { + session.registerTools(config.getTools()); + } + if (config.getOnPermissionRequest() != null) { + session.registerPermissionHandler(config.getOnPermissionRequest()); + } + if (config.getOnUserInputRequest() != null) { + session.registerUserInputHandler(config.getOnUserInputRequest()); + } + if (config.getHooks() != null) { + session.registerHooks(config.getHooks()); + } + if (config.getCommands() != null) { + session.registerCommands(config.getCommands()); + } + if (config.getOnElicitationRequest() != null) { + session.registerElicitationHandler(config.getOnElicitationRequest()); + } + if (config.getOnExitPlanMode() != null) { + session.registerExitPlanModeHandler(config.getOnExitPlanMode()); + } + if (config.getOnAutoModeSwitch() != null) { + session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch()); + } + if (config.getOnEvent() != null) { + session.on(config.getOnEvent()); + } + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java new file mode 100644 index 000000000..67fb5ea5e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/SystemMessageMode.java @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Specifies how the system message should be applied to a session. + *

+ * The system message controls the behavior and personality of the AI assistant. + * This enum determines whether to append custom instructions to the default + * system message or replace it entirely. + * + * @see com.github.copilot.sdk.json.SystemMessageConfig + * @since 1.0.0 + */ +public enum SystemMessageMode { + /** + * Append the custom content to the default system message. + *

+ * This mode preserves the default guardrails and behaviors while adding + * additional instructions or context. + */ + APPEND("append"), + + /** + * Replace the default system message entirely with the custom content. + *

+ * Warning: This mode removes all default guardrails and + * behaviors. Use with caution. + */ + REPLACE("replace"), + + /** + * Override individual sections of the system prompt. + *

+ * Use this mode with + * {@link com.github.copilot.sdk.json.SystemMessageConfig#setSections} to + * selectively replace, remove, append, prepend, or transform individual + * sections of the default system prompt. An optional {@code content} string is + * appended after all sections when provided. + * + * @since 1.2.0 + */ + CUSTOMIZE("customize"); + + private final String value; + + SystemMessageMode(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this mode. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AgentInfo.java b/java/src/main/java/com/github/copilot/sdk/json/AgentInfo.java new file mode 100644 index 000000000..1cc10a91d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AgentInfo.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a custom agent available for selection in a session. + * + * @since 1.0.11 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AgentInfo { + + @JsonProperty("name") + private String name; + + @JsonProperty("displayName") + private String displayName; + + @JsonProperty("description") + private String description; + + /** + * Gets the unique identifier of the agent. + * + * @return the agent name/identifier + */ + public String getName() { + return name; + } + + /** + * Sets the unique identifier of the agent. + * + * @param name + * the agent name/identifier + * @return this instance for chaining + */ + public AgentInfo setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable display name of the agent. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable display name of the agent. + * + * @param displayName + * the display name + * @return this instance for chaining + */ + public AgentInfo setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Gets the description of the agent's purpose. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the agent's purpose. + * + * @param description + * the description + * @return this instance for chaining + */ + public AgentInfo setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/Attachment.java b/java/src/main/java/com/github/copilot/sdk/json/Attachment.java new file mode 100644 index 000000000..04011d6f1 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/Attachment.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a file attachment to include with a message. + *

+ * Attachments provide additional context to the AI assistant, such as source + * code files, documents, or other relevant content. + * + *

Example Usage

+ * + *
{@code
+ * var attachment = new Attachment("file", "/path/to/source.java", "Main Source File");
+ * }
+ * + * @param type + * the attachment type (e.g., "file") + * @param path + * the absolute path to the file on the filesystem + * @param displayName + * a human-readable display name for the attachment + * @see MessageOptions#setAttachments(java.util.List) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Attachment(@JsonProperty("type") String type, @JsonProperty("path") String path, + @JsonProperty("displayName") String displayName) implements MessageAttachment { + + @Override + public String getType() { + return type; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java new file mode 100644 index 000000000..5fa19f4b1 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for auto-mode-switch requests from the agent. + *

+ * Register an auto-mode-switch handler via + * {@link SessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)} or + * {@link ResumeSessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)}. When + * provided, the server routes {@code autoModeSwitch.request} callbacks to this + * handler. + * + *

Example Usage

+ * + *
{@code
+ * AutoModeSwitchHandler handler = (request, invocation) -> {
+ * 	System.out.println("Rate limited: " + request.getErrorCode());
+ * 	return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
+ * };
+ *
+ * var session = client.createSession(new SessionConfig().setOnAutoModeSwitch(handler)).get();
+ * }
+ * + * @see AutoModeSwitchRequest + * @see AutoModeSwitchResponse + * @since 1.0.8 + */ +@FunctionalInterface +public interface AutoModeSwitchHandler { + + /** + * Handles an auto-mode-switch request from the agent. + * + * @param request + * the auto-mode-switch request containing the error code and + * retry-after seconds + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(AutoModeSwitchRequest request, + AutoModeSwitchInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java new file mode 100644 index 000000000..278d10ccf --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an auto-mode-switch request invocation. + * + * @since 1.0.8 + */ +public class AutoModeSwitchInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public AutoModeSwitchInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java new file mode 100644 index 000000000..0ef784c02 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request to switch to auto mode after an eligible rate limit. + *

+ * This is sent by the server when the agent encounters a rate limit and wants + * to switch to an alternative model automatically. + * + * @since 1.0.8 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AutoModeSwitchRequest { + + @JsonProperty("errorCode") + private String errorCode; + + @JsonProperty("retryAfterSeconds") + private Double retryAfterSeconds; + + /** + * Gets the rate-limit error code that triggered the request. + * + * @return the error code, or {@code null} + */ + public String getErrorCode() { + return errorCode; + } + + /** + * Sets the rate-limit error code. + * + * @param errorCode + * the error code + * @return this instance for method chaining + */ + public AutoModeSwitchRequest setErrorCode(String errorCode) { + this.errorCode = errorCode; + return this; + } + + /** + * Gets the seconds until the rate limit resets, when known. + * + * @return the retry-after seconds, or {@code null} + */ + public Double getRetryAfterSeconds() { + return retryAfterSeconds; + } + + /** + * Sets the seconds until the rate limit resets. + * + * @param retryAfterSeconds + * the retry-after seconds + * @return this instance for method chaining + */ + public AutoModeSwitchRequest setRetryAfterSeconds(Double retryAfterSeconds) { + this.retryAfterSeconds = retryAfterSeconds; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java new file mode 100644 index 000000000..c072b73e2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Response to an auto-mode-switch request. + * + * @since 1.0.8 + */ +public enum AutoModeSwitchResponse { + + /** Approve the switch for this rate-limit cycle. */ + YES("yes"), + + /** Approve and remember the choice for this session. */ + YES_ALWAYS("yes_always"), + + /** Decline the switch. */ + NO("no"); + + private final String value; + + AutoModeSwitchResponse(String value) { + this.value = value; + } + + /** + * Gets the wire value of this response. + * + * @return the string value + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java new file mode 100644 index 000000000..c2e146c19 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/AzureOptions.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Azure OpenAI-specific configuration options. + *

+ * When using a BYOK (Bring Your Own Key) setup with Azure OpenAI, this class + * allows you to specify Azure-specific settings such as the API version to use. + * + *

Example Usage

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("azure-openai").setHost("your-resource.openai.azure.com")
+ * 		.setApiKey("your-api-key").setAzure(new AzureOptions().setApiVersion("2024-02-01"));
+ * }
+ * + * @see ProviderConfig#setAzure(AzureOptions) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AzureOptions { + + @JsonProperty("apiVersion") + private String apiVersion; + + /** + * Gets the Azure OpenAI API version. + * + * @return the API version string + */ + public String getApiVersion() { + return apiVersion; + } + + /** + * Sets the Azure OpenAI API version to use. + *

+ * Examples: {@code "2024-02-01"}, {@code "2023-12-01-preview"} + * + * @param apiVersion + * the API version string + * @return this options object for method chaining + */ + public AzureOptions setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/BlobAttachment.java b/java/src/main/java/com/github/copilot/sdk/json/BlobAttachment.java new file mode 100644 index 000000000..d58a1e15e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/BlobAttachment.java @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an inline base64-encoded binary attachment (blob) for messages. + *

+ * Use this attachment type to pass image data or other binary content directly + * to the assistant, without requiring a file on disk. + * + *

Example Usage

+ * + *
{@code
+ * var attachment = new BlobAttachment().setData("iVBORw0KGgoAAAANSUhEUg...") // base64-encoded content
+ * 		.setMimeType("image/png").setDisplayName("screenshot.png");
+ *
+ * var options = new MessageOptions().setPrompt("Describe this image").setAttachments(List.of(attachment));
+ * }
+ * + * @see MessageOptions#setAttachments(java.util.List) + * @since 1.2.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class BlobAttachment implements MessageAttachment { + + @JsonProperty("type") + private final String type = "blob"; + + @JsonProperty("data") + private String data; + + @JsonProperty("mimeType") + private String mimeType; + + @JsonProperty("displayName") + private String displayName; + + /** + * Returns the attachment type, always {@code "blob"}. + * + * @return {@code "blob"} + */ + @Override + public String getType() { + return type; + } + + /** + * Gets the base64-encoded binary content. + * + * @return the base64 data string + */ + public String getData() { + return data; + } + + /** + * Sets the base64-encoded binary content. + * + * @param data + * the base64-encoded content + * @return this attachment for method chaining + */ + public BlobAttachment setData(String data) { + this.data = data; + return this; + } + + /** + * Gets the MIME type of the binary content. + * + * @return the MIME type (e.g., {@code "image/png"}) + */ + public String getMimeType() { + return mimeType; + } + + /** + * Sets the MIME type of the binary content. + * + * @param mimeType + * the MIME type (e.g., {@code "image/png"}, {@code "image/jpeg"}) + * @return this attachment for method chaining + */ + public BlobAttachment setMimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + /** + * Gets the human-readable display name for the attachment. + * + * @return the display name, or {@code null} + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable display name for the attachment. + * + * @param displayName + * a user-visible name (e.g., {@code "screenshot.png"}) + * @return this attachment for method chaining + */ + public BlobAttachment setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CloudSessionOptions.java b/java/src/main/java/com/github/copilot/sdk/json/CloudSessionOptions.java new file mode 100644 index 000000000..897d07eda --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CloudSessionOptions.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Options for creating a remote session in the cloud. + * + * @since 1.5.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CloudSessionOptions { + + @JsonProperty("repository") + private CloudSessionRepository repository; + + /** + * Gets the optional GitHub repository metadata to associate with the cloud + * session. + * + * @return the repository metadata, or {@code null} if not set + */ + public CloudSessionRepository getRepository() { + return repository; + } + + /** + * Sets the optional GitHub repository metadata to associate with the cloud + * session. + * + * @param repository + * the repository metadata + * @return this instance for method chaining + */ + public CloudSessionOptions setRepository(CloudSessionRepository repository) { + this.repository = repository; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CloudSessionRepository.java b/java/src/main/java/com/github/copilot/sdk/json/CloudSessionRepository.java new file mode 100644 index 000000000..5806c22cd --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CloudSessionRepository.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * GitHub repository metadata to associate with a cloud session. + * + * @since 1.5.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CloudSessionRepository { + + @JsonProperty("owner") + private String owner; + + @JsonProperty("name") + private String name; + + @JsonProperty("branch") + private String branch; + + /** + * Gets the repository owner. + * + * @return the repository owner + */ + public String getOwner() { + return owner; + } + + /** + * Sets the repository owner. + * + * @param owner + * the repository owner + * @return this instance for method chaining + */ + public CloudSessionRepository setOwner(String owner) { + this.owner = owner; + return this; + } + + /** + * Gets the repository name. + * + * @return the repository name + */ + public String getName() { + return name; + } + + /** + * Sets the repository name. + * + * @param name + * the repository name + * @return this instance for method chaining + */ + public CloudSessionRepository setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the optional branch name. + * + * @return the branch name, or {@code null} if not set + */ + public String getBranch() { + return branch; + } + + /** + * Sets the optional branch name. + * + * @param branch + * the branch name + * @return this instance for method chaining + */ + public CloudSessionRepository setBranch(String branch) { + this.branch = branch; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CommandContext.java b/java/src/main/java/com/github/copilot/sdk/json/CommandContext.java new file mode 100644 index 000000000..4657699bb --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CommandContext.java @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context passed to a {@link CommandHandler} when a slash command is executed. + * + * @since 1.0.0 + */ +public class CommandContext { + + private String sessionId; + private String command; + private String commandName; + private String args; + + /** Gets the session ID where the command was invoked. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public CommandContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the full command text (e.g., {@code /deploy production}). + * + * @return the full command text + */ + public String getCommand() { + return command; + } + + /** Sets the full command text. @param command the command text @return this */ + public CommandContext setCommand(String command) { + this.command = command; + return this; + } + + /** + * Gets the command name without the leading {@code /}. + * + * @return the command name + */ + public String getCommandName() { + return commandName; + } + + /** Sets the command name. @param commandName the command name @return this */ + public CommandContext setCommandName(String commandName) { + this.commandName = commandName; + return this; + } + + /** + * Gets the raw argument string after the command name. + * + * @return the argument string + */ + public String getArgs() { + return args; + } + + /** Sets the argument string. @param args the argument string @return this */ + public CommandContext setArgs(String args) { + this.args = args; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java b/java/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java new file mode 100644 index 000000000..33a6cbada --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CommandDefinition.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Defines a slash command that users can invoke from the CLI TUI. + *

+ * Register commands via {@link SessionConfig#setCommands(java.util.List)} or + * {@link ResumeSessionConfig#setCommands(java.util.List)}. Each command appears + * as {@code /name} in the CLI TUI. + * + *

Example Usage

+ * + *
{@code
+ * var config = new SessionConfig().setCommands(List.of(
+ * 		new CommandDefinition().setName("deploy").setDescription("Deploy the application").setHandler(context -> {
+ * 			System.out.println("Deploying: " + context.getArgs());
+ * 			return CompletableFuture.completedFuture(null);
+ * 		})));
+ * }
+ * + * @see CommandHandler + * @see CommandContext + * @since 1.0.0 + */ +public class CommandDefinition { + + private String name; + private String description; + private CommandHandler handler; + + /** + * Gets the command name (without leading {@code /}). + * + * @return the command name + */ + public String getName() { + return name; + } + + /** + * Sets the command name (without leading {@code /}). + *

+ * For example, {@code "deploy"} registers the {@code /deploy} command. + * + * @param name + * the command name + * @return this instance for method chaining + */ + public CommandDefinition setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable description shown in the command completion UI. + * + * @return the description, or {@code null} if not set + */ + public String getDescription() { + return description; + } + + /** + * Sets the human-readable description shown in the command completion UI. + * + * @param description + * the description + * @return this instance for method chaining + */ + public CommandDefinition setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the handler invoked when the command is executed. + * + * @return the command handler + */ + public CommandHandler getHandler() { + return handler; + } + + /** + * Sets the handler invoked when the command is executed. + * + * @param handler + * the command handler + * @return this instance for method chaining + */ + public CommandDefinition setHandler(CommandHandler handler) { + this.handler = handler; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CommandHandler.java b/java/src/main/java/com/github/copilot/sdk/json/CommandHandler.java new file mode 100644 index 000000000..d63955638 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CommandHandler.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling slash-command executions. + *

+ * Implement this interface to define the behavior of a registered slash + * command. The handler is invoked when the user executes the command in the CLI + * TUI. + * + *

Example Usage

+ * + *
{@code
+ * CommandHandler deployHandler = context -> {
+ * 	System.out.println("Deploying with args: " + context.getArgs());
+ * 	// perform deployment...
+ * 	return CompletableFuture.completedFuture(null);
+ * };
+ * }
+ * + * @see CommandDefinition + * @since 1.0.0 + */ +@FunctionalInterface +public interface CommandHandler { + + /** + * Handles a slash-command execution. + * + * @param context + * the command context containing session ID, command text, and + * arguments + * @return a future that completes when the command handling is done + */ + CompletableFuture handle(CommandContext context); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java b/java/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java new file mode 100644 index 000000000..2ee65c58e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CommandWireDefinition.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Wire-format representation of a command definition for RPC serialization. + *

+ * This is a low-level class used internally. Use {@link CommandDefinition} to + * define commands for a session. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CommandWireDefinition { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + /** Creates an empty definition. */ + public CommandWireDefinition() { + } + + /** Creates a definition with name and description. */ + public CommandWireDefinition(String name, String description) { + this.name = name; + this.description = description; + } + + /** Gets the command name. @return the name */ + public String getName() { + return name; + } + + /** Sets the command name. @param name the name @return this */ + public CommandWireDefinition setName(String name) { + this.name = name; + return this; + } + + /** Gets the description. @return the description */ + public String getDescription() { + return description; + } + + /** Sets the description. @param description the description @return this */ + public CommandWireDefinition setDescription(String description) { + this.description = description; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java new file mode 100644 index 000000000..e4605ffe1 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java @@ -0,0 +1,666 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * Configuration options for creating a + * {@link com.github.copilot.sdk.CopilotClient}. + *

+ * This class provides a fluent API for configuring how the client connects to + * and manages the Copilot CLI server. All setter methods return {@code this} + * for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions().setCliPath("/usr/local/bin/copilot").setLogLevel("debug")
+ * 		.setAutoStart(true);
+ *
+ * var client = new CopilotClient(options);
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CopilotClientOptions { + + @Deprecated + private boolean autoRestart; + private boolean autoStart = true; + private String[] cliArgs; + private String cliPath; + private String cliUrl; + private String copilotHome; + private String cwd; + private Map environment; + private Executor executor; + private String gitHubToken; + private String logLevel = "info"; + private Supplier>> onListModels; + private int port; + private TelemetryConfig telemetry; + private Integer sessionIdleTimeoutSeconds; + private boolean remote; + private String tcpConnectionToken; + private Boolean useLoggedInUser; + private boolean useStdio = true; + + /** + * Returns whether the client should automatically restart the server on crash. + * + * @return the auto-restart flag value (no longer has any effect) + * @deprecated This option has no effect and will be removed in a future + * release. + */ + @Deprecated + public boolean isAutoRestart() { + return autoRestart; + } + + /** + * Sets whether the client should automatically restart the CLI server if it + * crashes unexpectedly. + * + * @param autoRestart + * ignored β€” this option no longer has any effect + * @return this options instance for method chaining + * @deprecated This option has no effect and will be removed in a future + * release. + */ + @Deprecated + public CopilotClientOptions setAutoRestart(boolean autoRestart) { + this.autoRestart = autoRestart; + return this; + } + + /** + * Returns whether the client should automatically start the server. + * + * @return {@code true} to auto-start (default), {@code false} for manual start + */ + public boolean isAutoStart() { + return autoStart; + } + + /** + * Sets whether the client should automatically start the CLI server when the + * first request is made. + * + * @param autoStart + * {@code true} to auto-start, {@code false} for manual start + * @return this options instance for method chaining + */ + public CopilotClientOptions setAutoStart(boolean autoStart) { + this.autoStart = autoStart; + return this; + } + + /** + * Gets the extra CLI arguments. + *

+ * Returns a shallow copy of the internal array, or {@code null} if no arguments + * have been set. + * + * @return a copy of the extra arguments, or {@code null} + */ + public String[] getCliArgs() { + return cliArgs != null ? Arrays.copyOf(cliArgs, cliArgs.length) : null; + } + + /** + * Sets extra arguments to pass to the CLI process. + *

+ * These arguments are prepended before SDK-managed flags. A shallow copy of the + * provided array is stored. If {@code null} or empty, the existing arguments + * are cleared. + * + * @param cliArgs + * the extra arguments to pass, or {@code null}/empty to clear + * @return this options instance for method chaining + */ + public CopilotClientOptions setCliArgs(String[] cliArgs) { + if (cliArgs == null || cliArgs.length == 0) { + if (this.cliArgs != null) { + this.cliArgs = new String[0]; + } + } else { + this.cliArgs = Arrays.copyOf(cliArgs, cliArgs.length); + } + return this; + } + + /** + * Gets the path to the Copilot CLI executable. + * + * @return the CLI path, or {@code null} to use "copilot" from PATH + */ + public String getCliPath() { + return cliPath; + } + + /** + * Sets the path to the Copilot CLI executable. + * + * @param cliPath + * the path to the CLI executable + * @return this options instance for method chaining + */ + public CopilotClientOptions setCliPath(String cliPath) { + this.cliPath = Objects.requireNonNull(cliPath, "cliPath must not be null"); + return this; + } + + /** + * Gets the URL of an existing CLI server to connect to. + * + * @return the CLI server URL, or {@code null} to spawn a new process + */ + public String getCliUrl() { + return cliUrl; + } + + /** + * Sets the URL of an existing CLI server to connect to. + *

+ * When provided, the client will not spawn a CLI process but will connect to + * the specified URL instead. Format: "host:port" or "http://host:port". + *

+ * Note: This is mutually exclusive with + * {@link #setUseStdio(boolean)} and {@link #setCliPath(String)}. + * + * @param cliUrl + * the CLI server URL to connect to (must not be {@code null} or + * empty) + * @return this options instance for method chaining + * @throws IllegalArgumentException + * if {@code cliUrl} is {@code null} or empty + */ + public CopilotClientOptions setCliUrl(String cliUrl) { + this.cliUrl = Objects.requireNonNull(cliUrl, "cliUrl must not be null"); + return this; + } + + /** + * Gets the base directory for Copilot data (session state, config, etc.). + * + * @return the Copilot home directory path, or {@code null} to use the CLI + * default ({@code ~/.copilot}) + */ + public String getCopilotHome() { + return copilotHome; + } + + /** + * Sets the base directory for Copilot data (session state, config, etc.). + *

+ * Sets the {@code COPILOT_HOME} environment variable on the spawned CLI + * process. When {@code null}, the {@code COPILOT_HOME} env var is not set on + * the spawned process, so the CLI falls back to its default + * ({@code ~/.copilot}). + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param copilotHome + * the Copilot home directory path, or {@code null} to use the CLI + * default + * @return this options instance for method chaining + */ + public CopilotClientOptions setCopilotHome(String copilotHome) { + this.copilotHome = copilotHome; + return this; + } + + /** + * Gets the working directory for the CLI process. + * + * @return the working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the working directory for the CLI process. + * + * @param cwd + * the working directory path (must not be {@code null} or empty) + * @return this options instance for method chaining + * @throws IllegalArgumentException + * if {@code cwd} is {@code null} or empty + */ + public CopilotClientOptions setCwd(String cwd) { + this.cwd = Objects.requireNonNull(cwd, "cwd must not be null"); + return this; + } + + /** + * Gets the environment variables for the CLI process. + *

+ * Returns a shallow copy of the internal map, or {@code null} if no environment + * has been set. + * + * @return a copy of the environment variables map, or {@code null} + */ + public Map getEnvironment() { + return environment != null ? new HashMap<>(environment) : null; + } + + /** + * Sets environment variables to pass to the CLI process. + *

+ * When set, these environment variables replace the inherited environment. A + * shallow copy of the provided map is stored. If {@code null} or empty, the + * existing environment is cleared. + * + * @param environment + * the environment variables map, or {@code null}/empty to clear + * @return this options instance for method chaining + */ + public CopilotClientOptions setEnvironment(Map environment) { + if (environment == null || environment.isEmpty()) { + if (this.environment != null) { + this.environment.clear(); + } + } else { + this.environment = new HashMap<>(environment); + } + return this; + } + + /** + * Gets the executor used for internal asynchronous operations. + * + * @return the executor, or {@code null} to use the default + * {@code ForkJoinPool.commonPool()} + */ + public Executor getExecutor() { + return executor; + } + + /** + * Sets the executor used for internal asynchronous operations. + *

+ * When provided, the SDK uses this executor for all internal + * {@code CompletableFuture} combinators instead of the default + * {@code ForkJoinPool.commonPool()}. This allows callers to isolate SDK work + * onto a dedicated thread pool or integrate with container-managed threading. + *

+ * Passing {@code null} reverts to the default {@code ForkJoinPool.commonPool()} + * behavior. + * + * @param executor + * the executor to use, or {@code null} for the default + * @return this options instance for fluent chaining + */ + public CopilotClientOptions setExecutor(Executor executor) { + this.executor = executor; + return this; + } + + /** + * Gets the GitHub token for authentication. + * + * @return the GitHub token, or {@code null} to use other authentication methods + */ + public String getGitHubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token to use for authentication. + *

+ * When provided, the token is passed to the CLI server via environment + * variable. This takes priority over other authentication methods. + * + * @param gitHubToken + * the GitHub token (must not be {@code null} or empty) + * @return this options instance for method chaining + * @throws IllegalArgumentException + * if {@code gitHubToken} is {@code null} or empty + */ + public CopilotClientOptions setGitHubToken(String gitHubToken) { + this.gitHubToken = Objects.requireNonNull(gitHubToken, "gitHubToken must not be null"); + return this; + } + + /** + * Gets the GitHub token for authentication. + * + * @return the GitHub token, or {@code null} to use other authentication methods + * @deprecated Use {@link #getGitHubToken()} instead. + */ + @Deprecated + public String getGithubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token to use for authentication. + * + * @param githubToken + * the GitHub token + * @return this options instance for method chaining + * @deprecated Use {@link #setGitHubToken(String)} instead. + */ + @Deprecated + public CopilotClientOptions setGithubToken(String githubToken) { + this.gitHubToken = Objects.requireNonNull(githubToken, "githubToken must not be null"); + return this; + } + + /** + * Gets the log level for the CLI process. + * + * @return the log level (default: "info") + */ + public String getLogLevel() { + return logLevel; + } + + /** + * Sets the log level for the CLI process. + *

+ * Valid levels include: "error", "warn", "info", "debug", "trace". + * + * @param logLevel + * the log level (must not be {@code null} or empty) + * @return this options instance for method chaining + * @throws IllegalArgumentException + * if {@code logLevel} is {@code null} or empty + */ + public CopilotClientOptions setLogLevel(String logLevel) { + this.logLevel = Objects.requireNonNull(logLevel, "logLevel must not be null"); + return this; + } + + /** + * Gets the custom handler for listing available models. + * + * @return the handler, or {@code null} if not set + */ + public Supplier>> getOnListModels() { + return onListModels; + } + + /** + * Sets a custom handler for listing available models. + *

+ * When provided, {@code listModels()} calls this handler instead of querying + * the CLI server. Useful in BYOK (Bring Your Own Key) mode to return models + * available from your custom provider. + * + * @param onListModels + * the handler that returns the list of available models (must not be + * {@code null}) + * @return this options instance for method chaining + * @throws IllegalArgumentException + * if {@code onListModels} is {@code null} + */ + public CopilotClientOptions setOnListModels(Supplier>> onListModels) { + this.onListModels = Objects.requireNonNull(onListModels, "onListModels must not be null"); + return this; + } + + /** + * Gets the TCP port for the CLI server. + * + * @return the port number, or 0 for a random port + */ + public int getPort() { + return port; + } + + /** + * Sets the TCP port for the CLI server to listen on. + *

+ * This is only used when {@link #isUseStdio()} is {@code false}. + * + * @param port + * the port number, or 0 for a random port + * @return this options instance for method chaining + */ + public CopilotClientOptions setPort(int port) { + this.port = port; + return this; + } + + /** + * Returns whether remote session support (Mission Control integration) is + * enabled. + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + * + * @return {@code true} if remote sessions are enabled + */ + public boolean isRemote() { + return remote; + } + + /** + * Enables remote session support (Mission Control integration). + *

+ * When {@code true}, sessions in a GitHub repository working directory are + * accessible from GitHub web and mobile. + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param remote + * {@code true} to enable remote sessions + * @return this options instance for method chaining + */ + public CopilotClientOptions setRemote(boolean remote) { + this.remote = remote; + return this; + } + + /** + * Gets the OpenTelemetry configuration for the CLI server. + * + * @return the telemetry config, or {@code null} + * @since 1.2.0 + */ + public TelemetryConfig getTelemetry() { + return telemetry; + } + + /** + * Sets the OpenTelemetry configuration for the CLI server. + *

+ * When set, the CLI server is started with OpenTelemetry instrumentation + * enabled using the provided settings. + * + * @param telemetry + * the telemetry configuration + * @return this options instance for method chaining + * @since 1.2.0 + */ + public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { + this.telemetry = Objects.requireNonNull(telemetry, "telemetry must not be null"); + return this; + } + + /** + * Gets the server-wide idle timeout for sessions in seconds. + * + * @return an {@link OptionalInt} containing the session idle timeout in + * seconds, or {@link java.util.OptionalInt#empty()} if not set. Use + * {@link #clearSessionIdleTimeoutSeconds()} to revert to the default. + * @since 1.3.0 + */ + @JsonIgnore + public OptionalInt getSessionIdleTimeoutSeconds() { + return sessionIdleTimeoutSeconds == null ? OptionalInt.empty() : OptionalInt.of(sessionIdleTimeoutSeconds); + } + + /** + * Sets the server-wide idle timeout for sessions in seconds. + *

+ * Sessions without activity for this duration are automatically cleaned up. Set + * to {@code 0} to disable (sessions live indefinitely). Use + * {@link #clearSessionIdleTimeoutSeconds()} to revert to the default. + *

+ * This option is only used when the SDK spawns the CLI process; it is ignored + * when connecting to an external server via {@link #setCliUrl(String)}. + * + * @param sessionIdleTimeoutSeconds + * the idle timeout in seconds + * @return this options instance for method chaining + * @since 1.3.0 + */ + public CopilotClientOptions setSessionIdleTimeoutSeconds(int sessionIdleTimeoutSeconds) { + this.sessionIdleTimeoutSeconds = sessionIdleTimeoutSeconds; + return this; + } + + /** + * Clears the sessionIdleTimeoutSeconds setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public CopilotClientOptions clearSessionIdleTimeoutSeconds() { + this.sessionIdleTimeoutSeconds = null; + return this; + } + + /** + * Gets the connection token for the headless CLI server (TCP only). + * + * @return the connection token, or {@code null} if not set + */ + public String getTcpConnectionToken() { + return tcpConnectionToken; + } + + /** + * Sets the connection token for the headless CLI server (TCP only). + *

+ * When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is + * generated automatically so the loopback listener is safe by default. Cannot + * be combined with {@link #setUseStdio(boolean)} = {@code true}. + * + * @param tcpConnectionToken + * the connection token (must not be {@code null} or empty) + * @return this options instance for method chaining + */ + public CopilotClientOptions setTcpConnectionToken(String tcpConnectionToken) { + this.tcpConnectionToken = Objects.requireNonNull(tcpConnectionToken, "tcpConnectionToken must not be null"); + return this; + } + + /** + * Returns whether to use the logged-in user for authentication. + * + * @return an {@link Optional} containing the boolean value, or empty if not set + */ + @JsonIgnore + public Optional getUseLoggedInUser() { + return Optional.ofNullable(useLoggedInUser); + } + + /** + * Sets whether to use the logged-in user for authentication. + *

+ * When true, the CLI server will attempt to use stored OAuth tokens or gh CLI + * auth. When false, only explicit tokens (gitHubToken or environment variables) + * are used. Default: true (but defaults to false when gitHubToken is provided). + *

+ * + * @param useLoggedInUser + * {@code true} to use logged-in user auth, {@code false} otherwise + * @return this options instance for method chaining + */ + public CopilotClientOptions setUseLoggedInUser(boolean useLoggedInUser) { + this.useLoggedInUser = useLoggedInUser; + return this; + } + + /** + * Clears the useLoggedInUser setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public CopilotClientOptions clearUseLoggedInUser() { + this.useLoggedInUser = null; + return this; + } + + /** + * Returns whether to use stdio transport instead of TCP. + * + * @return {@code true} to use stdio (default), {@code false} to use TCP + */ + public boolean isUseStdio() { + return useStdio; + } + + /** + * Sets whether to use stdio transport instead of TCP. + *

+ * Stdio transport is more efficient and is the default. TCP transport can be + * useful for debugging or connecting to remote servers. + * + * @param useStdio + * {@code true} to use stdio, {@code false} to use TCP + * @return this options instance for method chaining + */ + public CopilotClientOptions setUseStdio(boolean useStdio) { + this.useStdio = useStdio; + return this; + } + + /** + * Creates a shallow clone of this {@code CopilotClientOptions} instance. + *

+ * Array properties (like {@code cliArgs}) are copied into new arrays so that + * modifications to the clone do not affect the original. The + * {@code environment} map is also copied to a new map instance. Other + * reference-type properties are shared between the original and clone. + * + * @return a clone of this options instance + */ + @Override + public CopilotClientOptions clone() { + CopilotClientOptions copy = new CopilotClientOptions(); + copy.autoRestart = this.autoRestart; + copy.autoStart = this.autoStart; + copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null; + copy.cliPath = this.cliPath; + copy.cliUrl = this.cliUrl; + copy.copilotHome = this.copilotHome; + copy.cwd = this.cwd; + copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; + copy.executor = this.executor; + copy.gitHubToken = this.gitHubToken; + copy.logLevel = this.logLevel; + copy.onListModels = this.onListModels; + copy.port = this.port; + copy.remote = this.remote; + copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds; + copy.tcpConnectionToken = this.tcpConnectionToken; + copy.telemetry = this.telemetry; + copy.useLoggedInUser = this.useLoggedInUser; + copy.useStdio = this.useStdio; + return copy; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java new file mode 100644 index 000000000..881840a73 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -0,0 +1,559 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for creating a new session. + *

+ * This is a low-level class for JSON-RPC communication. For creating sessions, + * use + * {@link com.github.copilot.sdk.CopilotClient#createSession(SessionConfig)}. + * + * @see com.github.copilot.sdk.CopilotClient#createSession(SessionConfig) + * @see SessionConfig + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class CreateSessionRequest { + + @JsonProperty("model") + private String model; + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("clientName") + private String clientName; + + @JsonProperty("reasoningEffort") + private String reasoningEffort; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("systemMessage") + private SystemMessageConfig systemMessage; + + @JsonProperty("availableTools") + private List availableTools; + + @JsonProperty("excludedTools") + private List excludedTools; + + @JsonProperty("provider") + private ProviderConfig provider; + + @JsonProperty("enableSessionTelemetry") + private Boolean enableSessionTelemetry; + + @JsonProperty("requestPermission") + private Boolean requestPermission; + + @JsonProperty("requestUserInput") + private Boolean requestUserInput; + + @JsonProperty("hooks") + private Boolean hooks; + + @JsonProperty("workingDirectory") + private String workingDirectory; + + @JsonProperty("streaming") + private Boolean streaming; + + @JsonProperty("includeSubAgentStreamingEvents") + private Boolean includeSubAgentStreamingEvents; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("envValueMode") + private String envValueMode; + + @JsonProperty("customAgents") + private List customAgents; + + @JsonProperty("defaultAgent") + private DefaultAgentConfig defaultAgent; + + @JsonProperty("agent") + private String agent; + + @JsonProperty("infiniteSessions") + private InfiniteSessionConfig infiniteSessions; + + @JsonProperty("skillDirectories") + private List skillDirectories; + + @JsonProperty("instructionDirectories") + private List instructionDirectories; + + @JsonProperty("disabledSkills") + private List disabledSkills; + + @JsonProperty("configDir") + private String configDir; + + @JsonProperty("enableConfigDiscovery") + private Boolean enableConfigDiscovery; + + @JsonProperty("commands") + private List commands; + + @JsonProperty("requestElicitation") + private Boolean requestElicitation; + + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + + @JsonProperty("modelCapabilities") + private ModelCapabilitiesOverride modelCapabilities; + + @JsonProperty("gitHubToken") + private String gitHubToken; + + @JsonProperty("remoteSession") + private String remoteSession; + + @JsonProperty("cloud") + private CloudSessionOptions cloud; + + /** Gets the model name. @return the model */ + public String getModel() { + return model; + } + + /** Sets the model name. @param model the model */ + public void setModel(String model) { + this.model = model; + } + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the client name. @return the client name */ + public String getClientName() { + return clientName; + } + + /** Sets the client name. @param clientName the client name */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /** Gets the reasoning effort. @return the reasoning effort level */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort. @param reasoningEffort the reasoning effort level + */ + public void setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } + + /** Gets the tools. @return the tool definitions */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** Sets the tools. @param tools the tool definitions */ + public void setTools(List tools) { + this.tools = tools; + } + + /** Gets the system message config. @return the config */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** Sets the system message config. @param systemMessage the config */ + public void setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + } + + /** Gets available tools. @return the tool names */ + public List getAvailableTools() { + return availableTools == null ? null : Collections.unmodifiableList(availableTools); + } + + /** Sets available tools. @param availableTools the tool names */ + public void setAvailableTools(List availableTools) { + this.availableTools = availableTools; + } + + /** Gets excluded tools. @return the tool names */ + public List getExcludedTools() { + return excludedTools == null ? null : Collections.unmodifiableList(excludedTools); + } + + /** Sets excluded tools. @param excludedTools the tool names */ + public void setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + } + + /** Gets the provider config. @return the provider */ + public ProviderConfig getProvider() { + return provider; + } + + /** Sets the provider config. @param provider the provider */ + public void setProvider(ProviderConfig provider) { + this.provider = provider; + } + + /** Gets enable session telemetry flag. @return the flag */ + public Boolean getEnableSessionTelemetry() { + return enableSessionTelemetry; + } + + /** + * Sets enable session telemetry flag. @param enableSessionTelemetry the flag + */ + public void setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + */ + public void clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + } + + /** Gets request permission flag. @return the flag */ + public Boolean getRequestPermission() { + return requestPermission; + } + + /** Sets request permission flag. @param requestPermission the flag */ + public void setRequestPermission(boolean requestPermission) { + this.requestPermission = requestPermission; + } + + /** + * Clears the requestPermission setting, reverting to the default behavior. + */ + public void clearRequestPermission() { + this.requestPermission = null; + } + + /** Gets request user input flag. @return the flag */ + public Boolean getRequestUserInput() { + return requestUserInput; + } + + /** Sets request user input flag. @param requestUserInput the flag */ + public void setRequestUserInput(boolean requestUserInput) { + this.requestUserInput = requestUserInput; + } + + /** + * Clears the requestUserInput setting, reverting to the default behavior. + */ + public void clearRequestUserInput() { + this.requestUserInput = null; + } + + /** Gets hooks flag. @return the flag */ + public Boolean getHooks() { + return hooks; + } + + /** Sets hooks flag. @param hooks the flag */ + public void setHooks(boolean hooks) { + this.hooks = hooks; + } + + /** + * Clears the hooks setting, reverting to the default behavior. + */ + public void clearHooks() { + this.hooks = null; + } + + /** Gets working directory. @return the working directory */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** Sets working directory. @param workingDirectory the working directory */ + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** Gets streaming flag. @return the flag */ + public Boolean getStreaming() { + return streaming; + } + + /** Sets streaming flag. @param streaming the flag */ + public void setStreaming(boolean streaming) { + this.streaming = streaming; + } + + /** + * Clears the streaming setting, reverting to the default behavior. + */ + public void clearStreaming() { + this.streaming = null; + } + + /** Gets MCP servers. @return the servers map */ + public Map getMcpServers() { + return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); + } + + /** Sets MCP servers. @param mcpServers the servers map */ + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } + + /** Gets MCP environment variable value mode. @return the mode */ + public String getEnvValueMode() { + return envValueMode; + } + + /** Sets MCP environment variable value mode. @param envValueMode the mode */ + public void setEnvValueMode(String envValueMode) { + this.envValueMode = envValueMode; + } + + /** Gets custom agents. @return the agents */ + public List getCustomAgents() { + return customAgents == null ? null : Collections.unmodifiableList(customAgents); + } + + /** Sets custom agents. @param customAgents the agents */ + public void setCustomAgents(List customAgents) { + this.customAgents = customAgents; + } + + /** Gets the default agent config. @return the default agent config */ + public DefaultAgentConfig getDefaultAgent() { + return defaultAgent; + } + + /** + * Sets the default agent config. @param defaultAgent the default agent config + */ + public void setDefaultAgent(DefaultAgentConfig defaultAgent) { + this.defaultAgent = defaultAgent; + } + + /** Gets the pre-selected agent name. @return the agent name */ + public String getAgent() { + return agent; + } + + /** Sets the pre-selected agent name. @param agent the agent name */ + public void setAgent(String agent) { + this.agent = agent; + } + + /** Gets infinite sessions config. @return the config */ + public InfiniteSessionConfig getInfiniteSessions() { + return infiniteSessions; + } + + /** Sets infinite sessions config. @param infiniteSessions the config */ + public void setInfiniteSessions(InfiniteSessionConfig infiniteSessions) { + this.infiniteSessions = infiniteSessions; + } + + /** Gets skill directories. @return the skill directories */ + public List getSkillDirectories() { + return skillDirectories == null ? null : Collections.unmodifiableList(skillDirectories); + } + + /** Sets skill directories. @param skillDirectories the directories */ + public void setSkillDirectories(List skillDirectories) { + this.skillDirectories = skillDirectories; + } + + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + + /** Gets disabled skills. @return the disabled skill names */ + public List getDisabledSkills() { + return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); + } + + /** Sets disabled skills. @param disabledSkills the skill names to disable */ + public void setDisabledSkills(List disabledSkills) { + this.disabledSkills = disabledSkills; + } + + /** Gets config directory. @return the config directory path */ + public String getConfigDir() { + return configDir; + } + + /** Sets config directory. @param configDir the config directory path */ + public void setConfigDir(String configDir) { + this.configDir = configDir; + } + + /** Gets enable config discovery flag. @return the flag */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ + public void setEnableConfigDiscovery(boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + } + + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + */ + public void clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + } + + /** Gets include sub-agent streaming events flag. @return the flag */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets include sub-agent streaming events flag. @param + * includeSubAgentStreamingEvents the flag + */ + public void setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + } + + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + */ + public void clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + } + + /** Gets the commands wire definitions. @return the commands */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** Sets the commands wire definitions. @param commands the commands */ + public void setCommands(List commands) { + this.commands = commands; + } + + /** Gets the requestElicitation flag. @return the flag */ + public Boolean getRequestElicitation() { + return requestElicitation; + } + + /** Sets the requestElicitation flag. @param requestElicitation the flag */ + public void setRequestElicitation(boolean requestElicitation) { + this.requestElicitation = requestElicitation; + } + + /** + * Clears the requestElicitation setting, reverting to the default behavior. + */ + public void clearRequestElicitation() { + this.requestElicitation = null; + } + + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + + /** Gets the model capabilities override. @return the override */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets the model capabilities override. @param modelCapabilities the override + */ + public void setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + } + + /** Gets the GitHub token for per-session authentication. @return the token */ + public String getGitHubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token for per-session authentication. @param gitHubToken the + * token + */ + public void setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + } + + /** Gets the remote session mode. @return the remote session mode */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the remote session mode. @param remoteSession the remote session mode + */ + public void setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + } + + /** Gets the cloud session options. @return the cloud session options */ + public CloudSessionOptions getCloud() { + return cloud; + } + + /** Sets the cloud session options. @param cloud the cloud session options */ + public void setCloud(CloudSessionOptions cloud) { + this.cloud = cloud; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java new file mode 100644 index 000000000..b47af050b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CreateSessionResponse.java @@ -0,0 +1,22 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from creating a session. + * + * @param sessionId + * the session ID assigned by the server + * @param workspacePath + * the workspace path, or {@code null} if infinite sessions are + * disabled + * @param capabilities + * the capabilities reported by the host, or {@code null} + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CreateSessionResponse(@JsonProperty("sessionId") String sessionId, + @JsonProperty("workspacePath") String workspacePath, + @JsonProperty("capabilities") SessionCapabilities capabilities) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java new file mode 100644 index 000000000..cb2dbf452 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/CustomAgentConfig.java @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; + +/** + * Configuration for a custom agent in a Copilot session. + *

+ * Custom agents extend the capabilities of the base Copilot assistant with + * specialized behavior, tools, and prompts. Each agent can be referenced in + * messages using the {@code @agent-name} mention syntax. + * + *

Example Usage

+ * + *
{@code
+ * var agent = new CustomAgentConfig().setName("code-reviewer").setDisplayName("Code Reviewer")
+ * 		.setDescription("Reviews code for best practices").setPrompt("You are a code review expert...")
+ * 		.setTools(List.of("read_file", "search_code"));
+ *
+ * var config = new SessionConfig().setCustomAgents(List.of(agent));
+ * }
+ * + * @see SessionConfig#setCustomAgents(List) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CustomAgentConfig { + + @JsonProperty("name") + private String name; + + @JsonProperty("displayName") + private String displayName; + + @JsonProperty("description") + private String description; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("prompt") + private String prompt; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("infer") + private Boolean infer; + + @JsonProperty("skills") + private List skills; + + @JsonProperty("model") + private String model; + + /** + * Gets the unique identifier name for this agent. + * + * @return the agent name used for {@code @mentions} + */ + public String getName() { + return name; + } + + /** + * Sets the unique identifier name for this agent. + *

+ * This name is used to mention the agent in messages (e.g., + * {@code @code-reviewer}). + * + * @param name + * the agent identifier (alphanumeric and hyphens) + * @return this config for method chaining + */ + public CustomAgentConfig setName(String name) { + this.name = name; + return this; + } + + /** + * Gets the human-readable display name. + * + * @return the display name shown to users + */ + public String getDisplayName() { + return displayName; + } + + /** + * Sets the human-readable display name. + * + * @param displayName + * the friendly name for the agent + * @return this config for method chaining + */ + public CustomAgentConfig setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Gets the agent description. + * + * @return the description of what this agent does + */ + public String getDescription() { + return description; + } + + /** + * Sets a description of the agent's capabilities. + *

+ * This helps users understand when to use this agent. + * + * @param description + * the agent description + * @return this config for method chaining + */ + public CustomAgentConfig setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the list of tool names available to this agent. + * + * @return the list of tool identifiers + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets the tools available to this agent. + *

+ * These can reference both built-in tools and custom tools registered in the + * session. + * + * @param tools + * the list of tool names + * @return this config for method chaining + */ + public CustomAgentConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the system prompt for this agent. + * + * @return the agent's system prompt + */ + public String getPrompt() { + return prompt; + } + + /** + * Sets the system prompt that defines this agent's behavior. + *

+ * This prompt is used to customize the agent's responses and capabilities. + * + * @param prompt + * the system prompt + * @return this config for method chaining + */ + public CustomAgentConfig setPrompt(String prompt) { + this.prompt = prompt; + return this; + } + + /** + * Gets the MCP server configurations for this agent. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); + } + + /** + * Sets MCP (Model Context Protocol) servers available to this agent. + * + * @param mcpServers + * the MCP server configurations + * @return this config for method chaining + */ + public CustomAgentConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets whether inference mode is enabled. + * + * @return an {@link java.util.Optional} containing the infer flag, or + * {@link java.util.Optional#empty()} if not set + */ + @JsonIgnore + public Optional getInfer() { + return Optional.ofNullable(infer); + } + + /** + * Sets whether to enable inference mode for this agent. + * + * @param infer + * {@code true} to enable inference mode + * @return this config for method chaining + */ + public CustomAgentConfig setInfer(boolean infer) { + this.infer = infer; + return this; + } + + /** + * Clears the infer setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public CustomAgentConfig clearInfer() { + this.infer = null; + return this; + } + + /** + * Gets the list of skill names to preload into this agent's context. + * + * @return the list of skill names, or {@code null} if not set + */ + public List getSkills() { + return skills == null ? null : Collections.unmodifiableList(skills); + } + + /** + * Sets the list of skill names to preload into this agent's context. + *

+ * When set, the full content of each listed skill is eagerly injected into the + * agent's context at startup. Skills are resolved by name from the session's + * configured skill directories + * ({@link SessionConfig#setSkillDirectories(List)}). When omitted, no skills + * are injected (opt-in model). + * + * @param skills + * the list of skill names to preload + * @return this config for method chaining + */ + public CustomAgentConfig setSkills(List skills) { + this.skills = skills; + return this; + } + + /** + * Gets the model identifier for this agent. + * + * @return the model identifier, or {@code null} if not set + */ + public String getModel() { + return model; + } + + /** + * Sets the model identifier for this agent. + *

+ * When set, the runtime will attempt to use this model for the agent, falling + * back to the parent session model if unavailable. + * + * @param model + * the model identifier (e.g., "claude-haiku-4.5") + * @return this config for method chaining + */ + public CustomAgentConfig setModel(String model) { + this.model = model; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/DefaultAgentConfig.java b/java/src/main/java/com/github/copilot/sdk/json/DefaultAgentConfig.java new file mode 100644 index 000000000..88f39ecff --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/DefaultAgentConfig.java @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for the default agent (the built-in agent that handles turns + * when no custom agent is selected). + *

+ * Use {@link #setExcludedTools(List)} to hide specific tools from the default + * agent while keeping them available to custom sub-agents. + * + *

Example Usage

+ * + *
{@code
+ * var config = new SessionConfig().setTools(List.of(secretTool))
+ * 		.setDefaultAgent(new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")));
+ * }
+ * + * @see SessionConfig#setDefaultAgent(DefaultAgentConfig) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DefaultAgentConfig { + + @JsonProperty("excludedTools") + private List excludedTools; + + /** + * Gets the list of tool names excluded from the default agent. + * + * @return the list of excluded tool names, or {@code null} if not set + */ + public List getExcludedTools() { + return excludedTools == null ? null : Collections.unmodifiableList(excludedTools); + } + + /** + * Sets the list of tool names to exclude from the default agent. + *

+ * These tools remain available to custom sub-agents that reference them in + * their {@link CustomAgentConfig#setTools(List)} list. + * + * @param excludedTools + * the list of tool names to exclude from the default agent + * @return this config for method chaining + */ + public DefaultAgentConfig setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java new file mode 100644 index 000000000..1f53dfac3 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/DeleteSessionResponse.java @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from deleting a session. + *

+ * This is a low-level class for JSON-RPC communication containing the result of + * a session deletion operation. + * + * @see com.github.copilot.sdk.CopilotClient#deleteSession(String) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record DeleteSessionResponse( + /** Whether the deletion was successful. */ + @JsonProperty("success") boolean success, + /** The error message, or {@code null} if successful. */ + @JsonProperty("error") String error) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java new file mode 100644 index 000000000..87687b194 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationContext.java @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an elicitation request received from the server or MCP tools. + * + * @since 1.0.0 + */ +public class ElicitationContext { + + private String sessionId; + private String message; + private ElicitationSchema requestedSchema; + private String mode; + private String elicitationSource; + private String url; + + /** + * Gets the session ID that triggered the elicitation request. @return the + * session ID + */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public ElicitationContext setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the message describing what information is needed from the user. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** Sets the message. @param message the message @return this */ + public ElicitationContext setMessage(String message) { + this.message = message; + return this; + } + + /** + * Gets the JSON Schema describing the form fields to present (form mode only). + * + * @return the schema, or {@code null} + */ + public ElicitationSchema getRequestedSchema() { + return requestedSchema; + } + + /** Sets the schema. @param requestedSchema the schema @return this */ + public ElicitationContext setRequestedSchema(ElicitationSchema requestedSchema) { + this.requestedSchema = requestedSchema; + return this; + } + + /** + * Gets the elicitation mode: {@code "form"} for structured input, {@code "url"} + * for browser redirect. + * + * @return the mode, or {@code null} (defaults to {@code "form"}) + */ + public String getMode() { + return mode; + } + + /** Sets the mode. @param mode the mode @return this */ + public ElicitationContext setMode(String mode) { + this.mode = mode; + return this; + } + + /** + * Gets the source that initiated the request (e.g., MCP server name). + * + * @return the elicitation source, or {@code null} + */ + public String getElicitationSource() { + return elicitationSource; + } + + /** + * Sets the elicitation source. @param elicitationSource the source @return this + */ + public ElicitationContext setElicitationSource(String elicitationSource) { + this.elicitationSource = elicitationSource; + return this; + } + + /** + * Gets the URL to open in the user's browser (url mode only). + * + * @return the URL, or {@code null} + */ + public String getUrl() { + return url; + } + + /** Sets the URL. @param url the URL @return this */ + public ElicitationContext setUrl(String url) { + this.url = url; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java new file mode 100644 index 000000000..d0a0d0616 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationHandler.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling elicitation requests from the server. + *

+ * Register an elicitation handler via + * {@link SessionConfig#setOnElicitationRequest(ElicitationHandler)} or + * {@link ResumeSessionConfig#setOnElicitationRequest(ElicitationHandler)}. When + * provided, the server routes elicitation requests to this handler and reports + * elicitation as a supported capability. + * + *

Example Usage

+ * + *
{@code
+ * ElicitationHandler handler = context -> {
+ * 	// Show the form to the user and collect responses
+ * 	Map formValues = showForm(context.getMessage(), context.getRequestedSchema());
+ * 	return CompletableFuture.completedFuture(
+ * 			new ElicitationResult().setAction(ElicitationResultAction.ACCEPT).setContent(formValues));
+ * };
+ * }
+ * + * @see ElicitationContext + * @see ElicitationResult + * @since 1.0.0 + */ +@FunctionalInterface +public interface ElicitationHandler { + + /** + * Handles an elicitation request from the server. + * + * @param context + * the elicitation context containing the message, schema, and mode + * @return a future that resolves with the elicitation result + */ + CompletableFuture handle(ElicitationContext context); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java new file mode 100644 index 000000000..8bd81022e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationParams.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Parameters for an elicitation request sent from the SDK to the host. + * + * @since 1.0.0 + */ +public class ElicitationParams { + + private String message; + private ElicitationSchema requestedSchema; + + /** + * Gets the message describing what information is needed from the user. + * + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * Sets the message describing what information is needed from the user. + * + * @param message + * the message + * @return this instance for method chaining + */ + public ElicitationParams setMessage(String message) { + this.message = message; + return this; + } + + /** + * Gets the JSON Schema describing the form fields to present. + * + * @return the requested schema + */ + public ElicitationSchema getRequestedSchema() { + return requestedSchema; + } + + /** + * Sets the JSON Schema describing the form fields to present. + * + * @param requestedSchema + * the schema + * @return this instance for method chaining + */ + public ElicitationParams setRequestedSchema(ElicitationSchema requestedSchema) { + this.requestedSchema = requestedSchema; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java new file mode 100644 index 000000000..3ba30b83d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationResult.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +/** + * Result returned from an elicitation dialog. + * + * @since 1.0.0 + */ +public class ElicitationResult { + + private ElicitationResultAction action; + private Map content; + + /** + * Gets the user action taken on the elicitation dialog. + *

+ * {@link ElicitationResultAction#ACCEPT} means the user submitted the form, + * {@link ElicitationResultAction#DECLINE} means the user rejected the request, + * and {@link ElicitationResultAction#CANCEL} means the user dismissed the + * dialog. + * + * @return the user action + */ + public ElicitationResultAction getAction() { + return action; + } + + /** + * Sets the user action taken on the elicitation dialog. + * + * @param action + * the user action + * @return this instance for method chaining + */ + public ElicitationResult setAction(ElicitationResultAction action) { + this.action = action; + return this; + } + + /** + * Gets the form values submitted by the user. + *

+ * Only present when {@link #getAction()} is + * {@link ElicitationResultAction#ACCEPT}. + * + * @return the submitted form values, or {@code null} if the user did not accept + */ + public Map getContent() { + return content; + } + + /** + * Sets the form values submitted by the user. + * + * @param content + * the submitted form values + * @return this instance for method chaining + */ + public ElicitationResult setContent(Map content) { + this.content = content; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java new file mode 100644 index 000000000..fd280cdeb --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationResultAction.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Action value for an {@link ElicitationResult}. + * + * @since 1.0.0 + */ +public enum ElicitationResultAction { + + /** The user submitted the form (accepted). */ + ACCEPT("accept"), + + /** The user explicitly rejected the request. */ + DECLINE("decline"), + + /** The user dismissed the dialog without responding. */ + CANCEL("cancel"); + + private final String value; + + ElicitationResultAction(String value) { + this.value = value; + } + + /** Returns the wire-format string value. @return the string value */ + public String getValue() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java b/java/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java new file mode 100644 index 000000000..c3d548775 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ElicitationSchema.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON Schema describing the form fields to present for an elicitation dialog. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ElicitationSchema { + + @JsonProperty("type") + private String type = "object"; + + @JsonProperty("properties") + private Map properties; + + @JsonProperty("required") + private List required; + + /** + * Gets the schema type indicator (always {@code "object"}). + * + * @return the type + */ + public String getType() { + return type; + } + + /** + * Sets the schema type indicator. + * + * @param type + * the type (typically {@code "object"}) + * @return this instance for method chaining + */ + public ElicitationSchema setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the form field definitions, keyed by field name. + * + * @return the properties map + */ + public Map getProperties() { + return properties; + } + + /** + * Sets the form field definitions, keyed by field name. + * + * @param properties + * the properties map + * @return this instance for method chaining + */ + public ElicitationSchema setProperties(Map properties) { + this.properties = properties; + return this; + } + + /** + * Gets the list of required field names. + * + * @return the required field names, or {@code null} + */ + public List getRequired() { + return required; + } + + /** + * Sets the list of required field names. + * + * @param required + * the required field names + * @return this instance for method chaining + */ + public ElicitationSchema setRequired(List required) { + this.required = required; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java new file mode 100644 index 000000000..13ecbe075 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for exit-plan-mode requests from the agent. + *

+ * Register an exit-plan-mode handler via + * {@link SessionConfig#setOnExitPlanMode(ExitPlanModeHandler)} or + * {@link ResumeSessionConfig#setOnExitPlanMode(ExitPlanModeHandler)}. When + * provided, the server routes {@code exitPlanMode.request} callbacks to this + * handler. + * + *

Example Usage

+ * + *
{@code
+ * ExitPlanModeHandler handler = (request, invocation) -> {
+ * 	// Review the plan and decide whether to approve
+ * 	return CompletableFuture
+ * 			.completedFuture(new ExitPlanModeResult().setApproved(true).setSelectedAction("interactive"));
+ * };
+ *
+ * var session = client.createSession(new SessionConfig().setOnExitPlanMode(handler)).get();
+ * }
+ * + * @see ExitPlanModeRequest + * @see ExitPlanModeResult + * @since 1.0.8 + */ +@FunctionalInterface +public interface ExitPlanModeHandler { + + /** + * Handles an exit-plan-mode request from the agent. + * + * @param request + * the exit-plan-mode request containing the summary, plan content, + * and available actions + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(ExitPlanModeRequest request, ExitPlanModeInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java new file mode 100644 index 000000000..6fd023126 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an exit-plan-mode request invocation. + * + * @since 1.0.8 + */ +public class ExitPlanModeInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public ExitPlanModeInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java new file mode 100644 index 000000000..be09350ef --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request to exit plan mode and continue with a selected action. + *

+ * This is sent by the server when the agent wants to exit plan mode and + * requests user confirmation. + * + * @since 1.0.8 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExitPlanModeRequest { + + @JsonProperty("summary") + private String summary = ""; + + @JsonProperty("planContent") + private String planContent; + + @JsonProperty("actions") + private List actions; + + @JsonProperty("recommendedAction") + private String recommendedAction = "autopilot"; + + /** + * Gets the summary of the plan or proposed next step. + * + * @return the summary + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary of the plan or proposed next step. + * + * @param summary + * the summary + * @return this instance for method chaining + */ + public ExitPlanModeRequest setSummary(String summary) { + this.summary = summary; + return this; + } + + /** + * Gets the full plan content, when available. + * + * @return the plan content, or {@code null} if not available + */ + public String getPlanContent() { + return planContent; + } + + /** + * Sets the full plan content. + * + * @param planContent + * the plan content + * @return this instance for method chaining + */ + public ExitPlanModeRequest setPlanContent(String planContent) { + this.planContent = planContent; + return this; + } + + /** + * Gets the available actions the user can select. + * + * @return the list of actions, or {@code null} if not specified + */ + public List getActions() { + return actions == null ? null : Collections.unmodifiableList(actions); + } + + /** + * Sets the available actions the user can select. + * + * @param actions + * the list of actions + * @return this instance for method chaining + */ + public ExitPlanModeRequest setActions(List actions) { + this.actions = actions; + return this; + } + + /** + * Gets the action recommended by the runtime. + * + * @return the recommended action + */ + public String getRecommendedAction() { + return recommendedAction; + } + + /** + * Sets the action recommended by the runtime. + * + * @param recommendedAction + * the recommended action + * @return this instance for method chaining + */ + public ExitPlanModeRequest setRecommendedAction(String recommendedAction) { + this.recommendedAction = recommendedAction; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java new file mode 100644 index 000000000..876e750b4 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response to an exit-plan-mode request. + * + * @since 1.0.8 + */ +public class ExitPlanModeResult { + + @JsonProperty("approved") + private boolean approved = true; + + @JsonProperty("selectedAction") + private String selectedAction; + + @JsonProperty("feedback") + private String feedback; + + /** + * Returns whether the user approved exiting plan mode. + * + * @return {@code true} if approved + */ + public boolean isApproved() { + return approved; + } + + /** + * Sets whether the user approved exiting plan mode. + * + * @param approved + * {@code true} if approved + * @return this instance for method chaining + */ + public ExitPlanModeResult setApproved(boolean approved) { + this.approved = approved; + return this; + } + + /** + * Gets the selected action, if the user chose one. + * + * @return the selected action, or {@code null} + */ + public String getSelectedAction() { + return selectedAction; + } + + /** + * Sets the selected action. + * + * @param selectedAction + * the selected action + * @return this instance for method chaining + */ + public ExitPlanModeResult setSelectedAction(String selectedAction) { + this.selectedAction = selectedAction; + return this; + } + + /** + * Gets optional feedback provided by the user. + * + * @return the feedback, or {@code null} + */ + public String getFeedback() { + return feedback; + } + + /** + * Sets feedback from the user. + * + * @param feedback + * the feedback text + * @return this instance for method chaining + */ + public ExitPlanModeResult setFeedback(String feedback) { + this.feedback = feedback; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java new file mode 100644 index 000000000..4cf5d80c6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetAuthStatusResponse.java @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from the auth.getStatus RPC call. + *

+ * Contains information about the current authentication status. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetAuthStatusResponse { + + /** + * Whether the user is authenticated. + */ + @JsonProperty("isAuthenticated") + private boolean isAuthenticated; + + /** + * Authentication type (user, env, gh-cli, hmac, api-key, token). + */ + @JsonProperty("authType") + private String authType; + + /** + * GitHub host URL. + */ + @JsonProperty("host") + private String host; + + /** + * User login name. + */ + @JsonProperty("login") + private String login; + + /** + * Human-readable status message. + */ + @JsonProperty("statusMessage") + private String statusMessage; + + public boolean isAuthenticated() { + return isAuthenticated; + } + + public GetAuthStatusResponse setAuthenticated(boolean authenticated) { + isAuthenticated = authenticated; + return this; + } + + public String getAuthType() { + return authType; + } + + public GetAuthStatusResponse setAuthType(String authType) { + this.authType = authType; + return this; + } + + public String getHost() { + return host; + } + + public GetAuthStatusResponse setHost(String host) { + this.host = host; + return this; + } + + public String getLogin() { + return login; + } + + public GetAuthStatusResponse setLogin(String login) { + this.login = login; + return this; + } + + public String getStatusMessage() { + return statusMessage; + } + + public GetAuthStatusResponse setStatusMessage(String statusMessage) { + this.statusMessage = statusMessage; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java new file mode 100644 index 000000000..96962c690 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetForegroundSessionResponse.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from session.getForeground RPC call. + *

+ * This is only available when connecting to a server running in TUI+server mode + * (--ui-server). + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record GetForegroundSessionResponse( + /** The session ID currently displayed in the TUI, or null if none. */ + @JsonProperty("sessionId") String sessionId, + /** The workspace path of the foreground session, or null. */ + @JsonProperty("workspacePath") String workspacePath) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java new file mode 100644 index 000000000..52042f57c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetLastSessionIdResponse.java @@ -0,0 +1,13 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from getting the last session ID. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetLastSessionIdResponse(@JsonProperty("sessionId") String sessionId) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java new file mode 100644 index 000000000..1a3ed6aae --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetMessagesResponse.java @@ -0,0 +1,16 @@ +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Internal response object from getting session messages. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetMessagesResponse(@JsonProperty("events") List events) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java new file mode 100644 index 000000000..8f13912b9 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetModelsResponse.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Response from the models.list RPC call. + *

+ * Contains a list of available models with their metadata. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetModelsResponse { + + @JsonProperty("models") + private List models; + + public List getModels() { + return models; + } + + public GetModelsResponse setModels(List models) { + this.models = models; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java new file mode 100644 index 000000000..eeceb4177 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetSessionMetadataResponse.java @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from getting session metadata by ID. + * + * @param session + * the session metadata, or {@code null} if not found + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record GetSessionMetadataResponse(@JsonProperty("session") SessionMetadata session) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java b/java/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java new file mode 100644 index 000000000..a77f378ab --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/GetStatusResponse.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from the status.get RPC call. + *

+ * Contains information about the CLI version and protocol version. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetStatusResponse { + + /** + * Package version (e.g., "1.0.0"). + */ + @JsonProperty("version") + private String version; + + /** + * Protocol version for SDK compatibility. + */ + @JsonProperty("protocolVersion") + private int protocolVersion; + + public String getVersion() { + return version; + } + + public GetStatusResponse setVersion(String version) { + this.version = version; + return this; + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public GetStatusResponse setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/HookInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/HookInvocation.java new file mode 100644 index 000000000..39ab50686 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/HookInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for a hook invocation. + * + * @since 1.0.6 + */ +public class HookInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public HookInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java b/java/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java new file mode 100644 index 000000000..561796ede --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/InfiniteSessionConfig.java @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalDouble; + +/** + * Configuration for infinite sessions with automatic context compaction and + * workspace persistence. + *

+ * When enabled, sessions automatically manage context window limits through + * background compaction and persist state to a workspace directory. + * + *

Example Usage

+ * + *
{@code
+ * var infiniteConfig = new InfiniteSessionConfig().setEnabled(true).setBackgroundCompactionThreshold(0.80)
+ * 		.setBufferExhaustionThreshold(0.95);
+ *
+ * var config = new SessionConfig().setInfiniteSessions(infiniteConfig);
+ *
+ * var session = client.createSession(config).get();
+ * }
+ * + * @see SessionConfig#setInfiniteSessions(InfiniteSessionConfig) + * @since 1.0.2 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class InfiniteSessionConfig { + + @JsonProperty("enabled") + private Boolean enabled; + + @JsonProperty("backgroundCompactionThreshold") + private Double backgroundCompactionThreshold; + + @JsonProperty("bufferExhaustionThreshold") + private Double bufferExhaustionThreshold; + + /** + * Gets whether infinite sessions are enabled. + * + * @return an {@link Optional} containing the boolean value, or empty to use + * default (true) + */ + @JsonIgnore + public Optional getEnabled() { + return Optional.ofNullable(enabled); + } + + /** + * Sets whether infinite sessions are enabled. + *

+ * Default: true + * + * @param enabled + * {@code true} to enable infinite sessions + * @return this config instance for method chaining + */ + public InfiniteSessionConfig setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + /** + * Clears the enabled setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearEnabled() { + this.enabled = null; + return this; + } + + /** + * Gets the background compaction threshold. + * + * @return an {@link OptionalDouble} containing the threshold (0.0-1.0), or + * empty to use default + */ + @JsonIgnore + public OptionalDouble getBackgroundCompactionThreshold() { + return backgroundCompactionThreshold == null + ? OptionalDouble.empty() + : OptionalDouble.of(backgroundCompactionThreshold); + } + + /** + * Sets the context utilization threshold at which background compaction starts. + *

+ * Compaction runs asynchronously, allowing the session to continue processing. + * Default: 0.80 + * + * @param backgroundCompactionThreshold + * the threshold (0.0-1.0) + * @return this config instance for method chaining + */ + public InfiniteSessionConfig setBackgroundCompactionThreshold(double backgroundCompactionThreshold) { + this.backgroundCompactionThreshold = backgroundCompactionThreshold; + return this; + } + + /** + * Clears the backgroundCompactionThreshold setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearBackgroundCompactionThreshold() { + this.backgroundCompactionThreshold = null; + return this; + } + + /** + * Gets the buffer exhaustion threshold. + * + * @return an {@link OptionalDouble} containing the threshold (0.0-1.0), or + * empty to use default + */ + @JsonIgnore + public OptionalDouble getBufferExhaustionThreshold() { + return bufferExhaustionThreshold == null + ? OptionalDouble.empty() + : OptionalDouble.of(bufferExhaustionThreshold); + } + + /** + * Sets the context utilization threshold at which the session blocks until + * compaction completes. + *

+ * This prevents context overflow when compaction hasn't finished in time. + * Default: 0.95 + * + * @param bufferExhaustionThreshold + * the threshold (0.0-1.0) + * @return this config instance for method chaining + */ + public InfiniteSessionConfig setBufferExhaustionThreshold(double bufferExhaustionThreshold) { + this.bufferExhaustionThreshold = bufferExhaustionThreshold; + return this; + } + + /** + * Clears the bufferExhaustionThreshold setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public InfiniteSessionConfig clearBufferExhaustionThreshold() { + this.bufferExhaustionThreshold = null; + return this; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/InputOptions.java b/java/src/main/java/com/github/copilot/sdk/json/InputOptions.java new file mode 100644 index 000000000..ca1cc5d38 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/InputOptions.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.OptionalInt; + +/** + * Options for the {@link SessionUiApi#input(String, InputOptions)} convenience + * method. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class InputOptions { + + private String title; + private String description; + @JsonProperty("minLength") + private Integer minLength; + @JsonProperty("maxLength") + private Integer maxLength; + private String format; + private String defaultValue; + + /** Gets the title label for the input field. @return the title */ + public String getTitle() { + return title; + } + + /** + * Sets the title label for the input field. @param title the title @return this + */ + public InputOptions setTitle(String title) { + this.title = title; + return this; + } + + /** Gets the descriptive text shown below the field. @return the description */ + public String getDescription() { + return description; + } + + /** + * Sets the descriptive text shown below the field. @param description the + * description @return this + */ + public InputOptions setDescription(String description) { + this.description = description; + return this; + } + + /** + * Gets the minimum character length. + * + * @return an {@link java.util.OptionalInt} containing the min length, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMinLength() { + return minLength == null ? OptionalInt.empty() : OptionalInt.of(minLength); + } + + /** + * Sets the minimum character length. @param minLength the min length @return + * this + */ + public InputOptions setMinLength(int minLength) { + this.minLength = minLength; + return this; + } + + /** + * Clears the minLength setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InputOptions clearMinLength() { + this.minLength = null; + return this; + } + + /** + * Gets the maximum character length. + * + * @return an {@link java.util.OptionalInt} containing the max length, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxLength() { + return maxLength == null ? OptionalInt.empty() : OptionalInt.of(maxLength); + } + + /** + * Sets the maximum character length. @param maxLength the max length @return + * this + */ + public InputOptions setMaxLength(int maxLength) { + this.maxLength = maxLength; + return this; + } + + /** + * Clears the maxLength setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public InputOptions clearMaxLength() { + this.maxLength = null; + return this; + } + + /** + * Gets the semantic format hint (e.g., {@code "email"}, {@code "uri"}, + * {@code "date"}, {@code "date-time"}). + * + * @return the format hint + */ + public String getFormat() { + return format; + } + + /** Sets the semantic format hint. @param format the format @return this */ + public InputOptions setFormat(String format) { + this.format = format; + return this; + } + + /** + * Gets the default value pre-populated in the field. @return the default value + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Sets the default value pre-populated in the field. @param defaultValue the + * default value @return this + */ + public InputOptions setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java new file mode 100644 index 000000000..e7f021f43 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcError.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 error structure. + *

+ * This is an internal class representing an error in a JSON-RPC response. It + * contains an error code, message, and optional additional data. + * + *

Standard Error Codes

+ *
    + *
  • -32700: Parse error
  • + *
  • -32600: Invalid Request
  • + *
  • -32601: Method not found
  • + *
  • -32602: Invalid params
  • + *
  • -32603: Internal error
  • + *
+ * + * @see JsonRpcResponse + * @see JSON-RPC + * Error Object + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcError { + + @JsonProperty("code") + private int code; + + @JsonProperty("message") + private String message; + + @JsonProperty("data") + private Object data; + + /** + * Gets the error code. + * + * @return the integer error code + */ + public int getCode() { + return code; + } + + /** + * Sets the error code. + * + * @param code + * the integer error code + */ + public void setCode(int code) { + this.code = code; + } + + /** + * Gets the error message. + * + * @return the human-readable error message + */ + public String getMessage() { + return message; + } + + /** + * Sets the error message. + * + * @param message + * the error message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the additional error data. + * + * @return the additional data, or {@code null} if none + */ + public Object getData() { + return data; + } + + /** + * Sets the additional error data. + * + * @param data + * the additional data + */ + public void setData(Object data) { + this.data = data; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java new file mode 100644 index 000000000..1921370e2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcRequest.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 request structure. + *

+ * This is an internal class representing the wire format of a JSON-RPC request. + * It follows the JSON-RPC 2.0 specification. + * + * @see JsonRpcResponse + * @see JSON-RPC 2.0 + * Specification + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcRequest { + + @JsonProperty("jsonrpc") + private String jsonrpc; + + @JsonProperty("id") + private Long id; + + @JsonProperty("method") + private String method; + + @JsonProperty("params") + private Object params; + + /** + * Gets the JSON-RPC version. + * + * @return the version string (should be "2.0") + */ + public String getJsonrpc() { + return jsonrpc; + } + + /** + * Sets the JSON-RPC version. + * + * @param jsonrpc + * the version string + */ + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + /** + * Gets the request ID. + * + * @return the request identifier + */ + public Long getId() { + return id; + } + + /** + * Sets the request ID. + * + * @param id + * the request identifier + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Gets the method name. + * + * @return the RPC method to invoke + */ + public String getMethod() { + return method; + } + + /** + * Sets the method name. + * + * @param method + * the RPC method to invoke + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Gets the method parameters. + * + * @return the parameters object + */ + public Object getParams() { + return params; + } + + /** + * Sets the method parameters. + * + * @param params + * the parameters object + */ + public void setParams(Object params) { + this.params = params; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java new file mode 100644 index 000000000..1a78ed017 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/JsonRpcResponse.java @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * JSON-RPC 2.0 response structure. + *

+ * This is an internal class representing the wire format of a JSON-RPC + * response. It follows the JSON-RPC 2.0 specification. A response contains + * either a result or an error, but not both. + * + * @see JsonRpcRequest + * @see JsonRpcError + * @see JSON-RPC 2.0 + * Specification + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JsonRpcResponse { + + @JsonProperty("jsonrpc") + private String jsonrpc; + + @JsonProperty("id") + private Object id; + + @JsonProperty("result") + private Object result; + + @JsonProperty("error") + private JsonRpcError error; + + /** + * Gets the JSON-RPC version. + * + * @return the version string (should be "2.0") + */ + public String getJsonrpc() { + return jsonrpc; + } + + /** + * Sets the JSON-RPC version. + * + * @param jsonrpc + * the version string + */ + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + /** + * Gets the response ID. + * + * @return the request identifier this response corresponds to + */ + public Object getId() { + return id; + } + + /** + * Sets the response ID. + * + * @param id + * the response identifier + */ + public void setId(Object id) { + this.id = id; + } + + /** + * Gets the result of the RPC call. + * + * @return the result object, or {@code null} if there was an error + */ + public Object getResult() { + return result; + } + + /** + * Sets the result of the RPC call. + * + * @param result + * the result object + */ + public void setResult(Object result) { + this.result = result; + } + + /** + * Gets the error if the RPC call failed. + * + * @return the error object, or {@code null} if successful + */ + public JsonRpcError getError() { + return error; + } + + /** + * Sets the error for a failed RPC call. + * + * @param error + * the error object + */ + public void setError(JsonRpcError error) { + this.error = error; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java new file mode 100644 index 000000000..b535ee396 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ListSessionsResponse.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from listing sessions. + *

+ * This is a low-level class for JSON-RPC communication containing the list of + * available sessions. + * + * @see com.github.copilot.sdk.CopilotClient#listSessions() + * @see SessionMetadata + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ListSessionsResponse( + /** The list of session metadata. */ + @JsonProperty("sessions") List sessions) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java b/java/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java new file mode 100644 index 000000000..7017db3d2 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/McpHttpServerConfig.java @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for a remote HTTP/SSE MCP (Model Context Protocol) server. + *

+ * Use this to configure an MCP server that communicates over HTTP or + * Server-Sent Events (SSE). + * + *

Example Usage

+ * + *
{@code
+ * var server = new McpHttpServerConfig().setUrl("https://mcp.example.com/sse").setTools(List.of("*"));
+ *
+ * var config = new SessionConfig().setMcpServers(Map.of("remote-server", server));
+ * }
+ * + * @see McpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class McpHttpServerConfig extends McpServerConfig { + + @JsonProperty("type") + private final String type = "http"; + + @JsonProperty("url") + private String url; + + @JsonProperty("headers") + private Map headers; + + /** + * Gets the server type discriminator. + * + * @return always {@code "http"} + */ + public String getType() { + return type; + } + + /** + * Gets the URL of the remote server. + * + * @return the server URL + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL of the remote server. + * + * @param url + * the server URL + * @return this config for method chaining + */ + public McpHttpServerConfig setUrl(String url) { + this.url = url; + return this; + } + + /** + * Gets the optional HTTP headers to include in requests. + * + * @return the headers map, or {@code null} + */ + public Map getHeaders() { + return headers == null ? null : Collections.unmodifiableMap(headers); + } + + /** + * Sets optional HTTP headers to include in requests to this server. + * + * @param headers + * the headers map + * @return this config for method chaining + */ + public McpHttpServerConfig setHeaders(Map headers) { + this.headers = headers; + return this; + } + + @Override + public McpHttpServerConfig setTools(List tools) { + super.setTools(tools); + return this; + } + + @Override + public McpHttpServerConfig setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java b/java/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java new file mode 100644 index 000000000..7cf39af6b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/McpServerConfig.java @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Abstract base class for MCP (Model Context Protocol) server configurations. + *

+ * Use one of the concrete subclasses to configure MCP servers: + *

    + *
  • {@link McpStdioServerConfig} β€” for local/stdio-based MCP servers
  • + *
  • {@link McpHttpServerConfig} β€” for remote HTTP/SSE-based MCP servers
  • + *
+ * + * @see McpStdioServerConfig + * @see McpHttpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = McpStdioServerConfig.class) +@JsonSubTypes({@JsonSubTypes.Type(value = McpStdioServerConfig.class, name = "stdio"), + @JsonSubTypes.Type(value = McpStdioServerConfig.class, name = "local"), + @JsonSubTypes.Type(value = McpHttpServerConfig.class, name = "http"), + @JsonSubTypes.Type(value = McpHttpServerConfig.class, name = "sse")}) +public abstract class McpServerConfig { + + @JsonProperty("tools") + private List tools; + + @JsonProperty("timeout") + private Integer timeout; + + /** + * Gets the list of tools to include from this server. + *

+ * An empty list means none; use {@code "*"} to include all tools. + * + * @return the list of tool names, or {@code null} if not set + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets the list of tools to include from this server. + *

+ * An empty list means none; use {@code "*"} to include all tools. + * + * @param tools + * the list of tool names, or {@code null} + * @return this config for method chaining + */ + public McpServerConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the optional timeout in milliseconds for tool calls to this server. + * + * @return the timeout in milliseconds, or {@code null} for the default + */ + public Integer getTimeout() { + return timeout; + } + + /** + * Sets an optional timeout in milliseconds for tool calls to this server. + * + * @param timeout + * the timeout in milliseconds, or {@code null} for the default + * @return this config for method chaining + */ + public McpServerConfig setTimeout(Integer timeout) { + this.timeout = timeout; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java b/java/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java new file mode 100644 index 000000000..900034be6 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/McpStdioServerConfig.java @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Configuration for a local/stdio MCP (Model Context Protocol) server. + *

+ * Use this to configure an MCP server that is launched as a local subprocess + * and communicates via standard input/output. + * + *

Example Usage

+ * + *
{@code
+ * var server = new McpStdioServerConfig().setCommand("npx")
+ * 		.setArgs(List.of("-y", "@modelcontextprotocol/server-filesystem", "/path")).setTools(List.of("*"));
+ *
+ * var config = new SessionConfig().setMcpServers(Map.of("filesystem", server));
+ * }
+ * + * @see McpServerConfig + * @see SessionConfig#setMcpServers(java.util.Map) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class McpStdioServerConfig extends McpServerConfig { + + @JsonProperty("type") + private final String type = "stdio"; + + @JsonProperty("command") + private String command; + + @JsonProperty("args") + private List args; + + @JsonProperty("env") + private Map env; + + @JsonProperty("workingDirectory") + private String workingDirectory; + + /** + * Gets the server type discriminator. + * + * @return always {@code "stdio"} + */ + public String getType() { + return type; + } + + /** + * Gets the command to run the MCP server. + * + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * Sets the command to run the MCP server. + * + * @param command + * the command + * @return this config for method chaining + */ + public McpStdioServerConfig setCommand(String command) { + this.command = command; + return this; + } + + /** + * Gets the arguments to pass to the command. + * + * @return the arguments list, or {@code null} + */ + public List getArgs() { + return args == null ? null : Collections.unmodifiableList(args); + } + + /** + * Sets the arguments to pass to the command. + * + * @param args + * the arguments list + * @return this config for method chaining + */ + public McpStdioServerConfig setArgs(List args) { + this.args = args; + return this; + } + + /** + * Gets the environment variables to pass to the server. + * + * @return the environment variables map, or {@code null} + */ + public Map getEnv() { + return env == null ? null : Collections.unmodifiableMap(env); + } + + /** + * Sets the environment variables to pass to the server. + * + * @param env + * the environment variables map + * @return this config for method chaining + */ + public McpStdioServerConfig setEnv(Map env) { + this.env = env; + return this; + } + + /** + * Gets the working directory for the server process. + * + * @return the working directory path, or {@code null} + */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** + * Sets the working directory for the server process. + * + * @param workingDirectory + * the working directory path + * @return this config for method chaining + */ + public McpStdioServerConfig setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + @Override + public McpStdioServerConfig setTools(List tools) { + super.setTools(tools); + return this; + } + + @Override + public McpStdioServerConfig setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/MessageAttachment.java b/java/src/main/java/com/github/copilot/sdk/json/MessageAttachment.java new file mode 100644 index 000000000..3371a56ba --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/MessageAttachment.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Marker interface for all attachment types that can be included in a message. + *

+ * This is the Java equivalent of the .NET SDK's + * {@code UserMessageDataAttachmentsItem} polymorphic base class. + * + * @see Attachment + * @see BlobAttachment + * @see MessageOptions#setAttachments(java.util.List) + * @since 1.2.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({@JsonSubTypes.Type(value = Attachment.class, name = "file"), + @JsonSubTypes.Type(value = BlobAttachment.class, name = "blob")}) +public sealed interface MessageAttachment permits Attachment, BlobAttachment { + + /** + * Returns the attachment type discriminator (e.g., "file", "blob"). + * + * @return the type string + */ + String getType(); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java new file mode 100644 index 000000000..21909d576 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/MessageOptions.java @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Options for sending a message to a Copilot session. + *

+ * This class specifies the message content and optional attachments to send to + * the assistant. All setter methods return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var options = new MessageOptions().setPrompt("Explain this code")
+ * 		.setAttachments(List.of(new Attachment("file", "/path/to/file.java", null)));
+ *
+ * session.send(options).get();
+ * }
+ * + *

Blob Attachment Example

+ * + *
{@code
+ * var options = new MessageOptions().setPrompt("Describe this image").setAttachments(List.of(new BlobAttachment()
+ * 		.setData("iVBORw0KGgoAAAANSUhEUg...").setMimeType("image/png").setDisplayName("screenshot.png")));
+ *
+ * session.send(options).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#send(MessageOptions) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MessageOptions { + + private String prompt; + private List attachments; + private String mode; + private Map requestHeaders; + + /** + * Gets the message prompt. + * + * @return the prompt text + */ + public String getPrompt() { + return prompt; + } + + /** + * Sets the message prompt to send to the assistant. + * + * @param prompt + * the message text + * @return this options instance for method chaining + */ + public MessageOptions setPrompt(String prompt) { + this.prompt = prompt; + return this; + } + + /** + * Gets the attachments. + * + * @return the list of attachments + */ + public List getAttachments() { + return attachments == null ? null : Collections.unmodifiableList(attachments); + } + + /** + * Sets attachments to include with the message. + *

+ * Attachments provide additional context to the assistant. Supported types: + *

    + *
  • {@link Attachment} β€” file, directory, code selection, or GitHub + * reference
  • + *
  • {@link BlobAttachment} β€” inline base64-encoded binary data (e.g. images) + *
  • + *
+ * + * @param attachments + * the list of attachments + * @return this options instance for method chaining + * @see Attachment + * @see BlobAttachment + */ + public MessageOptions setAttachments(List attachments) { + this.attachments = attachments != null ? new ArrayList<>(attachments) : null; + return this; + } + + /** + * Sets the message delivery mode. + *

+ * Valid modes: + *

    + *
  • "enqueue" - Queue the message for processing (default)
  • + *
  • "immediate" - Process the message immediately
  • + *
+ * + * @param mode + * the delivery mode + * @return this options instance for method chaining + */ + public MessageOptions setMode(String mode) { + this.mode = mode; + return this; + } + + /** + * Gets the delivery mode. + * + * @return the delivery mode + */ + public String getMode() { + return mode; + } + + /** + * Gets the custom per-turn HTTP headers for outbound model requests. + * + * @return the headers map, or {@code null} if not set + */ + public Map getRequestHeaders() { + return requestHeaders == null ? null : Collections.unmodifiableMap(requestHeaders); + } + + /** + * Sets custom per-turn HTTP headers for outbound model requests. + *

+ * These headers are included in the model API request for this specific message + * turn. Use this to pass per-request authentication, tracing, or custom + * metadata. + * + * @param requestHeaders + * the headers map + * @return this options instance for method chaining + */ + public MessageOptions setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + return this; + } + + /** + * Creates a shallow clone of this {@code MessageOptions} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like attachment items) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this options instance + */ + @Override + public MessageOptions clone() { + MessageOptions copy = new MessageOptions(); + copy.prompt = this.prompt; + copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null; + copy.mode = this.mode; + copy.requestHeaders = this.requestHeaders != null ? new HashMap<>(this.requestHeaders) : null; + return copy; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelBilling.java b/java/src/main/java/com/github/copilot/sdk/json/ModelBilling.java new file mode 100644 index 000000000..d04ef0d3e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelBilling.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model billing information. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelBilling { + + @JsonProperty("multiplier") + private double multiplier; + + public double getMultiplier() { + return multiplier; + } + + public ModelBilling setMultiplier(double multiplier) { + this.multiplier = multiplier; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java b/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java new file mode 100644 index 000000000..1cadcb05e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilities.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model capabilities and limits. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelCapabilities { + + @JsonProperty("supports") + private ModelSupports supports; + + @JsonProperty("limits") + private ModelLimits limits; + + public ModelSupports getSupports() { + return supports; + } + + public ModelCapabilities setSupports(ModelSupports supports) { + this.supports = supports; + return this; + } + + public ModelLimits getLimits() { + return limits; + } + + public ModelCapabilities setLimits(ModelLimits limits) { + this.limits = limits; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java b/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java new file mode 100644 index 000000000..02727d4b5 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelCapabilitiesOverride.java @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * Per-property overrides for model capabilities, deep-merged over runtime + * defaults. + *

+ * Use this to override specific model capabilities when creating a session or + * switching models with {@link com.github.copilot.sdk.CopilotSession#setModel}. + * Only non-null fields are applied; unset fields retain their runtime defaults. + * + *

Example: Disable vision for a session

+ * + *
{@code
+ * var config = new SessionConfig().setModel("claude-sonnet-4.5").setModelCapabilities(
+ * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(false)));
+ * }
+ * + *

Example: Override capabilities when switching models

+ * + *
{@code
+ * session.setModel("claude-sonnet-4.5", null,
+ * 		new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverride.Supports().setVision(true))).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#setModel(String, String, + * ModelCapabilitiesOverride) + * @see SessionConfig#setModelCapabilities(ModelCapabilitiesOverride) + * @since 1.3.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelCapabilitiesOverride { + + @JsonProperty("supports") + private Supports supports; + + @JsonProperty("limits") + private Limits limits; + + /** + * Gets the feature flag overrides. + * + * @return the supports overrides, or {@code null} if not set + */ + public Supports getSupports() { + return supports; + } + + /** + * Sets the feature flag overrides. + * + * @param supports + * the supports overrides + * @return this instance for method chaining + */ + public ModelCapabilitiesOverride setSupports(Supports supports) { + this.supports = supports; + return this; + } + + /** + * Gets the token limit overrides. + * + * @return the limits overrides, or {@code null} if not set + */ + public Limits getLimits() { + return limits; + } + + /** + * Sets the token limit overrides. + * + * @param limits + * the limits overrides + * @return this instance for method chaining + */ + public ModelCapabilitiesOverride setLimits(Limits limits) { + this.limits = limits; + return this; + } + + /** + * Feature flag overrides for model capabilities. + *

+ * Set a field to {@code true} or {@code false} to override that capability; + * leave it {@code null} to use the runtime default. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Supports { + + @JsonProperty("vision") + private Boolean vision; + + @JsonProperty("reasoningEffort") + private Boolean reasoningEffort; + + /** + * Gets the vision override. + * + * @return an {@link java.util.Optional} containing {@code true} to enable + * vision or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default + */ + @JsonIgnore + public Optional getVision() { + return Optional.ofNullable(vision); + } + + /** + * Sets whether vision (image input) is enabled. Use {@link #clearVision()} to + * revert to the runtime default. + * + * @param vision + * {@code true} to enable, {@code false} to disable + * @return this instance for method chaining + */ + public Supports setVision(boolean vision) { + this.vision = vision; + return this; + } + + /** + * Clears the vision setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Supports clearVision() { + this.vision = null; + return this; + } + + /** + * Gets the reasoning effort override. + * + * @return an {@link java.util.Optional} containing {@code true} to enable + * reasoning effort or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default + */ + @JsonIgnore + public Optional getReasoningEffort() { + return Optional.ofNullable(reasoningEffort); + } + + /** + * Sets whether reasoning effort configuration is enabled. Use + * {@link #clearReasoningEffort()} to revert to the runtime default. + * + * @param reasoningEffort + * {@code true} to enable, {@code false} to disable + * @return this instance for method chaining + */ + public Supports setReasoningEffort(boolean reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Clears the reasoningEffort setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Supports clearReasoningEffort() { + this.reasoningEffort = null; + return this; + } + + } + + /** + * Token limit overrides for model capabilities. + *

+ * Set a field to override that limit; leave it {@code null} to use the runtime + * default. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Limits { + + @JsonProperty("max_prompt_tokens") + private Integer maxPromptTokens; + + @JsonProperty("max_output_tokens") + private Integer maxOutputTokens; + + @JsonProperty("max_context_window_tokens") + private Integer maxContextWindowTokens; + + /** + * Gets the maximum prompt tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + @JsonIgnore + public OptionalInt getMaxPromptTokens() { + return maxPromptTokens == null ? OptionalInt.empty() : OptionalInt.of(maxPromptTokens); + } + + /** + * Sets the maximum number of tokens in a prompt. + * + * @param maxPromptTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxPromptTokens(int maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + /** + * Clears the maxPromptTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxPromptTokens() { + this.maxPromptTokens = null; + return this; + } + + /** + * Gets the maximum output tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + @JsonIgnore + public OptionalInt getMaxOutputTokens() { + return maxOutputTokens == null ? OptionalInt.empty() : OptionalInt.of(maxOutputTokens); + } + + /** + * Sets the maximum number of output tokens. + * + * @param maxOutputTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxOutputTokens(int maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + return this; + } + + /** + * Clears the maxOutputTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxOutputTokens() { + this.maxOutputTokens = null; + return this; + } + + /** + * Gets the maximum context window tokens override. + * + * @return the override value, or {@code null} to use the runtime default + */ + @JsonIgnore + public OptionalInt getMaxContextWindowTokens() { + return maxContextWindowTokens == null ? OptionalInt.empty() : OptionalInt.of(maxContextWindowTokens); + } + + /** + * Sets the maximum total context window size in tokens. + * + * @param maxContextWindowTokens + * the override value, or {@code null} to use the runtime default + * @return this instance for method chaining + */ + public Limits setMaxContextWindowTokens(int maxContextWindowTokens) { + this.maxContextWindowTokens = maxContextWindowTokens; + return this; + } + + /** + * Clears the maxContextWindowTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public Limits clearMaxContextWindowTokens() { + this.maxContextWindowTokens = null; + return this; + } + + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelInfo.java b/java/src/main/java/com/github/copilot/sdk/json/ModelInfo.java new file mode 100644 index 000000000..e04790069 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelInfo.java @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Information about an available model. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelInfo { + + /** + * Model identifier (e.g., "claude-sonnet-4.5"). + */ + @JsonProperty("id") + private String id; + + /** + * Display name. + */ + @JsonProperty("name") + private String name; + + /** + * Model capabilities and limits. + */ + @JsonProperty("capabilities") + private ModelCapabilities capabilities; + + /** + * Policy state. + */ + @JsonProperty("policy") + private ModelPolicy policy; + + /** + * Billing information. + */ + @JsonProperty("billing") + private ModelBilling billing; + + /** + * Supported reasoning effort levels (only present if model supports reasoning + * effort). + */ + @JsonProperty("supportedReasoningEfforts") + private List supportedReasoningEfforts; + + /** + * Default reasoning effort level (only present if model supports reasoning + * effort). + */ + @JsonProperty("defaultReasoningEffort") + private String defaultReasoningEffort; + + public String getId() { + return id; + } + + public ModelInfo setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public ModelInfo setName(String name) { + this.name = name; + return this; + } + + public ModelCapabilities getCapabilities() { + return capabilities; + } + + public ModelInfo setCapabilities(ModelCapabilities capabilities) { + this.capabilities = capabilities; + return this; + } + + public ModelPolicy getPolicy() { + return policy; + } + + public ModelInfo setPolicy(ModelPolicy policy) { + this.policy = policy; + return this; + } + + public ModelBilling getBilling() { + return billing; + } + + public ModelInfo setBilling(ModelBilling billing) { + this.billing = billing; + return this; + } + + /** + * Gets the supported reasoning effort levels. + * + * @return the list of supported reasoning effort levels, or {@code null} if the + * model doesn't support reasoning effort + */ + public List getSupportedReasoningEfforts() { + return supportedReasoningEfforts; + } + + /** + * Sets the supported reasoning effort levels. + * + * @param supportedReasoningEfforts + * the list of supported reasoning effort levels + * @return this instance for method chaining + */ + public ModelInfo setSupportedReasoningEfforts(List supportedReasoningEfforts) { + this.supportedReasoningEfforts = supportedReasoningEfforts; + return this; + } + + /** + * Gets the default reasoning effort level. + * + * @return the default reasoning effort level, or {@code null} if the model + * doesn't support reasoning effort + */ + public String getDefaultReasoningEffort() { + return defaultReasoningEffort; + } + + /** + * Sets the default reasoning effort level. + * + * @param defaultReasoningEffort + * the default reasoning effort level + * @return this instance for method chaining + */ + public ModelInfo setDefaultReasoningEffort(String defaultReasoningEffort) { + this.defaultReasoningEffort = defaultReasoningEffort; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelLimits.java b/java/src/main/java/com/github/copilot/sdk/json/ModelLimits.java new file mode 100644 index 000000000..734a50ded --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelLimits.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model limits. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelLimits { + + @JsonProperty("max_prompt_tokens") + private Integer maxPromptTokens; + + @JsonProperty("max_context_window_tokens") + private int maxContextWindowTokens; + + @JsonProperty("vision") + private ModelVisionLimits vision; + + public Integer getMaxPromptTokens() { + return maxPromptTokens; + } + + public ModelLimits setMaxPromptTokens(Integer maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + public int getMaxContextWindowTokens() { + return maxContextWindowTokens; + } + + public ModelLimits setMaxContextWindowTokens(int maxContextWindowTokens) { + this.maxContextWindowTokens = maxContextWindowTokens; + return this; + } + + public ModelVisionLimits getVision() { + return vision; + } + + public ModelLimits setVision(ModelVisionLimits vision) { + this.vision = vision; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java b/java/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java new file mode 100644 index 000000000..9cf226272 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelPolicy.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model policy state. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelPolicy { + + @JsonProperty("state") + private String state; + + @JsonProperty("terms") + private String terms; + + public String getState() { + return state; + } + + public ModelPolicy setState(String state) { + this.state = state; + return this; + } + + public String getTerms() { + return terms; + } + + public ModelPolicy setTerms(String terms) { + this.terms = terms; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelSupports.java b/java/src/main/java/com/github/copilot/sdk/json/ModelSupports.java new file mode 100644 index 000000000..905ac6823 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelSupports.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model support flags. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelSupports { + + @JsonProperty("vision") + private boolean vision; + + @JsonProperty("reasoningEffort") + private boolean reasoningEffort; + + public boolean isVision() { + return vision; + } + + public ModelSupports setVision(boolean vision) { + this.vision = vision; + return this; + } + + /** + * Returns whether this model supports reasoning effort configuration. + * + * @return {@code true} if the model supports reasoning effort + */ + public boolean isReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets whether this model supports reasoning effort configuration. + * + * @param reasoningEffort + * {@code true} if the model supports reasoning effort + * @return this instance for method chaining + */ + public ModelSupports setReasoningEffort(boolean reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java b/java/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java new file mode 100644 index 000000000..331985e53 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ModelVisionLimits.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Model vision-specific limits. + * + * @since 1.0.1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelVisionLimits { + + @JsonProperty("supported_media_types") + private List supportedMediaTypes; + + @JsonProperty("max_prompt_images") + private int maxPromptImages; + + @JsonProperty("max_prompt_image_size") + private int maxPromptImageSize; + + public List getSupportedMediaTypes() { + return supportedMediaTypes; + } + + public ModelVisionLimits setSupportedMediaTypes(List supportedMediaTypes) { + this.supportedMediaTypes = supportedMediaTypes; + return this; + } + + public int getMaxPromptImages() { + return maxPromptImages; + } + + public ModelVisionLimits setMaxPromptImages(int maxPromptImages) { + this.maxPromptImages = maxPromptImages; + return this; + } + + public int getMaxPromptImageSize() { + return maxPromptImageSize; + } + + public ModelVisionLimits setMaxPromptImageSize(int maxPromptImageSize) { + this.maxPromptImageSize = maxPromptImageSize; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java new file mode 100644 index 000000000..d230748fc --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionHandler.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling permission requests from the AI assistant. + *

+ * When the assistant needs permission to perform certain actions (such as + * executing tools or accessing resources), this handler is invoked to approve + * or deny the request. + * + *

Example Implementation

+ * + *
{@code
+ * PermissionHandler handler = (request, invocation) -> {
+ * 	// Check the permission kind
+ * 	if ("dangerous-action".equals(request.getKind())) {
+ * 		// Deny dangerous actions
+ * 		return CompletableFuture
+ * 				.completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.REJECTED));
+ * 	}
+ *
+ * 	// Approve other requests
+ * 	return CompletableFuture
+ * 			.completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED));
+ * };
+ * }
+ * + *

+ * A pre-built handler that approves all requests is available as + * {@link #APPROVE_ALL}. + * + * @see SessionConfig#setOnPermissionRequest(PermissionHandler) + * @see PermissionRequest + * @see PermissionRequestResult + * @since 1.0.0 + */ +@FunctionalInterface +public interface PermissionHandler { + + /** + * A pre-built handler that approves all permission requests. + * + * @since 1.0.11 + */ + PermissionHandler APPROVE_ALL = (request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + + /** + * Handles a permission request from the assistant. + *

+ * The handler should evaluate the request and return a result indicating + * whether the permission is granted or denied. + * + * @param request + * the permission request details + * @param invocation + * the invocation context with session information + * @return a future that completes with the permission decision + */ + CompletableFuture handle(PermissionRequest request, PermissionInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java new file mode 100644 index 000000000..218a570cf --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionInvocation.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context information for a permission request invocation. + *

+ * This object provides context about the session where the permission request + * originated. + * + * @see PermissionHandler + * @since 1.0.0 + */ +public final class PermissionInvocation { + + private String sessionId; + + /** + * Gets the session ID where the permission was requested. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this invocation for method chaining + */ + public PermissionInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java new file mode 100644 index 000000000..99dc7018a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequest.java @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a permission request from the AI assistant. + *

+ * When the assistant needs permission to perform certain actions, this object + * contains the details of the request, including the kind of permission and any + * associated tool call. + * + * @see PermissionHandler + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PermissionRequest { + + @JsonProperty("kind") + private String kind; + + @JsonProperty("toolCallId") + private String toolCallId; + + private Map extensionData; + + /** + * Gets the kind of permission being requested. + * + * @return the permission kind + */ + public String getKind() { + return kind; + } + + /** + * Sets the permission kind. + * + * @param kind + * the permission kind + */ + public void setKind(String kind) { + this.kind = kind; + } + + /** + * Gets the associated tool call ID, if applicable. + * + * @return the tool call ID, or {@code null} if not a tool-related request + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call ID. + * + * @param toolCallId + * the tool call ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * Gets additional extension data for the request. + * + * @return the extension data map + */ + public Map getExtensionData() { + return extensionData; + } + + /** + * Sets additional extension data for the request. + * + * @param extensionData + * the extension data map + */ + public void setExtensionData(Map extensionData) { + this.extensionData = extensionData; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java new file mode 100644 index 000000000..3d7390f03 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Result of a permission request decision. + *

+ * This object indicates whether a permission request was approved or denied, + * and may include additional rules for future similar requests. + * + *

Common Result Kinds

+ *
    + *
  • {@link PermissionRequestResultKind#APPROVED} β€” approved
  • + *
  • {@link PermissionRequestResultKind#DENIED_BY_RULES} β€” denied by + * rules
  • + *
  • {@link PermissionRequestResultKind#DENIED_COULD_NOT_REQUEST_FROM_USER} β€” + * no handler and couldn't ask user
  • + *
  • {@link PermissionRequestResultKind#DENIED_INTERACTIVELY_BY_USER} β€” denied + * by the user interactively
  • + *
+ * + * @see PermissionHandler + * @see PermissionRequestResultKind + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class PermissionRequestResult { + + @JsonProperty("kind") + private String kind; + + @JsonProperty("rules") + private List rules; + + /** + * Gets the result kind as a string. + * + * @return the result kind indicating approval or denial + */ + public String getKind() { + return kind; + } + + /** + * Sets the result kind using a {@link PermissionRequestResultKind} value. + * + * @param kind + * the result kind + * @return this result for method chaining + * @since 1.1.0 + */ + public PermissionRequestResult setKind(PermissionRequestResultKind kind) { + this.kind = kind != null ? kind.getValue() : null; + return this; + } + + /** + * Sets the result kind using a raw string value. + * + * @param kind + * the result kind string + * @return this result for method chaining + */ + public PermissionRequestResult setKind(String kind) { + this.kind = kind; + return this; + } + + /** + * Gets the approval rules. + * + * @return the list of rules for future similar requests + */ + public List getRules() { + return rules; + } + + /** + * Sets approval rules for future similar requests. + * + * @param rules + * the list of rules + * @return this result for method chaining + */ + public PermissionRequestResult setRules(List rules) { + this.rules = rules; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java new file mode 100644 index 000000000..f782fd76b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PermissionRequestResultKind.java @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Describes the outcome kind of a permission request result. + * + *

+ * This is a string-backed value type that can hold both well-known kinds (via + * the static constants) and arbitrary extension values forwarded by the server. + * Comparisons are case-insensitive to match server behaviour. + * + *

Well-known kinds

+ *
    + *
  • {@link #APPROVED} β€” the permission was approved for this one + * instance.
  • + *
  • {@link #REJECTED} β€” the permission was denied interactively by the + * user.
  • + *
  • {@link #USER_NOT_AVAILABLE} β€” the permission was denied because user + * confirmation was unavailable.
  • + *
  • {@link #NO_RESULT} β€” no permission decision was made.
  • + *
+ * + * @see PermissionRequestResult + * @since 1.1.0 + */ +public final class PermissionRequestResultKind { + + /** The permission was approved for this one instance. */ + public static final PermissionRequestResultKind APPROVED = new PermissionRequestResultKind("approve-once"); + + /** The permission was denied interactively by the user. */ + public static final PermissionRequestResultKind REJECTED = new PermissionRequestResultKind("reject"); + + /** The permission was denied because user confirmation was unavailable. */ + public static final PermissionRequestResultKind USER_NOT_AVAILABLE = new PermissionRequestResultKind( + "user-not-available"); + + /** + * Leaves the pending permission request unanswered. + *

+ * When the SDK is used as an extension and the extension's permission handler + * cannot or chooses not to handle a given permission request, it can return + * {@code NO_RESULT} to leave the request unanswered, allowing another client to + * handle it. + *

+ * Warning: This kind is only valid with protocol v3 servers + * (broadcast permission model). When connected to a protocol v2 server, the SDK + * will throw {@link IllegalStateException} because v2 expects exactly one + * response per permission request. + */ + public static final PermissionRequestResultKind NO_RESULT = new PermissionRequestResultKind("no-result"); + + /** + * @deprecated Use {@link #REJECTED} instead. + */ + @Deprecated + public static final PermissionRequestResultKind DENIED_INTERACTIVELY_BY_USER = REJECTED; + + /** + * @deprecated Use {@link #USER_NOT_AVAILABLE} instead. + */ + @Deprecated + public static final PermissionRequestResultKind DENIED_COULD_NOT_REQUEST_FROM_USER = USER_NOT_AVAILABLE; + + /** + * @deprecated Use {@link #USER_NOT_AVAILABLE} instead. + */ + @Deprecated + public static final PermissionRequestResultKind DENIED_BY_RULES = USER_NOT_AVAILABLE; + + private final String value; + + /** + * Creates a new {@code PermissionRequestResultKind} with the given string + * value. Useful for extension kinds not covered by the well-known constants. + * + * @param value + * the string value; {@code null} is treated as an empty string + */ + @JsonCreator + public PermissionRequestResultKind(String value) { + this.value = value != null ? value : ""; + } + + /** + * Returns the underlying string value of this kind. + * + * @return the string value, never {@code null} + */ + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof PermissionRequestResultKind)) { + return false; + } + PermissionRequestResultKind other = (PermissionRequestResultKind) obj; + return value.equalsIgnoreCase(other.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value.toLowerCase(java.util.Locale.ROOT)); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java new file mode 100644 index 000000000..e86499b2f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PingResponse.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from a ping request to the Copilot CLI server. + *

+ * The ping response confirms connectivity and provides information about the + * server, including the protocol version. + * + * @see com.github.copilot.sdk.CopilotClient#ping(String) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PingResponse( + /** The echo message from the server. */ + @JsonProperty("message") String message, + /** The server timestamp in milliseconds since epoch. */ + @JsonProperty("timestamp") long timestamp, + /** + * The SDK protocol version supported by the server. The SDK validates that this + * version matches the expected version to ensure compatibility. + */ + @JsonProperty("protocolVersion") Integer protocolVersion) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHandler.java new file mode 100644 index 000000000..12688e63c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHandler.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for post-tool-use hooks. + *

+ * This hook is called after a tool has been executed, allowing you to: + *

    + *
  • Inspect or modify tool results
  • + *
  • Add additional context for the model
  • + *
  • Suppress output
  • + *
+ * + * @since 1.0.6 + */ +@FunctionalInterface +public interface PostToolUseHandler { + + /** + * Handles a post-tool-use hook invocation. + * + * @param input + * the hook input containing tool name, arguments, and result + * @param invocation + * context information about the invocation + * @return a future that resolves with the hook output, or {@code null} to use + * defaults + */ + CompletableFuture handle(PostToolUseHookInput input, HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookInput.java new file mode 100644 index 000000000..4ac398506 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookInput.java @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Input for a post-tool-use hook. + * + * @since 1.0.6 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PostToolUseHookInput { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("timestamp") + private long timestamp; + + @JsonProperty("cwd") + private String cwd; + + @JsonProperty("toolName") + private String toolName; + + @JsonProperty("toolArgs") + private JsonNode toolArgs; + + @JsonProperty("toolResult") + private JsonNode toolResult; + + /** + * Gets the runtime session ID of the session that triggered the hook. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the runtime session ID of the session that triggered the hook. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public PostToolUseHookInput setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the timestamp of the hook invocation. + * + * @return the timestamp in milliseconds + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Sets the timestamp of the hook invocation. + * + * @param timestamp + * the timestamp in milliseconds + * @return this instance for method chaining + */ + public PostToolUseHookInput setTimestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + /** + * Gets the current working directory. + * + * @return the working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the current working directory. + * + * @param cwd + * the working directory path + * @return this instance for method chaining + */ + public PostToolUseHookInput setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the name of the tool that was invoked. + * + * @return the tool name + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the name of the tool that was invoked. + * + * @param toolName + * the tool name + * @return this instance for method chaining + */ + public PostToolUseHookInput setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Gets the arguments passed to the tool. + * + * @return the tool arguments as a JSON node + */ + public JsonNode getToolArgs() { + return toolArgs; + } + + /** + * Sets the arguments passed to the tool. + * + * @param toolArgs + * the tool arguments as a JSON node + * @return this instance for method chaining + */ + public PostToolUseHookInput setToolArgs(JsonNode toolArgs) { + this.toolArgs = toolArgs; + return this; + } + + /** + * Gets the result returned by the tool. + * + * @return the tool result as a JSON node + */ + public JsonNode getToolResult() { + return toolResult; + } + + /** + * Sets the result returned by the tool. + * + * @param toolResult + * the tool result as a JSON node + * @return this instance for method chaining + */ + public PostToolUseHookInput setToolResult(JsonNode toolResult) { + this.toolResult = toolResult; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java new file mode 100644 index 000000000..a532bf15e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PostToolUseHookOutput.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Output for a post-tool-use hook. + * + * @param modifiedResult + * the modified tool result, or {@code null} to use original + * @param additionalContext + * additional context to provide to the model + * @param suppressOutput + * {@code true} to suppress output + * @since 1.0.6 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PostToolUseHookOutput(@JsonProperty("modifiedResult") JsonNode modifiedResult, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHandler.java b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHandler.java new file mode 100644 index 000000000..3f98972e5 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHandler.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for pre-tool-use hooks. + *

+ * This hook is called before a tool is executed, allowing you to: + *

    + *
  • Approve or deny tool execution
  • + *
  • Modify tool arguments
  • + *
  • Add additional context for the model
  • + *
+ * + * @since 1.0.6 + */ +@FunctionalInterface +public interface PreToolUseHandler { + + /** + * Handles a pre-tool-use hook invocation. + * + * @param input + * the hook input containing tool name and arguments + * @param invocation + * context information about the invocation + * @return a future that resolves with the hook output, or {@code null} to use + * defaults + */ + CompletableFuture handle(PreToolUseHookInput input, HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookInput.java new file mode 100644 index 000000000..6cbab78b7 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookInput.java @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Input for a pre-tool-use hook. + * + * @since 1.0.6 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PreToolUseHookInput { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("timestamp") + private long timestamp; + + @JsonProperty("cwd") + private String cwd; + + @JsonProperty("toolName") + private String toolName; + + @JsonProperty("toolArgs") + private JsonNode toolArgs; + + /** + * Gets the runtime session ID of the session that triggered the hook. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the runtime session ID of the session that triggered the hook. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public PreToolUseHookInput setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the timestamp of the hook invocation. + * + * @return the timestamp in milliseconds + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Sets the timestamp of the hook invocation. + * + * @param timestamp + * the timestamp in milliseconds + * @return this instance for method chaining + */ + public PreToolUseHookInput setTimestamp(long timestamp) { + this.timestamp = timestamp; + return this; + } + + /** + * Gets the current working directory. + * + * @return the working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the current working directory. + * + * @param cwd + * the working directory path + * @return this instance for method chaining + */ + public PreToolUseHookInput setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the name of the tool being invoked. + * + * @return the tool name + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the name of the tool being invoked. + * + * @param toolName + * the tool name + * @return this instance for method chaining + */ + public PreToolUseHookInput setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Gets the arguments passed to the tool. + * + * @return the tool arguments as a JSON node + */ + public JsonNode getToolArgs() { + return toolArgs; + } + + /** + * Sets the arguments passed to the tool. + * + * @param toolArgs + * the tool arguments as a JSON node + * @return this instance for method chaining + */ + public PreToolUseHookInput setToolArgs(JsonNode toolArgs) { + this.toolArgs = toolArgs; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java new file mode 100644 index 000000000..a92c8f01a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/PreToolUseHookOutput.java @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Output for a pre-tool-use hook. + * + * @param permissionDecision + * "allow", "deny", or "ask" + * @param permissionDecisionReason + * the reason for the permission decision + * @param modifiedArgs + * the modified tool arguments, or {@code null} to use original + * @param additionalContext + * additional context to provide to the model + * @param suppressOutput + * {@code true} to suppress output + * @since 1.0.6 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PreToolUseHookOutput(@JsonProperty("permissionDecision") String permissionDecision, + @JsonProperty("permissionDecisionReason") String permissionDecisionReason, + @JsonProperty("modifiedArgs") JsonNode modifiedArgs, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { + + /** + * Creates an output that allows the tool to execute. + * + * @return a new PreToolUseHookOutput with permission decision "allow" + */ + public static PreToolUseHookOutput allow() { + return new PreToolUseHookOutput("allow", null, null, null, null); + } + + /** + * Creates an output that denies the tool execution. + * + * @return a new PreToolUseHookOutput with permission decision "deny" + */ + public static PreToolUseHookOutput deny() { + return new PreToolUseHookOutput("deny", null, null, null, null); + } + + /** + * Creates an output that denies the tool execution with a reason. + * + * @param reason + * the reason for denying the tool execution + * @return a new PreToolUseHookOutput with permission decision "deny" and reason + */ + public static PreToolUseHookOutput deny(String reason) { + return new PreToolUseHookOutput("deny", reason, null, null, null); + } + + /** + * Creates an output that asks for user confirmation before executing the tool. + * + * @return a new PreToolUseHookOutput with permission decision "ask" + */ + public static PreToolUseHookOutput ask() { + return new PreToolUseHookOutput("ask", null, null, null, null); + } + + /** + * Creates an output with modified tool arguments. + * + * @param permissionDecision + * "allow", "deny", or "ask" + * @param modifiedArgs + * the modified tool arguments + * @return a new PreToolUseHookOutput with the specified permission and modified + * arguments + */ + public static PreToolUseHookOutput withModifiedArgs(String permissionDecision, JsonNode modifiedArgs) { + return new PreToolUseHookOutput(permissionDecision, null, modifiedArgs, null, null); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java b/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java new file mode 100644 index 000000000..1c5e6fcc7 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ProviderConfig.java @@ -0,0 +1,372 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.OptionalInt; + +/** + * Configuration for a custom API provider (BYOK - Bring Your Own Key). + *

+ * This allows using your own OpenAI, Azure OpenAI, or other compatible API + * endpoints instead of the default Copilot backend. All setter methods return + * {@code this} for method chaining. + * + *

Example Usage - OpenAI

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("openai").setBaseUrl("https://api.openai.com/v1").setApiKey("sk-...");
+ * }
+ * + *

Example Usage - Azure OpenAI

+ * + *
{@code
+ * var provider = new ProviderConfig().setType("azure")
+ * 		.setAzure(new AzureOptions().setEndpoint("https://my-resource.openai.azure.com").setDeployment("gpt-4"));
+ * }
+ * + * @see SessionConfig#setProvider(ProviderConfig) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ProviderConfig { + + @JsonProperty("type") + private String type; + + @JsonProperty("wireApi") + private String wireApi; + + @JsonProperty("baseUrl") + private String baseUrl; + + @JsonProperty("apiKey") + private String apiKey; + + @JsonProperty("bearerToken") + private String bearerToken; + + @JsonProperty("azure") + private AzureOptions azure; + + @JsonProperty("headers") + private Map headers; + + @JsonProperty("modelId") + private String modelId; + + @JsonProperty("wireModel") + private String wireModel; + + @JsonProperty("maxPromptTokens") + private Integer maxPromptTokens; + + @JsonProperty("maxOutputTokens") + private Integer maxOutputTokens; + + /** + * Gets the provider type. + * + * @return the provider type (e.g., "openai", "azure") + */ + public String getType() { + return type; + } + + /** + * Sets the provider type. + *

+ * Supported types include: + *

    + *
  • "openai" - OpenAI API
  • + *
  • "azure" - Azure OpenAI Service
  • + *
+ * + * @param type + * the provider type + * @return this config for method chaining + */ + public ProviderConfig setType(String type) { + this.type = type; + return this; + } + + /** + * Gets the wire API format. + * + * @return the wire API format + */ + public String getWireApi() { + return wireApi; + } + + /** + * Sets the wire API format for custom providers. + *

+ * This specifies the API format when using a custom provider that has a + * different wire protocol. + * + * @param wireApi + * the wire API format + * @return this config for method chaining + */ + public ProviderConfig setWireApi(String wireApi) { + this.wireApi = wireApi; + return this; + } + + /** + * Gets the base URL for the API. + * + * @return the API base URL + */ + public String getBaseUrl() { + return baseUrl; + } + + /** + * Sets the base URL for the API. + *

+ * For OpenAI, this is typically "https://api.openai.com/v1". + * + * @param baseUrl + * the API base URL + * @return this config for method chaining + */ + public ProviderConfig setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Gets the API key. + * + * @return the API key + */ + public String getApiKey() { + return apiKey; + } + + /** + * Sets the API key for authentication. + * + * @param apiKey + * the API key + * @return this config for method chaining + */ + public ProviderConfig setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * Gets the bearer token. + * + * @return the bearer token + */ + public String getBearerToken() { + return bearerToken; + } + + /** + * Sets a bearer token for authentication. + *

+ * This is an alternative to API key authentication. + *

+ * Note: The bearer token is a static token + * string. 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. + * + * @param bearerToken + * the bearer token + * @return this config for method chaining + */ + public ProviderConfig setBearerToken(String bearerToken) { + this.bearerToken = bearerToken; + return this; + } + + /** + * Gets the Azure-specific options. + * + * @return the Azure options + */ + public AzureOptions getAzure() { + return azure; + } + + /** + * Sets Azure-specific options for Azure OpenAI Service. + * + * @param azure + * the Azure options + * @return this config for method chaining + * @see AzureOptions + */ + public ProviderConfig setAzure(AzureOptions azure) { + this.azure = azure; + return this; + } + + /** + * Gets the custom HTTP headers for outbound provider requests. + * + * @return the headers map, or {@code null} if not set + */ + public Map getHeaders() { + return headers == null ? null : Collections.unmodifiableMap(headers); + } + + /** + * Sets custom HTTP headers to include in outbound provider requests. + *

+ * Use this to pass additional authentication headers or custom metadata to the + * provider API. + * + * @param headers + * the headers map + * @return this config for method chaining + */ + public ProviderConfig setHeaders(Map headers) { + this.headers = headers; + return this; + } + + /** + * Gets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. + * + * @return the model ID, or {@code null} if not set + */ + public String getModelId() { + return modelId; + } + + /** + * Sets the well-known model name used by the runtime. + *

+ * Used to look up agent configuration (tools, prompts, reasoning behavior) and + * default token limits. Also used as the wire model when + * {@link #getWireModel()} is not set. Falls back to + * {@link SessionConfig#getModel()}. + * + * @param modelId + * the model ID + * @return this config for method chaining + */ + public ProviderConfig setModelId(String modelId) { + this.modelId = modelId; + return this; + } + + /** + * Gets the model name sent to the provider API for inference. + * + * @return the wire model name, or {@code null} if not set + */ + public String getWireModel() { + return wireModel; + } + + /** + * Sets the model name sent to the provider API for inference. + *

+ * Use this when the provider's model name (e.g. an Azure deployment name or a + * custom fine-tune name) differs from {@link #getModelId()}. Falls back to + * {@link #getModelId()}, then {@link SessionConfig#getModel()}. + * + * @param wireModel + * the wire model name + * @return this config for method chaining + */ + public ProviderConfig setWireModel(String wireModel) { + this.wireModel = wireModel; + return this; + } + + /** + * Gets the maximum prompt token override. + * + * @return an {@link java.util.OptionalInt} containing the max prompt tokens, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxPromptTokens() { + return maxPromptTokens == null ? OptionalInt.empty() : OptionalInt.of(maxPromptTokens); + } + + /** + * Sets the maximum prompt tokens override. + *

+ * Overrides the resolved model's default max prompt tokens. The runtime + * triggers conversation compaction before sending a request when the prompt + * (system message, history, tool definitions, user message) would exceed this + * limit. + * + * @param maxPromptTokens + * the max prompt tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxPromptTokens(int maxPromptTokens) { + this.maxPromptTokens = maxPromptTokens; + return this; + } + + /** + * Clears the maxPromptTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ProviderConfig clearMaxPromptTokens() { + this.maxPromptTokens = null; + return this; + } + + /** + * Gets the maximum output token override. + * + * @return an {@link java.util.OptionalInt} containing the max output tokens, or + * {@link java.util.OptionalInt#empty()} if not set + */ + @JsonIgnore + public OptionalInt getMaxOutputTokens() { + return maxOutputTokens == null ? OptionalInt.empty() : OptionalInt.of(maxOutputTokens); + } + + /** + * Sets the maximum output tokens override. + *

+ * Overrides the resolved model's default max output tokens. When hit, the model + * stops generating and returns a truncated response. + * + * @param maxOutputTokens + * the max output tokens + * @return this config for method chaining + */ + public ProviderConfig setMaxOutputTokens(int maxOutputTokens) { + this.maxOutputTokens = maxOutputTokens; + return this; + } + + /** + * Clears the maxOutputTokens setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ProviderConfig clearMaxOutputTokens() { + this.maxOutputTokens = null; + return this; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java new file mode 100644 index 000000000..72c9f6f47 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -0,0 +1,970 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import com.github.copilot.sdk.generated.SessionEvent; +import java.util.Optional; + +/** + * Configuration for resuming an existing Copilot session. + *

+ * This class provides options for configuring a resumed session, including tool + * registration, provider configuration, and streaming. All setter methods + * return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var config = new ResumeSessionConfig().setStreaming(true).setTools(List.of(myTool));
+ *
+ * var session = client.resumeSession(sessionId, config).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResumeSessionConfig { + + private String clientName; + private String model; + private List tools; + private SystemMessageConfig systemMessage; + private List availableTools; + private List excludedTools; + private ProviderConfig provider; + private Boolean enableSessionTelemetry; + private String reasoningEffort; + private ModelCapabilitiesOverride modelCapabilities; + private PermissionHandler onPermissionRequest; + private UserInputHandler onUserInputRequest; + private SessionHooks hooks; + private String workingDirectory; + private String configDir; + private Boolean enableConfigDiscovery; + private boolean disableResume; + private boolean streaming; + private Boolean includeSubAgentStreamingEvents; + private Map mcpServers; + private List customAgents; + private DefaultAgentConfig defaultAgent; + private String agent; + private List skillDirectories; + private List instructionDirectories; + private List disabledSkills; + private InfiniteSessionConfig infiniteSessions; + private Consumer onEvent; + private List commands; + private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; + private String gitHubToken; + private String remoteSession; + + /** + * Gets the AI model to use. + * + * @return the model name + */ + public String getModel() { + return model; + } + + /** + * Sets the AI model to use for the resumed session. + *

+ * Can change the model when resuming an existing session. + * + * @param model + * the model name + * @return this config for method chaining + */ + public ResumeSessionConfig setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the client name used to identify the application using the SDK. + * + * @return the client name, or {@code null} if not set + */ + public String getClientName() { + return clientName; + } + + /** + * Sets the client name to identify the application using the SDK. + *

+ * This value is included in the User-Agent header for API requests. + * + * @param clientName + * the client name + * @return this config for method chaining + */ + public ResumeSessionConfig setClientName(String clientName) { + this.clientName = clientName; + return this; + } + + /** + * Gets the custom tools for this session. + * + * @return the list of tool definitions + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets custom tools that the assistant can invoke during the session. + * + * @param tools + * the list of tool definitions + * @return this config for method chaining + * @see ToolDefinition + */ + public ResumeSessionConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the system message configuration. + * + * @return the system message config + */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** + * Sets the system message configuration. + *

+ * The system message controls the behavior and personality of the assistant. + * + * @param systemMessage + * the system message configuration + * @return this config for method chaining + * @see SystemMessageConfig + */ + public ResumeSessionConfig setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + return this; + } + + /** + * Gets the list of allowed tool names. + * + * @return the list of available tool names + */ + public List getAvailableTools() { + return availableTools == null ? null : Collections.unmodifiableList(availableTools); + } + + /** + * Sets the list of tool names that are allowed in this session. + *

+ * When specified, only tools in this list will be available to the assistant. + * Takes precedence over excluded tools. + * + * @param availableTools + * the list of allowed tool names + * @return this config for method chaining + */ + public ResumeSessionConfig setAvailableTools(List availableTools) { + this.availableTools = availableTools; + return this; + } + + /** + * Gets the list of excluded tool names. + * + * @return the list of excluded tool names + */ + public List getExcludedTools() { + return excludedTools == null ? null : Collections.unmodifiableList(excludedTools); + } + + /** + * Sets the list of tool names to exclude from this session. + *

+ * Tools in this list will not be available to the assistant. Ignored if + * available tools is specified. + * + * @param excludedTools + * the list of tool names to exclude + * @return this config for method chaining + */ + public ResumeSessionConfig setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + return this; + } + + /** + * Gets the custom API provider configuration. + * + * @return the provider configuration + */ + public ProviderConfig getProvider() { + return provider; + } + + /** + * Sets a custom API provider for BYOK scenarios. + * + * @param provider + * the provider configuration + * @return this config for method chaining + * @see ProviderConfig + */ + public ResumeSessionConfig setProvider(ProviderConfig provider) { + this.provider = provider; + return this; + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. This is independent of + * {@link com.github.copilot.sdk.json.CopilotClientOptions#getTelemetry() + * CopilotClientOptions.TelemetryConfig}, which configures OpenTelemetry export + * for observability. + * + * @return an {@link java.util.Optional} containing whether session telemetry is + * enabled, or {@link java.util.Optional#empty()} for the default + */ + @JsonIgnore + public Optional getEnableSessionTelemetry() { + return Optional.ofNullable(enableSessionTelemetry); + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. + * + * @param enableSessionTelemetry + * whether to enable session telemetry + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + return this; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + return this; + } + + /** + * Gets the reasoning effort level. + * + * @return the reasoning effort level ("low", "medium", "high", or "xhigh") + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level for models that support it. + *

+ * Valid values: "low", "medium", "high", "xhigh". + * + * @param reasoningEffort + * the reasoning effort level + * @return this config for method chaining + */ + public ResumeSessionConfig setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Gets the permission request handler. + * + * @return the permission handler + */ + public PermissionHandler getOnPermissionRequest() { + return onPermissionRequest; + } + + /** + * Sets a handler for permission requests from the assistant. + * + * @param onPermissionRequest + * the permission handler + * @return this config for method chaining + * @see PermissionHandler + */ + public ResumeSessionConfig setOnPermissionRequest(PermissionHandler onPermissionRequest) { + this.onPermissionRequest = onPermissionRequest; + return this; + } + + /** + * Gets the user input request handler. + * + * @return the user input handler + */ + public UserInputHandler getOnUserInputRequest() { + return onUserInputRequest; + } + + /** + * Sets a handler for user input requests from the agent. + * + * @param onUserInputRequest + * the user input handler + * @return this config for method chaining + * @see UserInputHandler + */ + public ResumeSessionConfig setOnUserInputRequest(UserInputHandler onUserInputRequest) { + this.onUserInputRequest = onUserInputRequest; + return this; + } + + /** + * Gets the hook handlers configuration. + * + * @return the session hooks + */ + public SessionHooks getHooks() { + return hooks; + } + + /** + * Sets hook handlers for session lifecycle events. + * + * @param hooks + * the hooks configuration + * @return this config for method chaining + * @see SessionHooks + */ + public ResumeSessionConfig setHooks(SessionHooks hooks) { + this.hooks = hooks; + return this; + } + + /** + * Gets the working directory for the session. + * + * @return the working directory path + */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** + * Sets the working directory for the session. + * + * @param workingDirectory + * the working directory path + * @return this config for method chaining + */ + public ResumeSessionConfig setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + /** + * Gets the configuration directory path. + * + * @return the configuration directory path + */ + public String getConfigDir() { + return configDir; + } + + /** + * Sets the configuration directory path. + *

+ * Override the default configuration directory location. + * + * @param configDir + * the configuration directory path + * @return this config for method chaining + */ + public ResumeSessionConfig setConfigDir(String configDir) { + this.configDir = configDir; + return this; + } + + /** + * Gets whether automatic configuration discovery is enabled. + * + * @return {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + */ + @JsonIgnore + public Optional getEnableConfigDiscovery() { + return Optional.ofNullable(enableConfigDiscovery); + } + + /** + * Sets whether to automatically discover MCP server configurations and skill + * directories from the working directory. + *

+ * When {@code true}, the CLI scans the working directory for {@code .mcp.json}, + * {@code .vscode/mcp.json} and skill directories, and merges them with + * explicitly provided configurations. + * + * @param enableConfigDiscovery + * {@code true} to enable discovery, {@code false} to disable, or + * {@code null} to use the runtime default + * @return this config for method chaining + */ + public ResumeSessionConfig setEnableConfigDiscovery(boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + return this; + } + + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + return this; + } + + /** + * Gets whether sub-agent streaming events are included. + * + * @return {@code true} to include sub-agent streaming events, {@code false} to + * suppress them, or {@code null} to use the runtime default + */ + @JsonIgnore + public Optional getIncludeSubAgentStreamingEvents() { + return Optional.ofNullable(includeSubAgentStreamingEvents); + } + + /** + * Sets whether to include sub-agent streaming events in the event stream. + * + * @param includeSubAgentStreamingEvents + * {@code true} to include streaming events, {@code false} to + * suppress + * @return this config for method chaining + */ + public ResumeSessionConfig setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + return this; + } + + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public ResumeSessionConfig clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + return this; + } + + /** + * Gets the model capabilities override. + * + * @return the model capabilities override, or {@code null} if not set + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities, deep-merged over runtime + * defaults. + * + * @param modelCapabilities + * the model capabilities override + * @return this config for method chaining + * @see ModelCapabilitiesOverride + */ + public ResumeSessionConfig setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + + /** + * Returns whether the resume event is disabled. + * + * @return {@code true} if the session.resume event is suppressed + */ + public boolean isDisableResume() { + return disableResume; + } + + /** + * Sets whether to disable the session.resume event. + *

+ * When true, the session.resume event is not emitted. + * + * @param disableResume + * {@code true} to suppress the resume event + * @return this config for method chaining + */ + public ResumeSessionConfig setDisableResume(boolean disableResume) { + this.disableResume = disableResume; + return this; + } + + /** + * Returns whether streaming is enabled. + * + * @return {@code true} if streaming is enabled + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Sets whether to enable streaming of response chunks. + * + * @param streaming + * {@code true} to enable streaming + * @return this config for method chaining + */ + public ResumeSessionConfig setStreaming(boolean streaming) { + this.streaming = streaming; + return this; + } + + /** + * Gets the MCP server configurations. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); + } + + /** + * Sets MCP (Model Context Protocol) server configurations. + * + * @param mcpServers + * the MCP servers configuration map + * @return this config for method chaining + */ + public ResumeSessionConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets the custom agent configurations. + * + * @return the list of custom agent configurations + */ + public List getCustomAgents() { + return customAgents == null ? null : Collections.unmodifiableList(customAgents); + } + + /** + * Sets custom agent configurations. + * + * @param customAgents + * the list of custom agent configurations + * @return this config for method chaining + * @see CustomAgentConfig + */ + public ResumeSessionConfig setCustomAgents(List customAgents) { + this.customAgents = customAgents; + return this; + } + + /** + * Gets the default agent configuration. + * + * @return the default agent configuration, or {@code null} if not set + */ + public DefaultAgentConfig getDefaultAgent() { + return defaultAgent; + } + + /** + * Sets the default agent configuration. + *

+ * Use {@link DefaultAgentConfig#setExcludedTools(List)} to hide specific tools + * from the default agent while keeping them available to custom sub-agents. + * + * @param defaultAgent + * the default agent configuration + * @return this config for method chaining + * @see DefaultAgentConfig + */ + public ResumeSessionConfig setDefaultAgent(DefaultAgentConfig defaultAgent) { + this.defaultAgent = defaultAgent; + return this; + } + + /** + * Gets the name of the custom agent to activate at session start. + * + * @return the agent name, or {@code null} if not set + */ + public String getAgent() { + return agent; + } + + /** + * Sets the name of the custom agent to activate when the session starts. + *

+ * Must match the name of one of the agents in {@link #setCustomAgents(List)}. + * + * @param agent + * the agent name to pre-select + * @return this config for method chaining + */ + public ResumeSessionConfig setAgent(String agent) { + this.agent = agent; + return this; + } + + /** + * Gets the skill directories. + * + * @return the list of skill directory paths + */ + public List getSkillDirectories() { + return skillDirectories == null ? null : Collections.unmodifiableList(skillDirectories); + } + + /** + * Sets directories containing skill definitions. + * + * @param skillDirectories + * the list of skill directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setSkillDirectories(List skillDirectories) { + this.skillDirectories = skillDirectories; + return this; + } + + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config for method chaining + */ + public ResumeSessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + + /** + * Gets the disabled skills. + * + * @return the list of disabled skill names + */ + public List getDisabledSkills() { + return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); + } + + /** + * Sets skills that should be disabled for this session. + * + * @param disabledSkills + * the list of skill names to disable + * @return this config for method chaining + */ + public ResumeSessionConfig setDisabledSkills(List disabledSkills) { + this.disabledSkills = disabledSkills; + return this; + } + + /** + * Gets the infinite session configuration. + * + * @return the infinite session config + */ + public InfiniteSessionConfig getInfiniteSessions() { + return infiniteSessions; + } + + /** + * Sets the infinite session configuration for persistent workspaces and + * automatic compaction. + * + * @param infiniteSessions + * the infinite session configuration + * @return this config for method chaining + * @see InfiniteSessionConfig + */ + public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSessions) { + this.infiniteSessions = infiniteSessions; + return this; + } + + /** + * Gets the event handler registered before the session.resume RPC is issued. + * + * @return the event handler, or {@code null} if not set + */ + public Consumer getOnEvent() { + return onEvent; + } + + /** + * Sets an event handler that is registered on the session before the + * {@code session.resume} RPC is issued. + *

+ * Equivalent to calling + * {@link com.github.copilot.sdk.CopilotSession#on(Consumer)} immediately after + * resumption, but executes earlier in the lifecycle so no events are missed. + * + * @param onEvent + * the event handler to register before session resumption + * @return this config for method chaining + */ + public ResumeSessionConfig setOnEvent(Consumer onEvent) { + this.onEvent = onEvent; + return this; + } + + /** + * Gets the slash commands registered for this session. + * + * @return the list of command definitions, or {@code null} + */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** + * Sets slash commands registered for this session. + *

+ * When the CLI has a TUI, each command appears as {@code /name} for the user to + * invoke. The handler is called when the user executes the command. + * + * @param commands + * the list of command definitions + * @return this config for method chaining + * @see CommandDefinition + */ + public ResumeSessionConfig setCommands(List commands) { + this.commands = commands; + return this; + } + + /** + * Gets the elicitation request handler. + * + * @return the elicitation handler, or {@code null} + */ + public ElicitationHandler getOnElicitationRequest() { + return onElicitationRequest; + } + + /** + * Sets a handler for elicitation requests from the server or MCP tools. + *

+ * When provided, the server will route elicitation requests to this handler and + * report elicitation as a supported capability. + * + * @param onElicitationRequest + * the elicitation handler + * @return this config for method chaining + * @see ElicitationHandler + */ + public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) { + this.onElicitationRequest = onElicitationRequest; + return this; + } + + /** + * Gets the exit-plan-mode request handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.8 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config for method chaining + * @see ExitPlanModeHandler + * @since 1.0.8 + */ + public ResumeSessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch request handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.8 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.8 + */ + public ResumeSessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + + /** + * Gets the GitHub token for per-session authentication. + * + * @return the GitHub token, or {@code null} if not set + * @since 1.3.0 + */ + public String getGitHubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token for per-session authentication. + *

+ * When provided, the runtime resolves this token into a full GitHub identity + * and stores it on the session for content exclusion, model routing, and quota + * checks. + * + * @param gitHubToken + * the GitHub token for per-session authentication + * @return this config for method chaining + * @since 1.3.0 + */ + public ResumeSessionConfig setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + return this; + } + + /** + * Gets the per-session remote behavior control. + *

+ * See {@link SessionConfig#getRemoteSession()} for details on possible values. + * + * @return the remote session mode, or {@code null} if not set + * @since 1.4.0 + */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the per-session remote behavior control. + *

+ * See {@link SessionConfig#setRemoteSession(String)} for details on possible + * values. + * + * @param remoteSession + * the remote session mode + * @return this config for method chaining + * @since 1.4.0 + */ + public ResumeSessionConfig setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + return this; + } + + /** + * Creates a shallow clone of this {@code ResumeSessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + @Override + public ResumeSessionConfig clone() { + ResumeSessionConfig copy = new ResumeSessionConfig(); + copy.clientName = this.clientName; + copy.model = this.model; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.enableSessionTelemetry = this.enableSessionTelemetry; + copy.reasoningEffort = this.reasoningEffort; + copy.modelCapabilities = this.modelCapabilities; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.configDir = this.configDir; + copy.enableConfigDiscovery = this.enableConfigDiscovery; + copy.disableResume = this.disableResume; + copy.streaming = this.streaming; + copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.defaultAgent = this.defaultAgent; + copy.agent = this.agent; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.infiniteSessions = this.infiniteSessions; + copy.onEvent = this.onEvent; + copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; + copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; + copy.gitHubToken = this.gitHubToken; + copy.remoteSession = this.remoteSession; + return copy; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java new file mode 100644 index 000000000..8aca77b7d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -0,0 +1,573 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for resuming an existing session. + *

+ * This is a low-level class for JSON-RPC communication. For resuming sessions, + * use + * {@link com.github.copilot.sdk.CopilotClient#resumeSession(String, ResumeSessionConfig)}. + * + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + * @see ResumeSessionConfig + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ResumeSessionRequest { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("clientName") + private String clientName; + + @JsonProperty("model") + private String model; + + @JsonProperty("reasoningEffort") + private String reasoningEffort; + + @JsonProperty("tools") + private List tools; + + @JsonProperty("systemMessage") + private SystemMessageConfig systemMessage; + + @JsonProperty("availableTools") + private List availableTools; + + @JsonProperty("excludedTools") + private List excludedTools; + + @JsonProperty("provider") + private ProviderConfig provider; + + @JsonProperty("enableSessionTelemetry") + private Boolean enableSessionTelemetry; + + @JsonProperty("requestPermission") + private Boolean requestPermission; + + @JsonProperty("requestUserInput") + private Boolean requestUserInput; + + @JsonProperty("hooks") + private Boolean hooks; + + @JsonProperty("workingDirectory") + private String workingDirectory; + + @JsonProperty("configDir") + private String configDir; + + @JsonProperty("enableConfigDiscovery") + private Boolean enableConfigDiscovery; + + @JsonProperty("disableResume") + private Boolean disableResume; + + @JsonProperty("streaming") + private Boolean streaming; + + @JsonProperty("includeSubAgentStreamingEvents") + private Boolean includeSubAgentStreamingEvents; + + @JsonProperty("mcpServers") + private Map mcpServers; + + @JsonProperty("envValueMode") + private String envValueMode; + + @JsonProperty("customAgents") + private List customAgents; + + @JsonProperty("defaultAgent") + private DefaultAgentConfig defaultAgent; + + @JsonProperty("agent") + private String agent; + + @JsonProperty("skillDirectories") + private List skillDirectories; + + @JsonProperty("instructionDirectories") + private List instructionDirectories; + + @JsonProperty("disabledSkills") + private List disabledSkills; + + @JsonProperty("infiniteSessions") + private InfiniteSessionConfig infiniteSessions; + + @JsonProperty("commands") + private List commands; + + @JsonProperty("requestElicitation") + private Boolean requestElicitation; + + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + + @JsonProperty("modelCapabilities") + private ModelCapabilitiesOverride modelCapabilities; + + @JsonProperty("gitHubToken") + private String gitHubToken; + + @JsonProperty("remoteSession") + private String remoteSession; + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the client name. @return the client name */ + public String getClientName() { + return clientName; + } + + /** Sets the client name. @param clientName the client name */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + /** Gets the model name. @return the model */ + public String getModel() { + return model; + } + + /** Sets the model name. @param model the model */ + public void setModel(String model) { + this.model = model; + } + + /** Gets the reasoning effort. @return the reasoning effort level */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort. @param reasoningEffort the reasoning effort level + */ + public void setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } + + /** Gets the tools. @return the tool definitions */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** Sets the tools. @param tools the tool definitions */ + public void setTools(List tools) { + this.tools = tools; + } + + /** Gets the system message config. @return the system message config */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** + * Sets the system message config. @param systemMessage the system message + * config + */ + public void setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + } + + /** Gets available tools. @return the available tool names */ + public List getAvailableTools() { + return availableTools == null ? null : Collections.unmodifiableList(availableTools); + } + + /** Sets available tools. @param availableTools the available tool names */ + public void setAvailableTools(List availableTools) { + this.availableTools = availableTools; + } + + /** Gets excluded tools. @return the excluded tool names */ + public List getExcludedTools() { + return excludedTools == null ? null : Collections.unmodifiableList(excludedTools); + } + + /** Sets excluded tools. @param excludedTools the excluded tool names */ + public void setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + } + + /** Gets the provider config. @return the provider */ + public ProviderConfig getProvider() { + return provider; + } + + /** Sets the provider config. @param provider the provider */ + public void setProvider(ProviderConfig provider) { + this.provider = provider; + } + + /** Gets enable session telemetry flag. @return the flag */ + public Boolean getEnableSessionTelemetry() { + return enableSessionTelemetry; + } + + /** + * Sets enable session telemetry flag. @param enableSessionTelemetry the flag + */ + public void setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + */ + public void clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + } + + /** Gets request permission flag. @return the flag */ + public Boolean getRequestPermission() { + return requestPermission; + } + + /** Sets request permission flag. @param requestPermission the flag */ + public void setRequestPermission(boolean requestPermission) { + this.requestPermission = requestPermission; + } + + /** + * Clears the requestPermission setting, reverting to the default behavior. + */ + public void clearRequestPermission() { + this.requestPermission = null; + } + + /** Gets request user input flag. @return the flag */ + public Boolean getRequestUserInput() { + return requestUserInput; + } + + /** Sets request user input flag. @param requestUserInput the flag */ + public void setRequestUserInput(boolean requestUserInput) { + this.requestUserInput = requestUserInput; + } + + /** + * Clears the requestUserInput setting, reverting to the default behavior. + */ + public void clearRequestUserInput() { + this.requestUserInput = null; + } + + /** Gets hooks flag. @return the flag */ + public Boolean getHooks() { + return hooks; + } + + /** Sets hooks flag. @param hooks the flag */ + public void setHooks(boolean hooks) { + this.hooks = hooks; + } + + /** + * Clears the hooks setting, reverting to the default behavior. + */ + public void clearHooks() { + this.hooks = null; + } + + /** Gets working directory. @return the working directory */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** Sets working directory. @param workingDirectory the working directory */ + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** Gets config directory. @return the config directory */ + public String getConfigDir() { + return configDir; + } + + /** Sets config directory. @param configDir the config directory */ + public void setConfigDir(String configDir) { + this.configDir = configDir; + } + + /** Gets enable config discovery flag. @return the flag */ + public Boolean getEnableConfigDiscovery() { + return enableConfigDiscovery; + } + + /** Sets enable config discovery flag. @param enableConfigDiscovery the flag */ + public void setEnableConfigDiscovery(boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + } + + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + */ + public void clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + } + + /** Gets disable resume flag. @return the flag */ + public Boolean getDisableResume() { + return disableResume; + } + + /** Sets disable resume flag. @param disableResume the flag */ + public void setDisableResume(boolean disableResume) { + this.disableResume = disableResume; + } + + /** + * Clears the disableResume setting, reverting to the default behavior. + */ + public void clearDisableResume() { + this.disableResume = null; + } + + /** Gets streaming flag. @return the flag */ + public Boolean getStreaming() { + return streaming; + } + + /** Sets streaming flag. @param streaming the flag */ + public void setStreaming(boolean streaming) { + this.streaming = streaming; + } + + /** + * Clears the streaming setting, reverting to the default behavior. + */ + public void clearStreaming() { + this.streaming = null; + } + + /** Gets include sub-agent streaming events flag. @return the flag */ + public Boolean getIncludeSubAgentStreamingEvents() { + return includeSubAgentStreamingEvents; + } + + /** + * Sets include sub-agent streaming events flag. @param + * includeSubAgentStreamingEvents the flag + */ + public void setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + } + + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + */ + public void clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + } + + /** Gets MCP servers. @return the servers map */ + public Map getMcpServers() { + return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); + } + + /** Sets MCP servers. @param mcpServers the servers map */ + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } + + /** Gets MCP environment variable value mode. @return the mode */ + public String getEnvValueMode() { + return envValueMode; + } + + /** Sets MCP environment variable value mode. @param envValueMode the mode */ + public void setEnvValueMode(String envValueMode) { + this.envValueMode = envValueMode; + } + + /** Gets custom agents. @return the agents */ + public List getCustomAgents() { + return customAgents == null ? null : Collections.unmodifiableList(customAgents); + } + + /** Sets custom agents. @param customAgents the agents */ + public void setCustomAgents(List customAgents) { + this.customAgents = customAgents; + } + + /** Gets the default agent config. @return the default agent config */ + public DefaultAgentConfig getDefaultAgent() { + return defaultAgent; + } + + /** + * Sets the default agent config. @param defaultAgent the default agent config + */ + public void setDefaultAgent(DefaultAgentConfig defaultAgent) { + this.defaultAgent = defaultAgent; + } + + /** Gets the pre-selected agent name. @return the agent name */ + public String getAgent() { + return agent; + } + + /** Sets the pre-selected agent name. @param agent the agent name */ + public void setAgent(String agent) { + this.agent = agent; + } + + /** Gets skill directories. @return the directories */ + public List getSkillDirectories() { + return skillDirectories == null ? null : Collections.unmodifiableList(skillDirectories); + } + + /** Sets skill directories. @param skillDirectories the directories */ + public void setSkillDirectories(List skillDirectories) { + this.skillDirectories = skillDirectories; + } + + /** Gets instruction directories. @return the instruction directories */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets instruction directories. @param instructionDirectories the directories + */ + public void setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + } + + /** Gets disabled skills. @return the disabled skill names */ + public List getDisabledSkills() { + return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); + } + + /** Sets disabled skills. @param disabledSkills the skill names to disable */ + public void setDisabledSkills(List disabledSkills) { + this.disabledSkills = disabledSkills; + } + + /** Gets infinite sessions config. @return the infinite sessions config */ + public InfiniteSessionConfig getInfiniteSessions() { + return infiniteSessions; + } + + /** + * Sets infinite sessions config. @param infiniteSessions the infinite sessions + * config + */ + public void setInfiniteSessions(InfiniteSessionConfig infiniteSessions) { + this.infiniteSessions = infiniteSessions; + } + + /** Gets the commands wire definitions. @return the commands */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** Sets the commands wire definitions. @param commands the commands */ + public void setCommands(List commands) { + this.commands = commands; + } + + /** Gets the requestElicitation flag. @return the flag */ + public Boolean getRequestElicitation() { + return requestElicitation; + } + + /** Sets the requestElicitation flag. @param requestElicitation the flag */ + public void setRequestElicitation(boolean requestElicitation) { + this.requestElicitation = requestElicitation; + } + + /** + * Clears the requestElicitation setting, reverting to the default behavior. + */ + public void clearRequestElicitation() { + this.requestElicitation = null; + } + + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + + /** Gets the model capabilities override. @return the override */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets the model capabilities override. @param modelCapabilities the override + */ + public void setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + } + + /** Gets the GitHub token for per-session authentication. @return the token */ + public String getGitHubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token for per-session authentication. @param gitHubToken the + * token + */ + public void setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + } + + /** Gets the remote session mode. @return the remote session mode */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the remote session mode. @param remoteSession the remote session mode + */ + public void setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java new file mode 100644 index 000000000..8349c5d30 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ResumeSessionResponse.java @@ -0,0 +1,22 @@ +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from resuming a session. + * + * @param sessionId + * the session ID + * @param workspacePath + * the workspace path, or {@code null} if infinite sessions are + * disabled + * @param capabilities + * the capabilities reported by the host, or {@code null} + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ResumeSessionResponse(@JsonProperty("sessionId") String sessionId, + @JsonProperty("workspacePath") String workspacePath, + @JsonProperty("capabilities") SessionCapabilities capabilities) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SectionOverride.java b/java/src/main/java/com/github/copilot/sdk/json/SectionOverride.java new file mode 100644 index 000000000..40a58449d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SectionOverride.java @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Override operation for a single system prompt section in + * {@link SystemMessageMode#CUSTOMIZE} mode. + *

+ * Each {@code SectionOverride} describes how one named section of the default + * system prompt should be modified. The section name keys come from + * {@link SystemPromptSections}. + * + *

Static override example

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE).setSections(Map.of(
+ * 		SystemPromptSections.TONE,
+ * 		new SectionOverride().setAction(SectionOverrideAction.REPLACE).setContent("Be concise and formal."),
+ * 		SystemPromptSections.CODE_CHANGE_RULES, new SectionOverride().setAction(SectionOverrideAction.REMOVE)));
+ * }
+ * + *

Transform callback example

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
+ * 		.setSections(Map.of(SystemPromptSections.IDENTITY, new SectionOverride().setTransform(
+ * 				content -> CompletableFuture.completedFuture(content + "\nAlways end replies with DONE."))));
+ * }
+ * + * @see SystemMessageConfig + * @see SectionOverrideAction + * @see SystemPromptSections + * @since 1.2.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SectionOverride { + + @JsonProperty("action") + private SectionOverrideAction action; + + @JsonProperty("content") + private String content; + + /** + * Transform callback invoked by the SDK when the CLI requests a + * {@code systemMessage.transform} RPC call. + *

+ * The function receives the current section content and returns the transformed + * content wrapped in a {@link CompletableFuture}. When a transform is set, it + * takes precedence over {@link #action}; the wire representation uses + * {@link SectionOverrideAction#TRANSFORM} automatically. + *

+ * This field is not serialized β€” it is handled entirely by the SDK. + */ + @JsonIgnore + private Function> transform; + + /** + * Gets the override action. + * + * @return the action, or {@code null} if a transform callback is set + */ + public SectionOverrideAction getAction() { + return action; + } + + /** + * Sets the override action. + * + * @param action + * the action to perform on this section + * @return this override for method chaining + */ + public SectionOverride setAction(SectionOverrideAction action) { + this.action = action; + return this; + } + + /** + * Gets the content for the override. + * + * @return the content, or {@code null} + */ + public String getContent() { + return content; + } + + /** + * Sets the content for the override. + *

+ * Used for {@link SectionOverrideAction#REPLACE}, + * {@link SectionOverrideAction#APPEND}, and + * {@link SectionOverrideAction#PREPEND}. Ignored for + * {@link SectionOverrideAction#REMOVE}. + * + * @param content + * the content string + * @return this override for method chaining + */ + public SectionOverride setContent(String content) { + this.content = content; + return this; + } + + /** + * Gets the transform callback. + * + * @return the transform function, or {@code null} if not set + */ + public Function> getTransform() { + return transform; + } + + /** + * Sets the transform callback for this section. + *

+ * The function receives the current section content as a {@code String} and + * returns the transformed content via a {@link CompletableFuture}. When set, + * this takes precedence over {@link #action}. + * + * @param transform + * a function that transforms the section content + * @return this override for method chaining + */ + public SectionOverride setTransform(Function> transform) { + this.transform = transform; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SectionOverrideAction.java b/java/src/main/java/com/github/copilot/sdk/json/SectionOverrideAction.java new file mode 100644 index 000000000..2d179f753 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SectionOverrideAction.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Specifies the operation to perform on a system prompt section in + * {@link SystemMessageMode#CUSTOMIZE} mode. + * + * @see SectionOverride + * @see SystemMessageConfig + * @since 1.2.0 + */ +public enum SectionOverrideAction { + + /** Replace the section content entirely. */ + REPLACE("replace"), + + /** Remove the section from the prompt. */ + REMOVE("remove"), + + /** Append content after the existing section. */ + APPEND("append"), + + /** Prepend content before the existing section. */ + PREPEND("prepend"), + + /** + * Transform the section content via a callback. + *

+ * When this action is used, the {@link SectionOverride#getTransform()} callback + * must be set. The SDK will not serialize this action over the wire directly; + * instead it registers a {@code systemMessage.transform} RPC handler. + */ + TRANSFORM("transform"); + + private final String value; + + SectionOverrideAction(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this action. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java b/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java new file mode 100644 index 000000000..2ef39770f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SendMessageRequest.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal request object for sending a message to a session. + *

+ * This is a low-level class for JSON-RPC communication. For sending messages, + * use {@link com.github.copilot.sdk.CopilotSession#send(String)} or + * {@link com.github.copilot.sdk.CopilotSession#sendAndWait(String)}. + * + * @see com.github.copilot.sdk.CopilotSession + * @see MessageOptions + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SendMessageRequest { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("prompt") + private String prompt; + + @JsonProperty("attachments") + private List attachments; + + @JsonProperty("mode") + private String mode; + + @JsonProperty("requestHeaders") + private Map requestHeaders; + + /** Gets the session ID. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** Gets the message prompt. @return the prompt text */ + public String getPrompt() { + return prompt; + } + + /** Sets the message prompt. @param prompt the prompt text */ + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + /** Gets the attachments. @return the list of attachments */ + public List getAttachments() { + return attachments == null ? null : Collections.unmodifiableList(attachments); + } + + /** Sets the attachments. @param attachments the list of attachments */ + public void setAttachments(List attachments) { + this.attachments = attachments; + } + + /** Gets the mode. @return the message mode */ + public String getMode() { + return mode; + } + + /** Sets the mode. @param mode the message mode */ + public void setMode(String mode) { + this.mode = mode; + } + + /** Gets the per-turn request headers. @return the headers map */ + public Map getRequestHeaders() { + return requestHeaders == null ? null : Collections.unmodifiableMap(requestHeaders); + } + + /** Sets the per-turn request headers. @param requestHeaders the headers map */ + public void setRequestHeaders(Map requestHeaders) { + this.requestHeaders = requestHeaders; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java b/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java new file mode 100644 index 000000000..7d79a7a2b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SendMessageResponse.java @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Internal response object from sending a message. + *

+ * This is a low-level class for JSON-RPC communication containing the message + * ID assigned by the server. + * + * @see SendMessageRequest + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SendMessageResponse( + /** The message ID assigned by the server. */ + @JsonProperty("messageId") String messageId) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java b/java/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java new file mode 100644 index 000000000..4eb4fc025 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionCapabilities.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Represents the capabilities reported by the host for a session. + *

+ * Capabilities are populated from the session create/resume response and + * updated in real time via {@code capabilities.changed} events. + * + * @since 1.0.0 + */ +public class SessionCapabilities { + + private SessionUiCapabilities ui; + + /** + * Gets the UI-related capabilities. + * + * @return the UI capabilities, or {@code null} if not reported + */ + public SessionUiCapabilities getUi() { + return ui; + } + + /** + * Sets the UI-related capabilities. + * + * @param ui + * the UI capabilities + * @return this instance for method chaining + */ + public SessionCapabilities setUi(SessionUiCapabilities ui) { + this.ui = ui; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java new file mode 100644 index 000000000..ddf06cca7 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -0,0 +1,1065 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import com.github.copilot.sdk.generated.SessionEvent; +import java.util.Optional; + +/** + * Configuration for creating a new Copilot session. + *

+ * This class provides options for customizing session behavior, including model + * selection, tool registration, system message customization, and more. All + * setter methods return {@code this} for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * var config = new SessionConfig().setModel("gpt-5").setStreaming(true).setSystemMessage(
+ * 		new SystemMessageConfig().setMode(SystemMessageMode.APPEND).setContent("Be concise in your responses."));
+ *
+ * var session = client.createSession(config).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#createSession(SessionConfig) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionConfig { + + private String sessionId; + private String clientName; + private String model; + private String reasoningEffort; + private List tools; + private SystemMessageConfig systemMessage; + private List availableTools; + private List excludedTools; + private ProviderConfig provider; + private Boolean enableSessionTelemetry; + private PermissionHandler onPermissionRequest; + private UserInputHandler onUserInputRequest; + private SessionHooks hooks; + private String workingDirectory; + private boolean streaming; + private Boolean includeSubAgentStreamingEvents; + private Map mcpServers; + private List customAgents; + private DefaultAgentConfig defaultAgent; + private String agent; + private InfiniteSessionConfig infiniteSessions; + private List skillDirectories; + private List instructionDirectories; + private List disabledSkills; + private String configDir; + private Boolean enableConfigDiscovery; + private ModelCapabilitiesOverride modelCapabilities; + private Consumer onEvent; + private List commands; + private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; + private String gitHubToken; + private String remoteSession; + private CloudSessionOptions cloud; + + /** + * Gets the custom session ID. + * + * @return the session ID, or {@code null} to generate automatically + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets a custom session ID. + *

+ * If not provided, a unique session ID will be generated automatically. + * + * @param sessionId + * the custom session ID + * @return this config instance for method chaining + */ + public SessionConfig setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the client name used to identify the application using the SDK. + * + * @return the client name, or {@code null} if not set + */ + public String getClientName() { + return clientName; + } + + /** + * Sets the client name to identify the application using the SDK. + *

+ * This value is included in the User-Agent header for API requests. + * + * @param clientName + * the client name + * @return this config instance for method chaining + */ + public SessionConfig setClientName(String clientName) { + this.clientName = clientName; + return this; + } + + /** + * Gets the AI model to use. + * + * @return the model name + */ + public String getModel() { + return model; + } + + /** + * Sets the AI model to use for this session. + *

+ * Examples: "gpt-5", "claude-sonnet-4.5", "o3-mini". + * + * @param model + * the model name + * @return this config instance for method chaining + */ + public SessionConfig setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the reasoning effort level. + * + * @return the reasoning effort level ("low", "medium", "high", or "xhigh") + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level for models that support it. + *

+ * Valid values: "low", "medium", "high", "xhigh". Only applies to models where + * {@code capabilities.supports.reasoningEffort} is true. + * + * @param reasoningEffort + * the reasoning effort level + * @return this config instance for method chaining + */ + public SessionConfig setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Gets the custom tools for this session. + * + * @return the list of tool definitions + */ + public List getTools() { + return tools == null ? null : Collections.unmodifiableList(tools); + } + + /** + * Sets custom tools that the assistant can invoke during the session. + *

+ * Tools allow the assistant to call back into your application to perform + * actions or retrieve information. + * + * @param tools + * the list of tool definitions + * @return this config instance for method chaining + * @see ToolDefinition + */ + public SessionConfig setTools(List tools) { + this.tools = tools; + return this; + } + + /** + * Gets the system message configuration. + * + * @return the system message config + */ + public SystemMessageConfig getSystemMessage() { + return systemMessage; + } + + /** + * Sets the system message configuration. + *

+ * The system message controls the behavior and personality of the assistant. + * Use {@link com.github.copilot.sdk.SystemMessageMode#APPEND} to add + * instructions while preserving default behavior, or + * {@link com.github.copilot.sdk.SystemMessageMode#REPLACE} to fully customize. + * + * @param systemMessage + * the system message configuration + * @return this config instance for method chaining + * @see SystemMessageConfig + */ + public SessionConfig setSystemMessage(SystemMessageConfig systemMessage) { + this.systemMessage = systemMessage; + return this; + } + + /** + * Gets the list of allowed tool names. + * + * @return the list of available tool names + */ + public List getAvailableTools() { + return availableTools == null ? null : Collections.unmodifiableList(availableTools); + } + + /** + * Sets the list of tool names that are allowed in this session. + *

+ * When specified, only tools in this list will be available to the assistant. + * + * @param availableTools + * the list of allowed tool names + * @return this config instance for method chaining + */ + public SessionConfig setAvailableTools(List availableTools) { + this.availableTools = availableTools; + return this; + } + + /** + * Gets the list of excluded tool names. + * + * @return the list of excluded tool names + */ + public List getExcludedTools() { + return excludedTools == null ? null : Collections.unmodifiableList(excludedTools); + } + + /** + * Sets the list of tool names to exclude from this session. + *

+ * Tools in this list will not be available to the assistant. + * + * @param excludedTools + * the list of tool names to exclude + * @return this config instance for method chaining + */ + public SessionConfig setExcludedTools(List excludedTools) { + this.excludedTools = excludedTools; + return this; + } + + /** + * Gets the custom API provider configuration. + * + * @return the provider configuration + */ + public ProviderConfig getProvider() { + return provider; + } + + /** + * Sets a custom API provider for BYOK (Bring Your Own Key) scenarios. + *

+ * This allows using your own OpenAI, Azure OpenAI, or other compatible API + * endpoints instead of the default Copilot backend. + * + * @param provider + * the provider configuration + * @return this config instance for method chaining + * @see ProviderConfig + */ + public SessionConfig setProvider(ProviderConfig provider) { + this.provider = provider; + return this; + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. This is independent of + * {@link com.github.copilot.sdk.json.CopilotClientOptions#getTelemetry() + * CopilotClientOptions.TelemetryConfig}, which configures OpenTelemetry export + * for observability. + * + * @return an {@link java.util.Optional} containing whether session telemetry is + * enabled, or {@link java.util.Optional#empty()} for the default + */ + @JsonIgnore + public Optional getEnableSessionTelemetry() { + return Optional.ofNullable(enableSessionTelemetry); + } + + /** + * Enables or disables internal session telemetry for this session. When + * {@code false}, disables session telemetry. When unset (the default) or + * {@code true}, telemetry is enabled for GitHub-authenticated sessions. When a + * custom {@link ProviderConfig} (BYOK) is configured, session telemetry is + * always disabled regardless of this setting. + * + * @param enableSessionTelemetry + * whether to enable session telemetry + * @return this config instance for method chaining + */ + public SessionConfig setEnableSessionTelemetry(boolean enableSessionTelemetry) { + this.enableSessionTelemetry = enableSessionTelemetry; + return this; + } + + /** + * Clears the enableSessionTelemetry setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearEnableSessionTelemetry() { + this.enableSessionTelemetry = null; + return this; + } + + /** + * Gets the permission request handler. + * + * @return the permission handler + */ + public PermissionHandler getOnPermissionRequest() { + return onPermissionRequest; + } + + /** + * Sets a handler for permission requests from the assistant. + *

+ * When the assistant needs permission to perform certain actions, this handler + * will be invoked to approve or deny the request. + * + * @param onPermissionRequest + * the permission handler + * @return this config instance for method chaining + * @see PermissionHandler + */ + public SessionConfig setOnPermissionRequest(PermissionHandler onPermissionRequest) { + this.onPermissionRequest = onPermissionRequest; + return this; + } + + /** + * Gets the user input request handler. + * + * @return the user input handler + */ + public UserInputHandler getOnUserInputRequest() { + return onUserInputRequest; + } + + /** + * Sets a handler for user input requests from the agent. + *

+ * When provided, enables the ask_user tool for the agent to request user input. + * + * @param onUserInputRequest + * the user input handler + * @return this config instance for method chaining + * @see UserInputHandler + */ + public SessionConfig setOnUserInputRequest(UserInputHandler onUserInputRequest) { + this.onUserInputRequest = onUserInputRequest; + return this; + } + + /** + * Gets the hook handlers configuration. + * + * @return the session hooks + */ + public SessionHooks getHooks() { + return hooks; + } + + /** + * Sets hook handlers for session lifecycle events. + *

+ * Hooks allow you to intercept and modify tool execution behavior. + * + * @param hooks + * the hooks configuration + * @return this config instance for method chaining + * @see SessionHooks + */ + public SessionConfig setHooks(SessionHooks hooks) { + this.hooks = hooks; + return this; + } + + /** + * Gets the working directory for the session. + * + * @return the working directory path + */ + public String getWorkingDirectory() { + return workingDirectory; + } + + /** + * Sets the working directory for the session. + * + * @param workingDirectory + * the working directory path + * @return this config instance for method chaining + */ + public SessionConfig setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + return this; + } + + /** + * Returns whether streaming is enabled. + * + * @return {@code true} if streaming is enabled + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Sets whether to enable streaming of response chunks. + *

+ * When enabled, the session will emit {@code AssistantMessageDeltaEvent} events + * as the response is generated, allowing for real-time display of partial + * responses. + * + * @param streaming + * {@code true} to enable streaming + * @return this config instance for method chaining + */ + public SessionConfig setStreaming(boolean streaming) { + this.streaming = streaming; + return this; + } + + /** + * Gets the MCP server configurations. + * + * @return the MCP servers map + */ + public Map getMcpServers() { + return mcpServers == null ? null : Collections.unmodifiableMap(mcpServers); + } + + /** + * Sets MCP (Model Context Protocol) server configurations. + *

+ * MCP servers extend the assistant's capabilities by providing additional + * context sources and tools. + * + * @param mcpServers + * the MCP servers configuration map + * @return this config instance for method chaining + */ + public SessionConfig setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + return this; + } + + /** + * Gets the custom agent configurations. + * + * @return the list of custom agent configurations + */ + public List getCustomAgents() { + return customAgents == null ? null : Collections.unmodifiableList(customAgents); + } + + /** + * Sets custom agent configurations. + *

+ * Custom agents allow extending the assistant with specialized behaviors and + * capabilities. + * + * @param customAgents + * the list of custom agent configurations + * @return this config instance for method chaining + * @see CustomAgentConfig + */ + public SessionConfig setCustomAgents(List customAgents) { + this.customAgents = customAgents; + return this; + } + + /** + * Gets the default agent configuration. + * + * @return the default agent configuration, or {@code null} if not set + */ + public DefaultAgentConfig getDefaultAgent() { + return defaultAgent; + } + + /** + * Sets the default agent configuration. + *

+ * Use {@link DefaultAgentConfig#setExcludedTools(List)} to hide specific tools + * from the default agent while keeping them available to custom sub-agents. + * + * @param defaultAgent + * the default agent configuration + * @return this config instance for method chaining + * @see DefaultAgentConfig + */ + public SessionConfig setDefaultAgent(DefaultAgentConfig defaultAgent) { + this.defaultAgent = defaultAgent; + return this; + } + + /** + * Gets the name of the custom agent to activate at session start. + * + * @return the agent name, or {@code null} if not set + */ + public String getAgent() { + return agent; + } + + /** + * Sets the name of the custom agent to activate when the session starts. + *

+ * Must match the name of one of the agents in {@link #setCustomAgents(List)}. + * + * @param agent + * the agent name to pre-select + * @return this config instance for method chaining + */ + public SessionConfig setAgent(String agent) { + this.agent = agent; + return this; + } + + /** + * Gets the infinite sessions configuration. + * + * @return the infinite sessions config + */ + public InfiniteSessionConfig getInfiniteSessions() { + return infiniteSessions; + } + + /** + * Sets the infinite session configuration for persistent workspaces and + * automatic compaction. + *

+ * When enabled (default), sessions automatically manage context limits and + * persist state to a workspace directory. The workspace contains checkpoints/, + * plan.md, and files/ subdirectories. + * + * @param infiniteSessions + * the infinite sessions configuration + * @return this config instance for method chaining + * @see InfiniteSessionConfig + */ + public SessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSessions) { + this.infiniteSessions = infiniteSessions; + return this; + } + + /** + * Gets the skill directories. + * + * @return the list of skill directory paths + */ + public List getSkillDirectories() { + return skillDirectories == null ? null : Collections.unmodifiableList(skillDirectories); + } + + /** + * Sets the skill directories for loading custom skills. + *

+ * Skills are loaded from SKILL.md files in subdirectories of the specified + * directories. Each skill subdirectory should contain a SKILL.md file with YAML + * frontmatter defining the skill metadata. + * + * @param skillDirectories + * the list of skill directory paths + * @return this config instance for method chaining + */ + public SessionConfig setSkillDirectories(List skillDirectories) { + this.skillDirectories = skillDirectories; + return this; + } + + /** + * Gets the additional directories to search for custom instruction files. + * + * @return the list of instruction directory paths + */ + public List getInstructionDirectories() { + return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories); + } + + /** + * Sets additional directories to search for custom instruction files. + * + * @param instructionDirectories + * the list of instruction directory paths + * @return this config instance for method chaining + */ + public SessionConfig setInstructionDirectories(List instructionDirectories) { + this.instructionDirectories = instructionDirectories; + return this; + } + + /** + * Gets the disabled skill names. + * + * @return the list of disabled skill names + */ + public List getDisabledSkills() { + return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills); + } + + /** + * Sets the list of skill names to disable. + *

+ * Skills in this list will not be applied to the session, even if they are + * found in the skill directories. + * + * @param disabledSkills + * the list of skill names to disable + * @return this config instance for method chaining + */ + public SessionConfig setDisabledSkills(List disabledSkills) { + this.disabledSkills = disabledSkills; + return this; + } + + /** + * Gets the custom configuration directory. + * + * @return the config directory path + */ + public String getConfigDir() { + return configDir; + } + + /** + * Sets a custom configuration directory for the session. + *

+ * This allows using a specific directory for session configuration instead of + * the default location. + * + * @param configDir + * the configuration directory path + * @return this config instance for method chaining + */ + public SessionConfig setConfigDir(String configDir) { + this.configDir = configDir; + return this; + } + + /** + * Gets whether automatic configuration discovery is enabled. + * + * @return an {@link java.util.Optional} containing {@code true} to enable + * discovery or {@code false} to disable, or + * {@link java.util.Optional#empty()} to use the runtime default + */ + @JsonIgnore + public Optional getEnableConfigDiscovery() { + return Optional.ofNullable(enableConfigDiscovery); + } + + /** + * Sets whether to automatically discover MCP server configurations and skill + * directories from the working directory. + *

+ * When {@code true}, the CLI scans the working directory for {@code .mcp.json}, + * {@code .vscode/mcp.json} and skill directories, and merges them with + * explicitly provided {@link #setMcpServers(Map)} and + * {@link #setSkillDirectories(List)}, with explicit values taking precedence on + * name collision. + * + * @param enableConfigDiscovery + * {@code true} to enable discovery, {@code false} to disable + * @return this config instance for method chaining + */ + public SessionConfig setEnableConfigDiscovery(boolean enableConfigDiscovery) { + this.enableConfigDiscovery = enableConfigDiscovery; + return this; + } + + /** + * Clears the enableConfigDiscovery setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearEnableConfigDiscovery() { + this.enableConfigDiscovery = null; + return this; + } + + /** + * Gets whether sub-agent streaming events are included. + * + * @return an {@link java.util.Optional} containing {@code true} to include + * sub-agent streaming events or {@code false} to suppress them, or + * {@link java.util.Optional#empty()} to use the runtime default + */ + @JsonIgnore + public Optional getIncludeSubAgentStreamingEvents() { + return Optional.ofNullable(includeSubAgentStreamingEvents); + } + + /** + * Sets whether to include sub-agent streaming events in the event stream. + *

+ * When {@code true}, streaming delta events from sub-agents (e.g., + * {@code assistant.message_delta} with {@code agentId} set) are forwarded to + * this connection. When {@code false}, only non-streaming sub-agent events and + * {@code subagent.*} lifecycle events are forwarded; streaming deltas from + * sub-agents are suppressed. Default: {@code true}. + * + * @param includeSubAgentStreamingEvents + * {@code true} to include streaming events, {@code false} to + * suppress + * @return this config instance for method chaining + */ + public SessionConfig setIncludeSubAgentStreamingEvents(boolean includeSubAgentStreamingEvents) { + this.includeSubAgentStreamingEvents = includeSubAgentStreamingEvents; + return this; + } + + /** + * Clears the includeSubAgentStreamingEvents setting, reverting to the default + * behavior. + * + * @return this instance for method chaining + */ + public SessionConfig clearIncludeSubAgentStreamingEvents() { + this.includeSubAgentStreamingEvents = null; + return this; + } + + /** + * Gets the model capabilities override. + * + * @return the model capabilities override, or {@code null} if not set + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities, deep-merged over runtime + * defaults. + *

+ * Use this to override specific model capabilities (such as vision support) for + * this session. Only non-null fields in the override are applied; unset fields + * retain their runtime defaults. + * + * @param modelCapabilities + * the model capabilities override + * @return this config instance for method chaining + * @see ModelCapabilitiesOverride + */ + public SessionConfig setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + + /** + * Gets the event handler registered before the session.create RPC is issued. + * + * @return the event handler, or {@code null} if not set + */ + public Consumer getOnEvent() { + return onEvent; + } + + /** + * Sets an event handler that is registered on the session before the + * {@code session.create} RPC is issued. + *

+ * Equivalent to calling + * {@link com.github.copilot.sdk.CopilotSession#on(Consumer)} immediately after + * creation, but executes earlier in the lifecycle so no events are missed. + * Using this property rather than {@code CopilotSession.on()} guarantees that + * early events emitted by the CLI during session creation (e.g. + * {@code session.start}) are delivered to the handler. + * + * @param onEvent + * the event handler to register before session creation + * @return this config instance for method chaining + */ + public SessionConfig setOnEvent(Consumer onEvent) { + this.onEvent = onEvent; + return this; + } + + /** + * Gets the slash commands registered for this session. + * + * @return the list of command definitions, or {@code null} + */ + public List getCommands() { + return commands == null ? null : Collections.unmodifiableList(commands); + } + + /** + * Sets slash commands registered for this session. + *

+ * When the CLI has a TUI, each command appears as {@code /name} for the user to + * invoke. The handler is called when the user executes the command. + * + * @param commands + * the list of command definitions + * @return this config instance for method chaining + * @see CommandDefinition + */ + public SessionConfig setCommands(List commands) { + this.commands = commands; + return this; + } + + /** + * Gets the elicitation request handler. + * + * @return the elicitation handler, or {@code null} + */ + public ElicitationHandler getOnElicitationRequest() { + return onElicitationRequest; + } + + /** + * Sets a handler for elicitation requests from the server or MCP tools. + *

+ * When provided, the server will route elicitation requests to this handler and + * report elicitation as a supported capability. + * + * @param onElicitationRequest + * the elicitation handler + * @return this config instance for method chaining + * @see ElicitationHandler + */ + public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationRequest) { + this.onElicitationRequest = onElicitationRequest; + return this; + } + + /** + * Gets the exit-plan-mode request handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.8 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config instance for method chaining + * @see ExitPlanModeHandler + * @since 1.0.8 + */ + public SessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch request handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.8 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config instance for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.8 + */ + public SessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + + /** + * Gets the GitHub token for per-session authentication. + * + * @return the GitHub token, or {@code null} if not set + * @since 1.3.0 + */ + public String getGitHubToken() { + return gitHubToken; + } + + /** + * Sets the GitHub token for per-session authentication. + *

+ * When provided, the runtime resolves this token into a full GitHub identity + * and stores it on the session for content exclusion, model routing, and quota + * checks. + * + * @param gitHubToken + * the GitHub token for per-session authentication + * @return this config instance for method chaining + * @since 1.3.0 + */ + public SessionConfig setGitHubToken(String gitHubToken) { + this.gitHubToken = gitHubToken; + return this; + } + + /** + * Gets the per-session remote behavior control. + *

+ * Possible values: + *

    + *
  • {@code "off"} β€” local only, no remote export (default)
  • + *
  • {@code "export"} β€” export session events to GitHub without enabling + * remote steering
  • + *
  • {@code "on"} β€” export to GitHub AND enable remote steering
  • + *
+ * + * @return the remote session mode, or {@code null} if not set + * @since 1.4.0 + */ + public String getRemoteSession() { + return remoteSession; + } + + /** + * Sets the per-session remote behavior control. + *

+ * Possible values: + *

    + *
  • {@code "off"} β€” local only, no remote export (default)
  • + *
  • {@code "export"} β€” export session events to GitHub without enabling + * remote steering
  • + *
  • {@code "on"} β€” export to GitHub AND enable remote steering
  • + *
+ * + * @param remoteSession + * the remote session mode + * @return this config instance for method chaining + * @since 1.4.0 + */ + public SessionConfig setRemoteSession(String remoteSession) { + this.remoteSession = remoteSession; + return this; + } + + /** + * Gets the cloud session options. + *

+ * When set, creates a remote session in the cloud instead of a local session. + * The optional repository is associated with the cloud session. + * + * @return the cloud session options, or {@code null} if not set + * @since 1.5.0 + */ + public CloudSessionOptions getCloud() { + return cloud; + } + + /** + * Sets the cloud session options. + *

+ * When set, creates a remote session in the cloud instead of a local session. + * The optional repository is associated with the cloud session. + * + * @param cloud + * the cloud session options + * @return this config instance for method chaining + * @since 1.5.0 + */ + public SessionConfig setCloud(CloudSessionOptions cloud) { + this.cloud = cloud; + return this; + } + + /** + * Creates a shallow clone of this {@code SessionConfig} instance. + *

+ * Mutable collection properties are copied into new collection instances so + * that modifications to those collections on the clone do not affect the + * original. Other reference-type properties (like provider configuration, + * system messages, hooks, infinite session configuration, and handlers) are not + * deep-cloned; the original and the clone will share those objects. + * + * @return a clone of this config instance + */ + @Override + public SessionConfig clone() { + SessionConfig copy = new SessionConfig(); + copy.sessionId = this.sessionId; + copy.clientName = this.clientName; + copy.model = this.model; + copy.reasoningEffort = this.reasoningEffort; + copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null; + copy.systemMessage = this.systemMessage; + copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; + copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; + copy.provider = this.provider; + copy.enableSessionTelemetry = this.enableSessionTelemetry; + copy.onPermissionRequest = this.onPermissionRequest; + copy.onUserInputRequest = this.onUserInputRequest; + copy.hooks = this.hooks; + copy.workingDirectory = this.workingDirectory; + copy.streaming = this.streaming; + copy.includeSubAgentStreamingEvents = this.includeSubAgentStreamingEvents; + copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null; + copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null; + copy.defaultAgent = this.defaultAgent; + copy.agent = this.agent; + copy.infiniteSessions = this.infiniteSessions; + copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null; + copy.instructionDirectories = this.instructionDirectories != null + ? new ArrayList<>(this.instructionDirectories) + : null; + copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null; + copy.configDir = this.configDir; + copy.enableConfigDiscovery = this.enableConfigDiscovery; + copy.modelCapabilities = this.modelCapabilities; + copy.onEvent = this.onEvent; + copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; + copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; + copy.gitHubToken = this.gitHubToken; + copy.remoteSession = this.remoteSession; + copy.cloud = this.cloud; + return copy; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionContext.java b/java/src/main/java/com/github/copilot/sdk/json/SessionContext.java new file mode 100644 index 000000000..fb6e16f8c --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionContext.java @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Working directory context for a session. + *

+ * Contains information about the working directory where the session was + * created, including git repository information if applicable. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionContext { + + @JsonProperty("cwd") + private String cwd; + + @JsonProperty("gitRoot") + private String gitRoot; + + @JsonProperty("repository") + private String repository; + + @JsonProperty("branch") + private String branch; + + /** + * Gets the working directory where the session was created. + * + * @return the current working directory path + */ + public String getCwd() { + return cwd; + } + + /** + * Sets the working directory. + * + * @param cwd + * the current working directory path + * @return this instance for method chaining + */ + public SessionContext setCwd(String cwd) { + this.cwd = cwd; + return this; + } + + /** + * Gets the git repository root directory. + * + * @return the git root path, or {@code null} if not in a git repository + */ + public String getGitRoot() { + return gitRoot; + } + + /** + * Sets the git repository root directory. + * + * @param gitRoot + * the git root path + * @return this instance for method chaining + */ + public SessionContext setGitRoot(String gitRoot) { + this.gitRoot = gitRoot; + return this; + } + + /** + * Gets the GitHub repository in "owner/repo" format. + * + * @return the repository identifier, or {@code null} if not available + */ + public String getRepository() { + return repository; + } + + /** + * Sets the GitHub repository. + * + * @param repository + * the repository in "owner/repo" format + * @return this instance for method chaining + */ + public SessionContext setRepository(String repository) { + this.repository = repository; + return this; + } + + /** + * Gets the current git branch. + * + * @return the branch name, or {@code null} if not available + */ + public String getBranch() { + return branch; + } + + /** + * Sets the git branch. + * + * @param branch + * the branch name + * @return this instance for method chaining + */ + public SessionContext setBranch(String branch) { + this.branch = branch; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java new file mode 100644 index 000000000..e8fc908df --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHandler.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for session-end hooks. + *

+ * This handler is invoked when a session ends, allowing you to perform cleanup + * or logging. + * + *

Example Usage

+ * + *
{@code
+ * SessionEndHandler handler = (input, invocation) -> {
+ * 	System.out.println("Session ended: " + input.reason());
+ * 	return CompletableFuture.completedFuture(new SessionEndHookOutput(null, null, "Session completed successfully"));
+ * };
+ * }
+ * + * @since 1.0.7 + */ +@FunctionalInterface +public interface SessionEndHandler { + + /** + * Handles a session end event. + * + * @param input + * the hook input containing session end details + * @param invocation + * metadata about the hook invocation + * @return a future that resolves with the hook output, or {@code null} to + * proceed without modification + */ + CompletableFuture handle(SessionEndHookInput input, HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java new file mode 100644 index 000000000..0d3d3e294 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookInput.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input for a session-end hook. + *

+ * This hook is invoked when a session ends, allowing you to perform cleanup or + * logging. + * + * @param sessionId + * the runtime session ID of the session that triggered the hook + * @param timestamp + * the timestamp in milliseconds since epoch when the session ended + * @param cwd + * the current working directory + * @param reason + * the reason: "complete", "error", "abort", "timeout", or + * "user_exit" + * @param finalMessage + * the final message, or {@code null} + * @param error + * the error message, or {@code null} + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionEndHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("reason") String reason, @JsonProperty("finalMessage") String finalMessage, + @JsonProperty("error") String error) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java new file mode 100644 index 000000000..23ebf958e --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionEndHookOutput.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a session-end hook. + *

+ * Allows specifying cleanup actions and session summary. + * + * @param suppressOutput + * {@code true} to suppress output, or {@code null} + * @param cleanupActions + * the cleanup actions to perform, or {@code null} + * @param sessionSummary + * the session summary, or {@code null} + * @since 1.0.7 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SessionEndHookOutput(@JsonProperty("suppressOutput") Boolean suppressOutput, + @JsonProperty("cleanupActions") List cleanupActions, + @JsonProperty("sessionSummary") String sessionSummary) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java b/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java new file mode 100644 index 000000000..8e22c3ee8 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionHooks.java @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Hook handlers configuration for a session. + *

+ * Hooks allow you to intercept and modify various session events including tool + * execution, user prompts, and session lifecycle events. + * + *

Example Usage

+ * + *
{@code
+ * var hooks = new SessionHooks().setOnPreToolUse((input, invocation) -> {
+ * 	System.out.println("Tool being called: " + input.getToolName());
+ * 	return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
+ * }).setOnPostToolUse((input, invocation) -> {
+ * 	System.out.println("Tool result: " + input.getToolResult());
+ * 	return CompletableFuture.completedFuture(null);
+ * }).setOnUserPromptSubmitted((input, invocation) -> {
+ * 	System.out.println("User prompt: " + input.prompt());
+ * 	return CompletableFuture.completedFuture(null);
+ * }).setOnSessionStart((input, invocation) -> {
+ * 	System.out.println("Session started: " + input.source());
+ * 	return CompletableFuture.completedFuture(null);
+ * }).setOnSessionEnd((input, invocation) -> {
+ * 	System.out.println("Session ended: " + input.reason());
+ * 	return CompletableFuture.completedFuture(null);
+ * });
+ *
+ * var session = client.createSession(new SessionConfig().setHooks(hooks)).get();
+ * }
+ * + * @since 1.0.6 + */ +public class SessionHooks { + + private PreToolUseHandler onPreToolUse; + private PostToolUseHandler onPostToolUse; + private UserPromptSubmittedHandler onUserPromptSubmitted; + private SessionStartHandler onSessionStart; + private SessionEndHandler onSessionEnd; + + /** + * Gets the pre-tool-use handler. + * + * @return the handler, or {@code null} if not set + */ + public PreToolUseHandler getOnPreToolUse() { + return onPreToolUse; + } + + /** + * Sets the handler called before a tool is executed. + * + * @param onPreToolUse + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnPreToolUse(PreToolUseHandler onPreToolUse) { + this.onPreToolUse = onPreToolUse; + return this; + } + + /** + * Gets the post-tool-use handler. + * + * @return the handler, or {@code null} if not set + */ + public PostToolUseHandler getOnPostToolUse() { + return onPostToolUse; + } + + /** + * Sets the handler called after a tool has been executed. + * + * @param onPostToolUse + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnPostToolUse(PostToolUseHandler onPostToolUse) { + this.onPostToolUse = onPostToolUse; + return this; + } + + /** + * Gets the user-prompt-submitted handler. + * + * @return the handler, or {@code null} if not set + * @since 1.0.7 + */ + public UserPromptSubmittedHandler getOnUserPromptSubmitted() { + return onUserPromptSubmitted; + } + + /** + * Sets the handler called when the user submits a prompt. + * + * @param onUserPromptSubmitted + * the handler + * @return this instance for method chaining + * @since 1.0.7 + */ + public SessionHooks setOnUserPromptSubmitted(UserPromptSubmittedHandler onUserPromptSubmitted) { + this.onUserPromptSubmitted = onUserPromptSubmitted; + return this; + } + + /** + * Gets the session-start handler. + * + * @return the handler, or {@code null} if not set + * @since 1.0.7 + */ + public SessionStartHandler getOnSessionStart() { + return onSessionStart; + } + + /** + * Sets the handler called when a session starts. + * + * @param onSessionStart + * the handler + * @return this instance for method chaining + * @since 1.0.7 + */ + public SessionHooks setOnSessionStart(SessionStartHandler onSessionStart) { + this.onSessionStart = onSessionStart; + return this; + } + + /** + * Gets the session-end handler. + * + * @return the handler, or {@code null} if not set + * @since 1.0.7 + */ + public SessionEndHandler getOnSessionEnd() { + return onSessionEnd; + } + + /** + * Sets the handler called when a session ends. + * + * @param onSessionEnd + * the handler + * @return this instance for method chaining + * @since 1.0.7 + */ + public SessionHooks setOnSessionEnd(SessionEndHandler onSessionEnd) { + this.onSessionEnd = onSessionEnd; + return this; + } + + /** + * Returns whether any hooks are registered. + * + * @return {@code true} if at least one hook handler is set + */ + public boolean hasHooks() { + return onPreToolUse != null || onPostToolUse != null || onUserPromptSubmitted != null || onSessionStart != null + || onSessionEnd != null; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEvent.java b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEvent.java new file mode 100644 index 000000000..59d1e252f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEvent.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Session lifecycle event notification. + *

+ * Lifecycle events are emitted when sessions are created, deleted, updated, or + * change foreground/background state (in TUI+server mode). + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SessionLifecycleEvent { + + @JsonProperty("type") + private String type; + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("metadata") + private SessionLifecycleEventMetadata metadata; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public SessionLifecycleEventMetadata getMetadata() { + return metadata; + } + + public void setMetadata(SessionLifecycleEventMetadata metadata) { + this.metadata = metadata; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java new file mode 100644 index 000000000..7c76c07aa --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventMetadata.java @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Metadata for session lifecycle events. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionLifecycleEventMetadata(@JsonProperty("startTime") String startTime, + @JsonProperty("modifiedTime") String modifiedTime, @JsonProperty("summary") String summary) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventTypes.java b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventTypes.java new file mode 100644 index 000000000..b3eb35598 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleEventTypes.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Types of session lifecycle events. + *

+ * Constants for session lifecycle event types used with + * {@link com.github.copilot.sdk.CopilotClient#onLifecycle(String, SessionLifecycleHandler)}. + * + * @since 1.0.0 + */ +public final class SessionLifecycleEventTypes { + + /** + * Event fired when a session is created. + */ + public static final String CREATED = "session.created"; + + /** + * Event fired when a session is deleted. + */ + public static final String DELETED = "session.deleted"; + + /** + * Event fired when a session is updated. + */ + public static final String UPDATED = "session.updated"; + + /** + * Event fired when a session moves to foreground (TUI+server mode). + */ + public static final String FOREGROUND = "session.foreground"; + + /** + * Event fired when a session moves to background (TUI+server mode). + */ + public static final String BACKGROUND = "session.background"; + + private SessionLifecycleEventTypes() { + // Utility class + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleHandler.java b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleHandler.java new file mode 100644 index 000000000..11937ee64 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionLifecycleHandler.java @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Handler for session lifecycle events. + *

+ * Implement this interface to receive notifications when sessions are created, + * deleted, updated, or change foreground/background state. + * + * @since 1.0.0 + */ +@FunctionalInterface +public interface SessionLifecycleHandler { + + /** + * Called when a session lifecycle event occurs. + * + * @param event + * the lifecycle event + */ + void onLifecycleEvent(SessionLifecycleEvent event); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionListFilter.java b/java/src/main/java/com/github/copilot/sdk/json/SessionListFilter.java new file mode 100644 index 000000000..f62f7674f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionListFilter.java @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Filter options for listing sessions. + *

+ * Extends {@link SessionContext} to provide filtering capabilities with fluent + * setter methods that return the filter instance for method chaining. + * + *

Example Usage

+ * + *
{@code
+ * // Filter sessions by repository
+ * var filter = new SessionListFilter().setRepository("owner/repo");
+ * var sessions = client.listSessions(filter).get();
+ *
+ * // Filter by working directory
+ * var filter = new SessionListFilter().setCwd("/path/to/project");
+ * var sessions = client.listSessions(filter).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#listSessions(SessionListFilter) + * @since 1.0.0 + */ +public class SessionListFilter extends SessionContext { + + /** + * Sets the filter for exact cwd match. + * + * @param cwd + * the current working directory to filter by + * @return this filter for method chaining + */ + @Override + public SessionListFilter setCwd(String cwd) { + super.setCwd(cwd); + return this; + } + + /** + * Sets the filter for git root directory. + * + * @param gitRoot + * the git root path to filter by + * @return this filter for method chaining + */ + @Override + public SessionListFilter setGitRoot(String gitRoot) { + super.setGitRoot(gitRoot); + return this; + } + + /** + * Sets the filter for repository (in "owner/repo" format). + * + * @param repository + * the repository identifier to filter by + * @return this filter for method chaining + */ + @Override + public SessionListFilter setRepository(String repository) { + super.setRepository(repository); + return this; + } + + /** + * Sets the filter for git branch. + * + * @param branch + * the branch name to filter by + * @return this filter for method chaining + */ + @Override + public SessionListFilter setBranch(String branch) { + super.setBranch(branch); + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java b/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java new file mode 100644 index 000000000..cb2690d19 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionMetadata.java @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Metadata about an existing Copilot session. + *

+ * This class represents session information returned when listing available + * sessions via {@link com.github.copilot.sdk.CopilotClient#listSessions()}. It + * includes timing information, a summary of the conversation, and whether the + * session is stored remotely. + * + *

Example Usage

+ * + *
{@code
+ * var sessions = client.listSessions().get();
+ * for (var meta : sessions) {
+ * 	System.out.println("Session: " + meta.getSessionId());
+ * 	System.out.println("  Started: " + meta.getStartTime());
+ * 	System.out.println("  Summary: " + meta.getSummary());
+ * }
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient#listSessions() + * @see com.github.copilot.sdk.CopilotClient#resumeSession(String, + * ResumeSessionConfig) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionMetadata { + + @JsonProperty("sessionId") + private String sessionId; + + @JsonProperty("startTime") + private String startTime; + + @JsonProperty("modifiedTime") + private String modifiedTime; + + @JsonProperty("summary") + private String summary; + + @JsonProperty("isRemote") + private boolean isRemote; + + @JsonProperty("context") + private SessionContext context; + + /** + * Gets the unique identifier for this session. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session identifier. + * + * @param sessionId + * the session ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * Gets the timestamp when the session was created. + * + * @return the start time as an ISO 8601 formatted string + */ + public String getStartTime() { + return startTime; + } + + /** + * Sets the session start time. + * + * @param startTime + * the start time as an ISO 8601 formatted string + */ + public void setStartTime(String startTime) { + this.startTime = startTime; + } + + /** + * Gets the timestamp when the session was last modified. + * + * @return the modified time as an ISO 8601 formatted string + */ + public String getModifiedTime() { + return modifiedTime; + } + + /** + * Sets the session modified time. + * + * @param modifiedTime + * the modified time as an ISO 8601 formatted string + */ + public void setModifiedTime(String modifiedTime) { + this.modifiedTime = modifiedTime; + } + + /** + * Gets a brief summary of the session's conversation. + *

+ * This is typically an AI-generated summary of the session content. + * + * @return the session summary, or {@code null} if not available + */ + public String getSummary() { + return summary; + } + + /** + * Sets the session summary. + * + * @param summary + * the session summary + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Returns whether this session is stored remotely. + * + * @return {@code true} if the session is stored on the server, {@code false} if + * it's stored locally + */ + public boolean isRemote() { + return isRemote; + } + + /** + * Sets whether this session is stored remotely. + * + * @param remote + * {@code true} if stored remotely + */ + public void setRemote(boolean remote) { + isRemote = remote; + } + + /** + * Gets the working directory context from session creation. + *

+ * Contains information about the working directory, git repository, and branch + * where the session was created. + * + * @return the session context, or {@code null} if not available + */ + public SessionContext getContext() { + return context; + } + + /** + * Sets the working directory context. + * + * @param context + * the session context + */ + public void setContext(SessionContext context) { + this.context = context; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java new file mode 100644 index 000000000..fd631cb7f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHandler.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for session-start hooks. + *

+ * This handler is invoked when a session starts, allowing you to perform + * initialization or modify the session configuration. + * + *

Example Usage

+ * + *
{@code
+ * SessionStartHandler handler = (input, invocation) -> {
+ * 	System.out.println("Session started from: " + input.source());
+ * 	return CompletableFuture.completedFuture(new SessionStartHookOutput("Custom initialization context", null));
+ * };
+ * }
+ * + * @since 1.0.7 + */ +@FunctionalInterface +public interface SessionStartHandler { + + /** + * Handles a session start event. + * + * @param input + * the hook input containing session start details + * @param invocation + * metadata about the hook invocation + * @return a future that resolves with the hook output, or {@code null} to + * proceed without modification + */ + CompletableFuture handle(SessionStartHookInput input, HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java new file mode 100644 index 000000000..55bff3e26 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookInput.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input for a session-start hook. + *

+ * This hook is invoked when a session starts, allowing you to perform + * initialization or modify the session configuration. + * + * @param sessionId + * the runtime session ID of the session that triggered the hook + * @param timestamp + * the timestamp in milliseconds since epoch when the session started + * @param cwd + * the current working directory + * @param source + * the source: "startup", "resume", or "new" + * @param initialPrompt + * the initial prompt, or {@code null} + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionStartHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("source") String source, @JsonProperty("initialPrompt") String initialPrompt) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java new file mode 100644 index 000000000..3ef5971c4 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionStartHookOutput.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a session-start hook. + *

+ * Allows adding additional context or modifying session configuration. + * + * @param additionalContext + * additional context to be added to the session, or {@code null} + * @param modifiedConfig + * modified configuration options for the session, or {@code null} + * @since 1.0.7 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SessionStartHookOutput(@JsonProperty("additionalContext") String additionalContext, + @JsonProperty("modifiedConfig") Map modifiedConfig) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java b/java/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java new file mode 100644 index 000000000..f0a43f261 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionUiApi.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Provides UI methods for eliciting information from the user during a session. + *

+ * All methods on this interface throw {@link IllegalStateException} if the host + * does not report elicitation support via + * {@link com.github.copilot.sdk.CopilotSession#getCapabilities()}. Check + * {@code session.getCapabilities().getUi() != null && + * Boolean.TRUE.equals(session.getCapabilities().getUi().getElicitation())} + * before calling. + * + *

Example Usage

+ * + *
{@code
+ * var caps = session.getCapabilities();
+ * if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
+ * 	boolean confirmed = session.getUi().confirm("Are you sure?").get();
+ * }
+ * }
+ * + * @see com.github.copilot.sdk.CopilotSession#getUi() + * @since 1.0.0 + */ +public interface SessionUiApi { + + /** + * Shows a generic elicitation dialog with a custom schema. + * + * @param params + * the elicitation parameters including message and schema + * @return a future that resolves with the {@link ElicitationResult} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture elicitation(ElicitationParams params); + + /** + * Shows a confirmation dialog and returns the user's boolean answer. + *

+ * Returns {@code false} if the user declines or cancels. + * + * @param message + * the message to display + * @return a future that resolves to {@code true} if the user confirmed + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture confirm(String message); + + /** + * Shows a selection dialog with the given options. + *

+ * Returns the selected value, or {@code null} if the user declines/cancels. + * + * @param message + * the message to display + * @param options + * the options to present + * @return a future that resolves to the selected string, or {@code null} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture select(String message, String[] options); + + /** + * Shows a text input dialog. + *

+ * Returns the entered text, or {@code null} if the user declines/cancels. + * + * @param message + * the message to display + * @param options + * optional input field options, or {@code null} + * @return a future that resolves to the entered string, or {@code null} + * @throws IllegalStateException + * if the host does not support elicitation + */ + CompletableFuture input(String message, InputOptions options); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java b/java/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java new file mode 100644 index 000000000..d19d531ee --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SessionUiCapabilities.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * UI-specific capability flags for a session. + * + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SessionUiCapabilities { + + @JsonProperty("elicitation") + private Boolean elicitation; + + /** + * Returns whether the host supports interactive elicitation dialogs. + * + * @return an {@link Optional} containing the boolean value, or empty if not set + */ + @JsonIgnore + public Optional getElicitation() { + return Optional.ofNullable(elicitation); + } + + /** + * Sets whether the host supports interactive elicitation dialogs. + * + * @param elicitation + * {@code true} if elicitation is supported + * @return this instance for method chaining + */ + public SessionUiCapabilities setElicitation(boolean elicitation) { + this.elicitation = elicitation; + return this; + } + + /** + * Clears the elicitation setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public SessionUiCapabilities clearElicitation() { + this.elicitation = null; + return this; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java b/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java new file mode 100644 index 000000000..d3944871a --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionRequest.java @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for session.setForeground RPC call. + *

+ * Using an explicit record type (rather than an ad-hoc map) ensures correct + * JSON serialization in all execution environments. + * + * @since 1.0.0 + */ +public record SetForegroundSessionRequest( + /** The session ID to bring to the foreground. */ + @JsonProperty("sessionId") String sessionId) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java b/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java new file mode 100644 index 000000000..c4680c95d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SetForegroundSessionResponse.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response from session.setForeground RPC call. + *

+ * This is only available when connecting to a server running in TUI+server mode + * (--ui-server). + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SetForegroundSessionResponse( + /** Whether the operation was successful. */ + @JsonProperty("success") boolean success, + /** The error message, or null if successful. */ + @JsonProperty("error") String error) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java b/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java new file mode 100644 index 000000000..94af117ea --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SystemMessageConfig.java @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.github.copilot.sdk.SystemMessageMode; + +/** + * Configuration for customizing the system message. + *

+ * The system message controls the behavior and personality of the AI assistant. + * This configuration allows you to either append to, replace, or fine-tune the + * default system message. + * + *

Example - Append Mode

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.APPEND)
+ * 		.setContent("Always respond in a formal tone.");
+ * }
+ * + *

Example - Replace Mode

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.REPLACE)
+ * 		.setContent("You are a helpful coding assistant.");
+ * }
+ * + *

Example - Customize Mode

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
+ * 		.setSections(
+ * 				Map.of(SystemPromptSections.TONE,
+ * 						new SectionOverride().setAction(SectionOverrideAction.REPLACE)
+ * 								.setContent("Be concise and formal."),
+ * 						SystemPromptSections.CODE_CHANGE_RULES,
+ * 						new SectionOverride().setAction(SectionOverrideAction.REMOVE)))
+ * 		.setContent("Additional instructions appended after all sections.");
+ * }
+ * + * @see SessionConfig#setSystemMessage(SystemMessageConfig) + * @see SystemMessageMode + * @see SectionOverride + * @see SystemPromptSections + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SystemMessageConfig { + + private SystemMessageMode mode; + private String content; + @JsonInclude(JsonInclude.Include.NON_NULL) + @com.fasterxml.jackson.annotation.JsonProperty("sections") + private Map sections; + + /** + * Gets the system message mode. + * + * @return the mode (APPEND, REPLACE, or CUSTOMIZE) + */ + public SystemMessageMode getMode() { + return mode; + } + + /** + * Sets the system message mode. + *

+ * Use {@link SystemMessageMode#APPEND} to add to the default system message + * while preserving guardrails, {@link SystemMessageMode#REPLACE} to fully + * customize the system message, or {@link SystemMessageMode#CUSTOMIZE} to + * override individual sections. + * + * @param mode + * the mode (APPEND, REPLACE, or CUSTOMIZE) + * @return this config for method chaining + */ + public SystemMessageConfig setMode(SystemMessageMode mode) { + this.mode = mode; + return this; + } + + /** + * Gets the system message content. + * + * @return the content to append or use as replacement + */ + public String getContent() { + return content; + } + + /** + * Sets the system message content. + *

+ * For {@link SystemMessageMode#APPEND} and {@link SystemMessageMode#REPLACE} + * modes, this is the primary content. For {@link SystemMessageMode#CUSTOMIZE} + * mode, this is appended after all section overrides. + * + * @param content + * the system message content + * @return this config for method chaining + */ + public SystemMessageConfig setContent(String content) { + this.content = content; + return this; + } + + /** + * Gets the section-level overrides for {@link SystemMessageMode#CUSTOMIZE} + * mode. + * + * @return the sections map, or {@code null} + */ + public Map getSections() { + return sections; + } + + /** + * Sets section-level overrides for {@link SystemMessageMode#CUSTOMIZE} mode. + *

+ * Keys are section identifiers from {@link SystemPromptSections}. Each value + * describes how that section should be modified. Sections with a + * {@link SectionOverride#getTransform() transform} callback are handled locally + * by the SDK via a {@code systemMessage.transform} RPC call; the rest are sent + * to the CLI as-is. + * + * @param sections + * a map of section identifier to override operation + * @return this config for method chaining + * @since 1.2.0 + */ + public SystemMessageConfig setSections(Map sections) { + this.sections = sections; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/SystemPromptSections.java b/java/src/main/java/com/github/copilot/sdk/json/SystemPromptSections.java new file mode 100644 index 000000000..fa512d032 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/SystemPromptSections.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Well-known system prompt section identifiers for use with + * {@link SystemMessageMode#CUSTOMIZE} mode. + *

+ * Each constant names a section of the default Copilot system prompt. Pass + * these as keys in the {@code sections} map of {@link SystemMessageConfig} to + * override individual sections. + * + *

Example

+ * + *
{@code
+ * var config = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE).setSections(Map.of(
+ * 		SystemPromptSections.TONE,
+ * 		new SectionOverride().setAction(SectionOverrideAction.REPLACE).setContent("Always be concise."),
+ * 		SystemPromptSections.CODE_CHANGE_RULES, new SectionOverride().setAction(SectionOverrideAction.REMOVE)));
+ * }
+ * + * @see SystemMessageConfig + * @see SectionOverride + * @since 1.2.0 + */ +public final class SystemPromptSections { + + /** Agent identity preamble and mode statement. */ + public static final String IDENTITY = "identity"; + + /** Response style, conciseness rules, output formatting preferences. */ + public static final String TONE = "tone"; + + /** Tool usage patterns, parallel calling, batching guidelines. */ + public static final String TOOL_EFFICIENCY = "tool_efficiency"; + + /** CWD, OS, git root, directory listing, available tools. */ + public static final String ENVIRONMENT_CONTEXT = "environment_context"; + + /** Coding rules, linting/testing, ecosystem tools, style. */ + public static final String CODE_CHANGE_RULES = "code_change_rules"; + + /** Tips, behavioral best practices, behavioral guidelines. */ + public static final String GUIDELINES = "guidelines"; + + /** Environment limitations, prohibited actions, security policies. */ + public static final String SAFETY = "safety"; + + /** Per-tool usage instructions. */ + public static final String TOOL_INSTRUCTIONS = "tool_instructions"; + + /** Repository and organization custom instructions. */ + public static final String CUSTOM_INSTRUCTIONS = "custom_instructions"; + + /** + * End-of-prompt instructions: parallel tool calling, persistence, task + * completion. + */ + public static final String LAST_INSTRUCTIONS = "last_instructions"; + + private SystemPromptSections() { + // utility class + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java b/java/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java new file mode 100644 index 000000000..7272c9884 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/TelemetryConfig.java @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * OpenTelemetry configuration for the Copilot CLI server. + *

+ * When set on {@link CopilotClientOptions#setTelemetry(TelemetryConfig)}, the + * CLI server is started with OpenTelemetry instrumentation enabled using the + * provided settings. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions()
+ * 		.setTelemetry(new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setSourceName("my-app"));
+ * }
+ * + * @see CopilotClientOptions#setTelemetry(TelemetryConfig) + * @since 1.2.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TelemetryConfig { + + private String otlpEndpoint; + private String filePath; + private String exporterType; + private String sourceName; + private Boolean captureContent; + + /** + * Gets the OTLP exporter endpoint URL. + *

+ * Maps to the {@code OTEL_EXPORTER_OTLP_ENDPOINT} environment variable. + * + * @return the OTLP endpoint URL, or {@code null} + */ + public String getOtlpEndpoint() { + return otlpEndpoint; + } + + /** + * Sets the OTLP exporter endpoint URL. + * + * @param otlpEndpoint + * the endpoint URL (e.g., {@code "http://localhost:4318"}) + * @return this config for method chaining + */ + public TelemetryConfig setOtlpEndpoint(String otlpEndpoint) { + this.otlpEndpoint = otlpEndpoint; + return this; + } + + /** + * Gets the file path for the file exporter. + *

+ * Maps to the {@code COPILOT_OTEL_FILE_EXPORTER_PATH} environment variable. + * + * @return the file path, or {@code null} + */ + public String getFilePath() { + return filePath; + } + + /** + * Sets the file path for the file exporter. + * + * @param filePath + * the path where telemetry spans are written + * @return this config for method chaining + */ + public TelemetryConfig setFilePath(String filePath) { + this.filePath = filePath; + return this; + } + + /** + * Gets the exporter type. + *

+ * Maps to the {@code COPILOT_OTEL_EXPORTER_TYPE} environment variable. + * + * @return the exporter type (e.g., {@code "otlp-http"} or {@code "file"}), or + * {@code null} + */ + public String getExporterType() { + return exporterType; + } + + /** + * Sets the exporter type. + * + * @param exporterType + * the exporter type ({@code "otlp-http"} or {@code "file"}) + * @return this config for method chaining + */ + public TelemetryConfig setExporterType(String exporterType) { + this.exporterType = exporterType; + return this; + } + + /** + * Gets the source name for telemetry spans. + *

+ * Maps to the {@code COPILOT_OTEL_SOURCE_NAME} environment variable. + * + * @return the source name, or {@code null} + */ + public String getSourceName() { + return sourceName; + } + + /** + * Sets the source name for telemetry spans. + * + * @param sourceName + * a name identifying the application producing the spans + * @return this config for method chaining + */ + public TelemetryConfig setSourceName(String sourceName) { + this.sourceName = sourceName; + return this; + } + + /** + * Gets whether to capture message content as part of telemetry. + *

+ * Maps to the {@code OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT} + * environment variable. + * + * @return an {@link java.util.Optional} containing {@code true} to capture + * content or {@code false} to suppress it, or + * {@link java.util.Optional#empty()} to use the default + */ + @JsonIgnore + public Optional getCaptureContent() { + return Optional.ofNullable(captureContent); + } + + /** + * Sets whether to capture message content as part of telemetry. + * + * @param captureContent + * {@code true} to capture content, {@code false} to suppress it + * @return this config for method chaining + */ + public TelemetryConfig setCaptureContent(boolean captureContent) { + this.captureContent = captureContent; + return this; + } + + /** + * Clears the captureContent setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public TelemetryConfig clearCaptureContent() { + this.captureContent = null; + return this; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java b/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java new file mode 100644 index 000000000..e00fce9cf --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolBinaryResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Binary result from a tool execution. + *

+ * This record represents binary data (such as images) returned by a tool. The + * data is base64-encoded for JSON transmission. + * + *

Example Usage

+ * + *
{@code
+ * var binaryResult = new ToolBinaryResult(Base64.getEncoder().encodeToString(imageBytes), "image/png", "image",
+ * 		"Generated chart");
+ * }
+ * + * @param data + * the base64-encoded binary data + * @param mimeType + * the MIME type (e.g., "image/png", "application/pdf") + * @param type + * the content type (e.g., "image", "file") + * @param description + * the content description, helps the assistant understand the + * content + * @see ToolResultObject#setBinaryResultsForLlm(java.util.List) + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ToolBinaryResult(@JsonProperty("data") String data, @JsonProperty("mimeType") String mimeType, + @JsonProperty("type") String type, @JsonProperty("description") String description) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java b/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java new file mode 100644 index 000000000..ba33ce1e3 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolDefinition.java @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Defines a tool that can be invoked by the AI assistant. + *

+ * Tools extend the assistant's capabilities by allowing it to call back into + * your application to perform actions or retrieve information. Each tool has a + * name, description, parameter schema, and a handler function that executes + * when the tool is invoked. + * + *

Example Usage

+ * + *
{@code
+ * // Define a record for your tool's arguments
+ * record WeatherArgs(String location) {
+ * }
+ *
+ * var tool = ToolDefinition.create("get_weather", "Get the current weather for a location",
+ * 		Map.of("type", "object", "properties",
+ * 				Map.of("location", Map.of("type", "string", "description", "City name")), "required",
+ * 				List.of("location")),
+ * 		invocation -> {
+ * 			// Type-safe access with records (recommended)
+ * 			WeatherArgs args = invocation.getArgumentsAs(WeatherArgs.class);
+ * 			return CompletableFuture.completedFuture(getWeatherData(args.location()));
+ *
+ * 			// Or use Map-based access
+ * 			// Map args = invocation.getArguments();
+ * 			// String location = (String) args.get("location");
+ * 		});
+ * }
+ * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param parameters + * the JSON Schema defining the tool's parameters + * @param handler + * the handler function to execute when invoked + * @param overridesBuiltInTool + * when {@code true}, indicates that this tool intentionally + * overrides a built-in CLI tool with the same name; {@code null} or + * {@code false} means the tool is purely custom + * @param skipPermission + * when {@code true}, the CLI skips the permission request for this + * tool invocation; {@code null} or {@code false} uses normal + * permission handling + * @see SessionConfig#setTools(java.util.List) + * @see ToolHandler + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description, + @JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler, + @JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool, + @JsonProperty("skipPermission") Boolean skipPermission) { + + /** + * Creates a tool definition with a JSON schema for parameters. + *

+ * This is a convenience factory method for creating tools with a + * {@code Map}-based parameter schema. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param schema + * the JSON Schema as a {@code Map} + * @param handler + * the handler function to execute when invoked + * @return a new tool definition + */ + public static ToolDefinition create(String name, String description, Map schema, + ToolHandler handler) { + return new ToolDefinition(name, description, schema, handler, null, null); + } + + /** + * Creates a tool definition that overrides a built-in CLI tool. + *

+ * Use this factory method when you want your custom tool to replace a built-in + * tool (e.g., {@code grep}, {@code read_file}) with the same name. Setting + * {@code overridesBuiltInTool} to {@code true} signals to the CLI that this is + * intentional. + * + * @param name + * the name of the built-in tool to override + * @param description + * a description of what the tool does + * @param schema + * the JSON Schema as a {@code Map} + * @param handler + * the handler function to execute when invoked + * @return a new tool definition with the override flag set + * @since 1.0.11 + */ + public static ToolDefinition createOverride(String name, String description, Map schema, + ToolHandler handler) { + return new ToolDefinition(name, description, schema, handler, true, null); + } + + /** + * Creates a tool definition that skips the permission request. + *

+ * Use this factory method when the tool is safe to invoke without user + * permission confirmation. Setting {@code skipPermission} to {@code true} + * signals to the CLI that no permission check is needed. + * + * @param name + * the unique name of the tool + * @param description + * a description of what the tool does + * @param schema + * the JSON Schema as a {@code Map} + * @param handler + * the handler function to execute when invoked + * @return a new tool definition with permission skipping enabled + * @since 1.2.0 + */ + public static ToolDefinition createSkipPermission(String name, String description, Map schema, + ToolHandler handler) { + return new ToolDefinition(name, description, schema, handler, null, true); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java b/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java new file mode 100644 index 000000000..e3e421b65 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolHandler.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Functional interface for handling tool invocations from the AI assistant. + *

+ * When the assistant decides to use a tool, it invokes this handler with the + * tool's arguments. The handler should perform the requested action and return + * the result. + * + *

Example Implementation

+ * + *
{@code
+ * // Option 1: Type-safe access with records (recommended)
+ * record SearchArgs(String query) {
+ * }
+ *
+ * ToolHandler handler = invocation -> {
+ * 	SearchArgs args = invocation.getArgumentsAs(SearchArgs.class);
+ * 	String result = performSearch(args.query());
+ * 	return CompletableFuture.completedFuture(result);
+ * };
+ *
+ * // Option 2: Map-based access
+ * ToolHandler handler = invocation -> {
+ * 	Map args = invocation.getArguments();
+ * 	String query = (String) args.get("query");
+ * 	String result = performSearch(query);
+ * 	return CompletableFuture.completedFuture(result);
+ * };
+ * }
+ * + * @see ToolDefinition + * @see ToolInvocation + * @since 1.0.0 + */ +@FunctionalInterface +public interface ToolHandler { + + /** + * Invokes the tool with the given invocation context. + *

+ * The returned object will be serialized to JSON and sent back to the assistant + * as the tool's result. This can be a {@code String}, {@code Map}, or any + * JSON-serializable object. + * + * @param invocation + * the invocation context containing arguments + * @return a future that completes with the tool's result + */ + CompletableFuture invoke(ToolInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java new file mode 100644 index 000000000..e5febba6f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolInvocation.java @@ -0,0 +1,171 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Represents a tool invocation request from the AI assistant. + *

+ * When the assistant invokes a tool, this object contains the context including + * the session ID, tool call ID, tool name, and arguments parsed from the + * assistant's request. + * + * @see ToolHandler + * @see ToolDefinition + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ToolInvocation { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE = new TypeReference<>() { + }; + + private String sessionId; + private String toolCallId; + private String toolName; + private JsonNode argumentsNode; + + /** + * Gets the session ID where the tool was invoked. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this invocation for method chaining + */ + public ToolInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Gets the unique identifier for this tool call. + *

+ * This ID correlates the tool invocation with its response. + * + * @return the tool call ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * Sets the tool call ID. + * + * @param toolCallId + * the tool call ID + * @return this invocation for method chaining + */ + public ToolInvocation setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + return this; + } + + /** + * Gets the name of the tool being invoked. + * + * @return the tool name + */ + public String getToolName() { + return toolName; + } + + /** + * Sets the tool name. + * + * @param toolName + * the tool name + * @return this invocation for method chaining + */ + public ToolInvocation setToolName(String toolName) { + this.toolName = toolName; + return this; + } + + /** + * Gets the arguments passed to the tool as a Map. + *

+ * The arguments are provided as a {@code Map} matching the + * parameter schema defined in the tool's {@link ToolDefinition}. Values can be + * accessed using standard Map operations. + *

+ * For type-safe access, use {@link #getArgumentsAs(Class)} to deserialize + * arguments into a record or POJO. + * + * @return the arguments as a Map, or null if no arguments + * @see #getArgumentsAs(Class) + */ + public Map getArguments() { + if (argumentsNode == null) { + return null; + } + return MAPPER.convertValue(argumentsNode, MAP_TYPE); + } + + /** + * Deserializes the tool arguments into the specified type. + *

+ * This method provides type-safe access to tool arguments by converting the + * JSON arguments into a record, POJO, or other compatible type. + * + *

{@code
+     * // Define a record for your tool's arguments
+     * record WeatherArgs(String city) {
+     * }
+     *
+     * // In your tool handler
+     * WeatherArgs args = invocation.getArgumentsAs(WeatherArgs.class);
+     * String city = args.city();
+     * }
+ * + * @param + * the type to deserialize to + * @param type + * the class of the target type + * @return the arguments deserialized as the specified type + * @throws IllegalArgumentException + * if deserialization fails + * @since 1.0.0 + */ + public T getArgumentsAs(Class type) { + try { + return MAPPER.treeToValue(argumentsNode, type); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to deserialize arguments to " + type.getName(), e); + } + } + + /** + * Sets the tool arguments. + *

+ * Note: This method is intended for internal SDK use and JSON + * deserialization. Users typically do not need to call this method directly. + * + * @param arguments + * the arguments as a JsonNode + * @return this invocation for method chaining + */ + @JsonSetter("arguments") + public ToolInvocation setArguments(JsonNode arguments) { + this.argumentsNode = arguments; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java b/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java new file mode 100644 index 000000000..dcb5ad78f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/ToolResultObject.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Result object returned from a tool execution. + *

+ * This record represents the structured result of a tool invocation, including + * text output, binary data, error information, and telemetry. + * + *

Example: Success Result

+ * + *
{@code
+ * return ToolResultObject.success("File contents: " + content);
+ * }
+ * + *

Example: Error Result

+ * + *
{@code
+ * return ToolResultObject.error("File not found: " + path);
+ * }
+ * + *

Example: Custom Result

+ * + *
{@code
+ * return new ToolResultObject("success", "Result text", null, null, null, null);
+ * }
+ * + * @param resultType + * the result type ("success" or "error"), defaults to "success" + * @param textResultForLlm + * the text result to be sent to the LLM + * @param binaryResultsForLlm + * the list of binary results to be sent to the LLM + * @param error + * the error message, or {@code null} if successful + * @param sessionLog + * the session log text + * @param toolTelemetry + * the tool telemetry data + * @see ToolHandler + * @see ToolBinaryResult + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ToolResultObject(@JsonProperty("resultType") String resultType, + @JsonProperty("textResultForLlm") String textResultForLlm, + @JsonProperty("binaryResultsForLlm") List binaryResultsForLlm, + @JsonProperty("error") String error, @JsonProperty("sessionLog") String sessionLog, + @JsonProperty("toolTelemetry") Map toolTelemetry) { + + /** + * Creates a success result with the given text. + * + * @param textResultForLlm + * the text result to be sent to the LLM + * @return a success result + */ + public static ToolResultObject success(String textResultForLlm) { + return new ToolResultObject("success", textResultForLlm, null, null, null, null); + } + + /** + * Creates an error result with the given error message. + * + * @param error + * the error message + * @return an error result + */ + public static ToolResultObject error(String error) { + return new ToolResultObject("error", null, null, error, null, null); + } + + /** + * Creates an error result with both a text result and error message. + * + * @param textResultForLlm + * the text result to be sent to the LLM + * @param error + * the error message + * @return an error result + */ + public static ToolResultObject error(String textResultForLlm, String error) { + return new ToolResultObject("error", textResultForLlm, null, error, null, null); + } + + /** + * Creates a failure result with the given text and error message. + *

+ * The "failure" result type indicates that the tool execution itself failed + * (e.g., tool not found), while "error" indicates the tool executed but + * encountered an error during processing. + * + * @param textResultForLlm + * the text result to be sent to the LLM + * @param error + * the error message + * @return a failure result + */ + public static ToolResultObject failure(String textResultForLlm, String error) { + return new ToolResultObject("failure", textResultForLlm, null, error, null, null); + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserInputHandler.java b/java/src/main/java/com/github/copilot/sdk/json/UserInputHandler.java new file mode 100644 index 000000000..e5d171098 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserInputHandler.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for user input requests from the agent. + *

+ * Implement this interface to handle user input requests when the agent uses + * the ask_user tool. + * + *

Example Usage

+ * + *
{@code
+ * UserInputHandler handler = (request, invocation) -> {
+ * 	System.out.println("Agent asks: " + request.getQuestion());
+ * 	String answer = readUserInput(); // your input method
+ * 	return CompletableFuture.completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(true));
+ * };
+ *
+ * var session = client.createSession(new SessionConfig().setOnUserInputRequest(handler)).get();
+ * }
+ * + * @since 1.0.6 + */ +@FunctionalInterface +public interface UserInputHandler { + + /** + * Handles a user input request from the agent. + * + * @param request + * the user input request containing the question and optional + * choices + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's response + */ + CompletableFuture handle(UserInputRequest request, UserInputInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserInputInvocation.java b/java/src/main/java/com/github/copilot/sdk/json/UserInputInvocation.java new file mode 100644 index 000000000..3232b0c34 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserInputInvocation.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for a user input request invocation. + * + * @since 1.0.6 + */ +public class UserInputInvocation { + + private String sessionId; + + /** + * Gets the session ID. + * + * @return the session ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * Sets the session ID. + * + * @param sessionId + * the session ID + * @return this instance for method chaining + */ + public UserInputInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java b/java/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java new file mode 100644 index 000000000..23b0d8812 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserInputRequest.java @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; + +/** + * Request for user input from the agent. + *

+ * This is sent when the agent uses the ask_user tool to request input from the + * user. + * + * @since 1.0.6 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class UserInputRequest { + + @JsonProperty("question") + private String question; + + @JsonProperty("choices") + private List choices; + + @JsonProperty("allowFreeform") + private Boolean allowFreeform; + + /** + * Gets the question to ask the user. + * + * @return the question text + */ + public String getQuestion() { + return question; + } + + /** + * Sets the question to ask the user. + * + * @param question + * the question text + * @return this instance for method chaining + */ + public UserInputRequest setQuestion(String question) { + this.question = question; + return this; + } + + /** + * Gets the optional choices for multiple choice questions. + * + * @return the list of choices, or {@code null} for freeform input + */ + public List getChoices() { + return choices == null ? null : Collections.unmodifiableList(choices); + } + + /** + * Sets the choices for multiple choice questions. + * + * @param choices + * the list of choices + * @return this instance for method chaining + */ + public UserInputRequest setChoices(List choices) { + this.choices = choices; + return this; + } + + /** + * Returns whether freeform text input is allowed. + * + * @return an {@link java.util.Optional} containing {@code true} if freeform + * input is allowed, or {@link java.util.Optional#empty()} if not + * specified + */ + @JsonIgnore + public Optional getAllowFreeform() { + return Optional.ofNullable(allowFreeform); + } + + /** + * Sets whether freeform text input is allowed. + * + * @param allowFreeform + * {@code true} to allow freeform input + * @return this instance for method chaining + */ + public UserInputRequest setAllowFreeform(boolean allowFreeform) { + this.allowFreeform = allowFreeform; + return this; + } + + /** + * Clears the allowFreeform setting, reverting to the default behavior. + * + * @return this instance for method chaining + */ + public UserInputRequest clearAllowFreeform() { + this.allowFreeform = null; + return this; + } + +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserInputResponse.java b/java/src/main/java/com/github/copilot/sdk/json/UserInputResponse.java new file mode 100644 index 000000000..4cfaa13f0 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserInputResponse.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response to a user input request. + * + * @since 1.0.6 + */ +public class UserInputResponse { + + @JsonProperty("answer") + private String answer; + + @JsonProperty("wasFreeform") + private boolean wasFreeform; + + /** + * Gets the user's answer. + * + * @return the answer text + */ + public String getAnswer() { + return answer; + } + + /** + * Sets the user's answer. + * + * @param answer + * the answer text + * @return this instance for method chaining + */ + public UserInputResponse setAnswer(String answer) { + this.answer = answer; + return this; + } + + /** + * Returns whether the answer was freeform (not from the provided choices). + * + * @return {@code true} if the answer was freeform + */ + public boolean isWasFreeform() { + return wasFreeform; + } + + /** + * Sets whether the answer was freeform. + * + * @param wasFreeform + * {@code true} if the answer was freeform + * @return this instance for method chaining + */ + public UserInputResponse setWasFreeform(boolean wasFreeform) { + this.wasFreeform = wasFreeform; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java new file mode 100644 index 000000000..0dc59762b --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHandler.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for user-prompt-submitted hooks. + *

+ * This handler is invoked when the user submits a prompt, allowing you to + * intercept and modify the prompt before it is processed. + * + *

Example Usage

+ * + *
{@code
+ * UserPromptSubmittedHandler handler = (input, invocation) -> {
+ * 	System.out.println("User submitted: " + input.prompt());
+ * 	// Optionally modify the prompt
+ * 	return CompletableFuture
+ * 			.completedFuture(new UserPromptSubmittedHookOutput(input.prompt() + " (enhanced)", null, null));
+ * };
+ * }
+ * + * @since 1.0.7 + */ +@FunctionalInterface +public interface UserPromptSubmittedHandler { + + /** + * Handles a user prompt submission event. + * + * @param input + * the hook input containing the prompt details + * @param invocation + * metadata about the hook invocation + * @return a future that resolves with the hook output, or {@code null} to + * proceed without modification + */ + CompletableFuture handle(UserPromptSubmittedHookInput input, + HookInvocation invocation); +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java new file mode 100644 index 000000000..2f3a0948d --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookInput.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input for a user-prompt-submitted hook. + *

+ * This hook is invoked when the user submits a prompt, allowing you to + * intercept and modify the prompt before it is processed. + * + * @param sessionId + * the runtime session ID of the session that triggered the hook + * @param timestamp + * the timestamp in milliseconds since epoch when the prompt was + * submitted + * @param cwd + * the current working directory + * @param prompt + * the user's prompt text + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record UserPromptSubmittedHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("prompt") String prompt) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java new file mode 100644 index 000000000..d5b345556 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/UserPromptSubmittedHookOutput.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a user-prompt-submitted hook. + *

+ * Allows modifying the user's prompt before processing. + * + * @param modifiedPrompt + * the modified prompt to use instead of the original, or + * {@code null} to use the original + * @param additionalContext + * additional context to be added to the prompt, or {@code null} + * @param suppressOutput + * {@code true} to suppress output, or {@code null} + * @since 1.0.7 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record UserPromptSubmittedHookOutput(@JsonProperty("modifiedPrompt") String modifiedPrompt, + @JsonProperty("additionalContext") String additionalContext, + @JsonProperty("suppressOutput") Boolean suppressOutput) { +} diff --git a/java/src/main/java/com/github/copilot/sdk/json/package-info.java b/java/src/main/java/com/github/copilot/sdk/json/package-info.java new file mode 100644 index 000000000..aabf62069 --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/json/package-info.java @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Configuration classes and data transfer objects for the Copilot SDK. + * + *

+ * This package contains all the configuration, request, response, and data + * transfer objects used throughout the SDK. These classes are designed for JSON + * serialization with Jackson and provide fluent setter methods for convenient + * configuration. + * + *

Client Configuration

+ *
    + *
  • {@link com.github.copilot.sdk.json.CopilotClientOptions} - Options for + * configuring the {@link com.github.copilot.sdk.CopilotClient}, including CLI + * path, port, transport mode, and auto-start behavior.
  • + *
+ * + *

Session Configuration

+ *
    + *
  • {@link com.github.copilot.sdk.json.SessionConfig} - Configuration for + * creating a new session, including model selection, tools, system message, and + * MCP server configuration.
  • + *
  • {@link com.github.copilot.sdk.json.ResumeSessionConfig} - Configuration + * for resuming an existing session.
  • + *
  • {@link com.github.copilot.sdk.json.InfiniteSessionConfig} - Configuration + * for infinite sessions with automatic context compaction.
  • + *
  • {@link com.github.copilot.sdk.json.SystemMessageConfig} - System message + * customization options.
  • + *
+ * + *

Message and Tool Configuration

+ *
    + *
  • {@link com.github.copilot.sdk.json.MessageOptions} - Options for sending + * messages, including prompt text and attachments.
  • + *
  • {@link com.github.copilot.sdk.json.ToolDefinition} - Definition of a + * custom tool that can be invoked by the assistant.
  • + *
  • {@link com.github.copilot.sdk.json.ToolInvocation} - Represents a tool + * invocation request from the assistant.
  • + *
  • {@link com.github.copilot.sdk.json.Attachment} - File attachment for + * messages.
  • + *
+ * + *

Provider Configuration (BYOK)

+ *
    + *
  • {@link com.github.copilot.sdk.json.ProviderConfig} - Configuration for + * using your own API keys with custom providers (OpenAI, Azure, etc.).
  • + *
  • {@link com.github.copilot.sdk.json.AzureOptions} - Azure-specific + * configuration options.
  • + *
+ * + *

Model Information

+ *
    + *
  • {@link com.github.copilot.sdk.json.ModelInfo} - Information about an + * available AI model.
  • + *
  • {@link com.github.copilot.sdk.json.ModelCapabilities} - Model + * capabilities and limits.
  • + *
  • {@link com.github.copilot.sdk.json.ModelPolicy} - Model policy and state + * information.
  • + *
+ * + *

Custom Agents

+ *
    + *
  • {@link com.github.copilot.sdk.json.CustomAgentConfig} - Configuration for + * custom agents with specialized behaviors and tools.
  • + *
+ * + *

Permissions

+ *
    + *
  • {@link com.github.copilot.sdk.json.PermissionHandler} - Handler for + * permission requests from the assistant.
  • + *
  • {@link com.github.copilot.sdk.json.PermissionRequest} - A permission + * request from the assistant.
  • + *
  • {@link com.github.copilot.sdk.json.PermissionRequestResult} - Result of a + * permission request decision.
  • + *
+ * + *

Usage Example

+ * + *
{@code
+ * var config = new SessionConfig().setModel("gpt-4.1").setStreaming(true)
+ * 		.setSystemMessage(new SystemMessageConfig().setMode(SystemMessageMode.APPEND)
+ * 				.setContent("Be concise in your responses."))
+ * 		.setTools(List.of(ToolDefinition.create("my_tool", "Description", schema, handler)));
+ *
+ * var session = client.createSession(config).get();
+ * }
+ * + * @see com.github.copilot.sdk.CopilotClient + * @see com.github.copilot.sdk.CopilotSession + */ +@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "DTOs for JSON deserialization - low risk") +package com.github.copilot.sdk.json; diff --git a/java/src/main/java/com/github/copilot/sdk/package-info.java b/java/src/main/java/com/github/copilot/sdk/package-info.java new file mode 100644 index 000000000..f775d575f --- /dev/null +++ b/java/src/main/java/com/github/copilot/sdk/package-info.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Core classes for the GitHub Copilot SDK for Java. + * + *

+ * This package provides the main entry points for interacting with GitHub + * Copilot programmatically. The SDK enables Java applications to leverage + * Copilot's agentic capabilities, including multi-turn conversations, tool + * execution, and AI-powered code generation. + * + *

Main Classes

+ *
    + *
  • {@link com.github.copilot.sdk.CopilotClient} - The main client for + * connecting to and communicating with the Copilot CLI. Manages the lifecycle + * of the CLI process and provides methods for creating sessions, querying + * models, and checking authentication status.
  • + *
  • {@link com.github.copilot.sdk.CopilotSession} - Represents a single + * conversation session with Copilot. Sessions maintain context across multiple + * messages and support streaming responses, tool invocations, and event + * handling.
  • + *
  • {@link com.github.copilot.sdk.JsonRpcClient} - Low-level JSON-RPC client + * for communication with the Copilot CLI process.
  • + *
+ * + *

Quick Start

+ * + *
{@code
+ * try (var client = new CopilotClient()) {
+ * 	client.start().get();
+ *
+ * 	var session = client.createSession(new SessionConfig().setModel("gpt-4.1")).get();
+ *
+ * 	session.on(AssistantMessageEvent.class, msg -> {
+ * 		System.out.println(msg.getData().content());
+ * 	});
+ *
+ * 	session.send(new MessageOptions().setPrompt("Hello, Copilot!")).get();
+ * }
+ * }
+ * + *

Related Packages

+ *
    + *
  • {@link com.github.copilot.sdk.generated} - Auto-generated event types + * emitted during session processing
  • + *
  • {@link com.github.copilot.sdk.json} - Configuration and data transfer + * objects
  • + *
+ * + * @see com.github.copilot.sdk.CopilotClient + * @see com.github.copilot.sdk.CopilotSession + * @see GitHub + * Repository + */ +package com.github.copilot.sdk; diff --git a/java/src/main/java/module-info.java b/java/src/main/java/module-info.java new file mode 100644 index 000000000..d912fb420 --- /dev/null +++ b/java/src/main/java/module-info.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * GitHub Copilot SDK for Java. + */ +module com.github.copilot.sdk.java { + requires transitive com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires transitive com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.datatype.jsr310; + requires static com.github.spotbugs.annotations; + requires static java.compiler; + requires static java.net.http; + requires java.logging; + + exports com.github.copilot.sdk; + exports com.github.copilot.sdk.generated; + exports com.github.copilot.sdk.generated.rpc; + exports com.github.copilot.sdk.json; + + opens com.github.copilot.sdk to com.fasterxml.jackson.databind; + opens com.github.copilot.sdk.generated to com.fasterxml.jackson.databind; + opens com.github.copilot.sdk.json to com.fasterxml.jackson.databind; +} diff --git a/java/src/site/jacoco-resources/report.css b/java/src/site/jacoco-resources/report.css new file mode 100644 index 000000000..585ab6821 --- /dev/null +++ b/java/src/site/jacoco-resources/report.css @@ -0,0 +1,299 @@ +/* ===== Custom JaCoCo Report Theme ===== */ +/* Matches the Copilot SDK site design */ + +body, td { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-size: 10pt; + color: #24292f; + -webkit-font-smoothing: antialiased; +} + +body { + background: #f6f8fa; + margin: 0; + padding: 20px; +} + +h1 { + font-weight: 800; + font-size: 18pt; + color: #24292f; + margin-bottom: 16px; +} + +a { + color: #0969da; + text-decoration: none; +} + +a:hover { + color: #0550ae; + text-decoration: underline; +} + +/* ===== Breadcrumb ===== */ +.breadcrumb { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 10px 16px; + margin-bottom: 20px; +} + +.breadcrumb .info { + float: right; +} + +.breadcrumb .info a { + margin-left: 8px; + color: #57606a; + font-size: 0.9em; +} + +.breadcrumb .info a:hover { + color: #0969da; +} + +/* ===== Element Icons ===== */ +.el_report { + padding-left: 18px; + background-image: url(report.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_group { + padding-left: 18px; + background-image: url(group.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_bundle { + padding-left: 18px; + background-image: url(bundle.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_package { + padding-left: 18px; + background-image: url(package.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_class { + padding-left: 18px; + background-image: url(class.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_source { + padding-left: 18px; + background-image: url(source.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_method { + padding-left: 18px; + background-image: url(method.gif); + background-position: left center; + background-repeat: no-repeat; +} + +.el_session { + padding-left: 18px; + background-image: url(session.gif); + background-position: left center; + background-repeat: no-repeat; +} + +/* ===== Source Code ===== */ +pre.source { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + overflow-x: auto; +} + +pre.source ol { + margin-bottom: 0; + margin-top: 0; +} + +pre.source li { + border-left: 1px solid #d0d7de; + color: #8b949e; + padding-left: 0; +} + +pre.source span.fc { + background-color: #dafbe1; +} + +pre.source span.nc { + background-color: #ffeef0; +} + +pre.source span.pc { + background-color: #fff8c5; +} + +pre.source span.bfc { + background-image: url(branchfc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bfc:hover { + background-color: #aff5b4; +} + +pre.source span.bnc { + background-image: url(branchnc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bnc:hover { + background-color: #ffcecb; +} + +pre.source span.bpc { + background-image: url(branchpc.gif); + background-repeat: no-repeat; + background-position: 2px center; +} + +pre.source span.bpc:hover { + background-color: #fff2b2; +} + +/* ===== Coverage Table ===== */ +table.coverage { + empty-cells: show; + border-collapse: separate; + border-spacing: 0; + border: 1px solid #d0d7de; + border-radius: 10px; + overflow: hidden; + width: 100%; + background: #fff; +} + +table.coverage thead { + background: #f6f8fa; +} + +table.coverage thead td { + white-space: nowrap; + padding: 10px 14px 10px 10px; + border-bottom: 2px solid #d0d7de; + font-weight: 700; + color: #24292f; + font-size: 0.92em; +} + +table.coverage thead td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage thead td.ctr1 { + text-align: right; + border-left: 1px solid #eaeef2; +} + +table.coverage thead td.ctr2 { + text-align: right; + padding-left: 2px; +} + +table.coverage thead td.sortable { + cursor: pointer; + background-image: url(sort.gif); + background-position: right center; + background-repeat: no-repeat; +} + +table.coverage thead td.up { + background-image: url(up.gif); +} + +table.coverage thead td.down { + background-image: url(down.gif); +} + +table.coverage tbody td { + white-space: nowrap; + padding: 8px 10px; + border-bottom: 1px solid #eaeef2; +} + +table.coverage tbody tr:hover { + background: rgba(102, 126, 234, 0.04) !important; +} + +table.coverage tbody td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage tbody td.ctr1 { + text-align: right; + padding-right: 14px; + border-left: 1px solid #eaeef2; +} + +table.coverage tbody td.ctr2 { + text-align: right; + padding-right: 14px; + padding-left: 2px; +} + +table.coverage tfoot td { + white-space: nowrap; + padding: 8px 10px; + font-weight: 700; + background: #f6f8fa; + border-top: 2px solid #d0d7de; +} + +table.coverage tfoot td.bar { + border-left: 1px solid #eaeef2; +} + +table.coverage tfoot td.ctr1 { + text-align: right; + padding-right: 14px; + border-left: 1px solid #eaeef2; +} + +table.coverage tfoot td.ctr2 { + text-align: right; + padding-right: 14px; + padding-left: 2px; +} + +/* ===== Footer ===== */ +.footer { + margin-top: 24px; + border-top: 1px solid #d0d7de; + padding-top: 8px; + font-size: 8pt; + color: #8b949e; +} + +.footer a { + color: #8b949e; +} + +.footer a:hover { + color: #0969da; +} + +.right { + float: right; +} diff --git a/java/src/site/markdown/advanced.md b/java/src/site/markdown/advanced.md new file mode 100644 index 000000000..5f15d726b --- /dev/null +++ b/java/src/site/markdown/advanced.md @@ -0,0 +1,1412 @@ +# 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) +- [Mode Handlers](#Mode_Handlers) + - [Exit Plan Mode](#Exit_Plan_Mode) + - [Auto Mode Switch](#Auto_Mode_Switch) +- [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. + +### Model Overrides + +Use `modelId` and `wireModel` to control model resolution and the model name on the wire: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setProvider(new ProviderConfig() + .setType("openai") + .setBaseUrl("https://api.openai.com/v1") + .setApiKey("sk-...") + .setModelId("gpt-4o") // Runtime config lookup + .setWireModel("my-finetune-v3") // Sent to the provider API + .setMaxPromptTokens(100_000) // Override max prompt tokens + .setMaxOutputTokens(4096)) // Override max output tokens +).get(); +``` + +- **`modelId`** β€” Well-known model name used by the runtime to look up agent configuration (tools, prompts, reasoning behavior) and default token limits. Also used as the wire model when `wireModel` is not set. +- **`wireModel`** β€” Model name sent to the provider API for inference. Use when the provider's model name (e.g., an Azure deployment name or a custom fine-tune name) differs from `modelId`. +- **`maxPromptTokens`** β€” Overrides the resolved model's default max prompt tokens. The runtime triggers conversation compaction when the prompt would exceed this limit. +- **`maxOutputTokens`** β€” Overrides the resolved model's default max output tokens. + +### 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 | +| `skills` | List<String> | Skill names to preload into the agent's context | +| `model` | String | Model identifier for this agent (e.g., "claude-haiku-4.5"); falls back to the parent session model if unavailable | + +### 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(); +``` + +--- + +## Instruction Directories + +Provide additional directories containing custom instruction files. These instructions are automatically included in the system message for all conversations in the session. + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +).get(); +``` + +Instruction files are discovered from `.github/instructions/` subdirectories within each specified path and should use the `.instructions.md` extension. + +This is also supported on session resume: + +```java +var session = client.resumeSession(sessionId, + new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setInstructionDirectories(List.of("/path/to/instructions")) +); +``` + +--- + +## 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. + +--- + +## Mode Handlers + +Mode handlers let your application respond to mode transitions requested by the Copilot CLI. + +### Exit Plan Mode + +When the model finishes creating a plan and wants to transition out of plan mode, it invokes the `exitPlanMode` handler. Register the handler via `SessionConfig.setOnExitPlanMode()`: + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnExitPlanMode((request, invocation) -> { + System.out.println("Plan summary: " + request.getSummary()); + System.out.println("Available actions: " + request.getActions()); + System.out.println("Recommended: " + request.getRecommendedAction()); + + return CompletableFuture.completedFuture( + new ExitPlanModeResult() + .setApproved(true) + .setSelectedAction("interactive") + .setFeedback("Looks good, proceed!")); + })).get(); +``` + +When no handler is registered, the SDK automatically approves the plan (`approved=true`). The handler receives an `ExitPlanModeRequest` with: + +| Field | Description | +|---------------------|-------------------------------------------------| +| `summary` | Summary of the plan that was created | +| `planContent` | Full content of the plan file | +| `actions` | Available actions (e.g., interactive, autopilot) | +| `recommendedAction` | The recommended action for the user | + +### Auto Mode Switch + +When the model encounters a rate limit or similar constraint, it may request to switch modes automatically. Register the handler via `SessionConfig.setOnAutoModeSwitch()`: + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnAutoModeSwitch((request, invocation) -> { + System.out.println("Error: " + request.getErrorCode()); + System.out.println("Retry after: " + request.getRetryAfterSeconds() + "s"); + + return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES); + })).get(); +``` + +When no handler is registered, the SDK returns `NO` (declining the mode switch). The response options are: + +| Response | Description | +|---------------------------------|------------------------------------------| +| `AutoModeSwitchResponse.YES` | Allow the mode switch this time | +| `AutoModeSwitchResponse.YES_ALWAYS`| Always allow automatic mode switches | +| `AutoModeSwitchResponse.NO` | Decline the mode switch | + +Both handlers are also available on `ResumeSessionConfig` for resumed sessions. + +--- + +## 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. + +--- + +## Remote Sessions + +Remote sessions enable Mission Control integration, making sessions accessible from GitHub web and mobile. When enabled, sessions in a GitHub repository working directory receive a remote URL. + +### Enabling Remote Sessions + +Set `remote(true)` on the client options to enable remote session support for all sessions: + +```java +var options = new CopilotClientOptions() + .setRemote(true) + .setCwd("/path/to/github-repo"); + +try (var client = new CopilotClient(options)) { + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + ).get(); + + // Listen for the remote URL info event + session.on(SessionInfoEvent.class, event -> { + System.out.println("Remote URL: " + event.getData()); + }); +} +``` + +### Prerequisites + +- The user must be authenticated (GitHub token or logged-in user) +- The session's working directory must be a GitHub repository +- This option is only used when the SDK spawns the CLI process; it is ignored when connecting to an external server via `setCliUrl()` + +### Cloud Sessions + +Cloud sessions create a remote session in the cloud instead of a local session. Optionally associate repository metadata with the cloud session: + +```java +var cloudOptions = new CloudSessionOptions() + .setRepository(new CloudSessionRepository() + .setOwner("my-org") + .setName("my-repo") + .setBranch("main")); + +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(cloudOptions) +).get(); +``` + +See [CloudSessionOptions](apidocs/com/github/copilot/sdk/json/CloudSessionOptions.html) and [CloudSessionRepository](apidocs/com/github/copilot/sdk/json/CloudSessionRepository.html) Javadoc for details. + +--- + +## 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 diff --git a/java/src/site/markdown/cookbook/README.md b/java/src/site/markdown/cookbook/README.md new file mode 100644 index 000000000..0b51c0377 --- /dev/null +++ b/java/src/site/markdown/cookbook/README.md @@ -0,0 +1,38 @@ +# GitHub Copilot SDK Cookbook β€” Java + +This folder hosts short, practical recipes for using the GitHub Copilot SDK with Java. Each recipe is concise, copy‑pasteable, and points to fuller examples and tests. + +## Prerequisites + +All cookbook examples can be run directly using [JBang](https://www.jbang.dev/), which allows you to run Java code without a full project setup. + +**Install JBang:** + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +For other installation methods, see the [JBang installation guide](https://www.jbang.dev/download/). + +## Recipes + +- [Error Handling](error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. +- [Multiple Sessions](multiple-sessions.md): Manage multiple independent conversations simultaneously. +- [Managing Local Files](managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. +- [PR Visualization](pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. +- [Persisting Sessions](persisting-sessions.md): Save and resume sessions across restarts. + +## Contributing + +Add a new recipe by creating a markdown file in this folder and linking it above. Follow repository guidance in the [main README](https://github.com/github/copilot-sdk-java#contributing). + +## Status + +These recipes are complete, practical examples and can be used directly or adapted for your own projects. diff --git a/java/src/site/markdown/cookbook/error-handling.md b/java/src/site/markdown/cookbook/error-handling.md new file mode 100644 index 000000000..963b8b093 --- /dev/null +++ b/java/src/site/markdown/cookbook/error-handling.md @@ -0,0 +1,281 @@ +# Error Handling Patterns + +Handle errors gracefully in your Copilot SDK applications. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) to run these examples: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +## Example scenario + +You need to handle various error conditions like connection failures, timeouts, and invalid responses. + +## Basic error handling + +**Usage:** +```bash +jbang BasicErrorHandling.java +``` + +**Code:** +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class BasicErrorHandling { + public static void main(String[] args) { + try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); + + session.on(AssistantMessageEvent.class, msg -> { + System.out.println(msg.getData().content()); + }); + + session.sendAndWait(new MessageOptions() + .setPrompt("Hello!")).get(); + + session.close(); + } catch (Exception ex) { + System.err.println("Error: " + ex.getMessage()); + ex.printStackTrace(); + } + } +} +``` + +## Handling specific error types + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import java.util.concurrent.ExecutionException; + +public class SpecificErrorHandling { + public static void startClient() { + try (var client = new CopilotClient()) { + client.start().get(); + // ... use client ... + } catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof java.io.IOException) { + System.err.println("Copilot CLI not found. Please install it first."); + System.err.println("Details: " + cause.getMessage()); + } else if (cause instanceof java.util.concurrent.TimeoutException) { + System.err.println("Could not connect to Copilot CLI server."); + System.err.println("Details: " + cause.getMessage()); + } else { + System.err.println("Unexpected error: " + cause.getMessage()); + cause.printStackTrace(); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + System.err.println("Operation interrupted: " + ex.getMessage()); + } catch (Exception ex) { + System.err.println("Unexpected error: " + ex.getMessage()); + ex.printStackTrace(); + } + } +} +``` + +## Timeout handling + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotSession; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class TimeoutHandling { + public static void sendWithTimeout(CopilotSession session) { + try { + session.on(AssistantMessageEvent.class, msg -> { + System.out.println(msg.getData().content()); + }); + + // Wait up to 30 seconds for response + session.sendAndWait(new MessageOptions() + .setPrompt("Complex question...")) + .get(30, TimeUnit.SECONDS); + + } catch (TimeoutException ex) { + System.err.println("Request timed out"); + } catch (Exception ex) { + System.err.println("Error: " + ex.getMessage()); + } + } +} +``` + +## Aborting a request + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotSession; +import com.github.copilot.sdk.json.MessageOptions; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class AbortRequest { + public static void abortAfterDelay(CopilotSession session) { + // Start a request (non-blocking) + session.send(new MessageOptions() + .setPrompt("Write a very long story...")); + + // Schedule abort after 5 seconds + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(() -> { + try { + session.abort().get(); + System.out.println("Request aborted"); + } catch (Exception ex) { + System.err.println("Failed to abort: " + ex.getMessage()); + } finally { + scheduler.shutdown(); + } + }, 5, TimeUnit.SECONDS); + } +} +``` + +## Graceful shutdown + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; + +public class GracefulShutdown { + public static void main(String[] args) { + var client = new CopilotClient(); + + // Set up shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nShutting down..."); + try { + client.close(); + } catch (Exception ex) { + System.err.println("Error during shutdown: " + ex.getMessage()); + } + })); + + try { + client.start().get(); + // ... do work ... + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} +``` + +## Try-with-resources pattern + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class TryWithResources { + public static void doWork() throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + try (var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get()) { + + session.on(AssistantMessageEvent.class, msg -> { + System.out.println(msg.getData().content()); + }); + + session.sendAndWait(new MessageOptions() + .setPrompt("Hello!")).get(); + + // Session and client are automatically closed + } + } + } +} +``` + +## Handling tool errors + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; +import java.util.Map; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class ToolErrorHandling { + public static void handleToolErrors() throws Exception { + var errorTool = ToolDefinition.create( + "get_user_location", + "Gets the user's location", + Map.of("type", "object", "properties", Map.of()), + invocation -> { + // Return an error result + return CompletableFuture.completedFuture( + ToolResultObject.error("Location service unavailable") + ); + } + ); + + try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(errorTool))).get(); + + session.on(AssistantMessageEvent.class, msg -> { + System.out.println(msg.getData().content()); + }); + + // Session continues even when tool fails + session.sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(); + + session.close(); + } + } +} +``` + +## Best practices + +1. **Always clean up**: Use try-with-resources to ensure `close()` is called +2. **Handle connection errors**: The CLI might not be installed or running +3. **Set appropriate timeouts**: Use `get(timeout, TimeUnit)` for long-running requests +4. **Log errors**: Capture error details for debugging +5. **Wrap operations**: Consider wrapping SDK operations in methods that handle common errors +6. **Check error causes**: Use `ExecutionException.getCause()` to get the actual error from `CompletableFuture` diff --git a/java/src/site/markdown/cookbook/managing-local-files.md b/java/src/site/markdown/cookbook/managing-local-files.md new file mode 100644 index 000000000..2a7b5540f --- /dev/null +++ b/java/src/site/markdown/cookbook/managing-local-files.md @@ -0,0 +1,213 @@ +# Grouping Files by Metadata + +Use Copilot to intelligently organize files in a folder based on their metadata. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) to run these examples: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +## Example scenario + +You have a folder with many files and want to organize them into subfolders based on metadata like file type, creation date, size, or other attributes. Copilot can analyze the files and suggest or execute a grouping strategy. + +## Example code + +**Usage:** +```bash +# Use with a specific folder (recommended) +jbang ManagingLocalFiles.java /path/to/your/folder + +# Or run without arguments to use a safe default (temp directory) +jbang ManagingLocalFiles.java +``` + +**Code:** +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.generated.ToolExecutionStartEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import java.nio.file.Paths; +import java.util.concurrent.CountDownLatch; + +public class ManagingLocalFiles { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Create session + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); + + // Set up event handlers + var done = new CountDownLatch(1); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nCopilot: " + msg.getData().content()) + ); + + session.on(ToolExecutionStartEvent.class, evt -> + System.out.println(" β†’ Running: " + evt.getData().toolName()) + ); + + session.on(ToolExecutionCompleteEvent.class, evt -> + System.out.println(" βœ“ Completed: " + evt.getData().toolCallId()) + ); + + session.on(SessionIdleEvent.class, evt -> done.countDown()); + + // Ask Copilot to organize files - using a safe example folder + // For real use, replace with your target folder + String targetFolder = args.length > 0 ? args[0] : + System.getProperty("java.io.tmpdir") + "/example-files"; + + String prompt = String.format(""" + Analyze the files in "%s" and show how you would organize them into subfolders. + + 1. First, list all files and their metadata + 2. Preview grouping by file extension + 3. Suggest appropriate subfolders (e.g., "images", "documents", "videos") + + IMPORTANT: DO NOT move any files. Only show the plan. + """, targetFolder); + + session.send(new MessageOptions().setPrompt(prompt)); + + // Wait for completion + done.await(); + + session.close(); + } + } +} +``` + +## Grouping strategies + +### By file extension + +```java +// Groups files like: +// images/ -> .jpg, .png, .gif +// documents/ -> .pdf, .docx, .txt +// videos/ -> .mp4, .avi, .mov +``` + +### By creation date + +```java +// Groups files like: +// 2024-01/ -> files created in January 2024 +// 2024-02/ -> files created in February 2024 +``` + +### By file size + +```java +// Groups files like: +// tiny-under-1kb/ +// small-under-1mb/ +// medium-under-100mb/ +// large-over-100mb/ +``` + +## Dry-run mode + +For safety, you can ask Copilot to only preview changes: + +```java +String prompt = String.format(""" + Analyze files in "%s" and show me how you would organize them + by file type. DO NOT move any files - just show me the plan. + """, targetFolder); + +session.send(new MessageOptions().setPrompt(prompt)); +``` + +## Custom grouping with AI analysis + +Let Copilot determine the best grouping based on file content: + +```java +String prompt = String.format(""" + Look at the files in "%s" and suggest a logical organization. + Consider: + - File names and what they might contain + - File types and their typical uses + - Date patterns that might indicate projects or events + + Propose folder names that are descriptive and useful. + """, targetFolder); + +session.send(new MessageOptions().setPrompt(prompt)); +``` + +## Interactive file organization + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import java.io.BufferedReader; +import java.io.InputStreamReader; + +public class InteractiveFileOrganizer { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient(); + var reader = new BufferedReader(new InputStreamReader(System.in))) { + + client.start().get(); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nCopilot: " + msg.getData().content()) + ); + + System.out.print("Enter folder path to organize: "); + String folderPath = reader.readLine(); + + String initialPrompt = String.format(""" + Analyze the files in "%s" and suggest an organization strategy. + Wait for my confirmation before making any changes. + """, folderPath); + + session.send(new MessageOptions().setPrompt(initialPrompt)); + + // Interactive loop + System.out.println("\nEnter commands (or 'exit' to quit):"); + String line; + while ((line = reader.readLine()) != null) { + if (line.equalsIgnoreCase("exit")) { + break; + } + session.send(new MessageOptions().setPrompt(line)); + } + + session.close(); + } + } +} +``` + +## Safety considerations + +1. **Confirm before moving**: Ask Copilot to confirm before executing moves +2. **Handle duplicates**: Consider what happens if a file with the same name exists +3. **Preserve originals**: Consider copying instead of moving for important files +4. **Test with dry-run**: Always test with a dry-run first to preview the changes diff --git a/java/src/site/markdown/cookbook/multiple-sessions.md b/java/src/site/markdown/cookbook/multiple-sessions.md new file mode 100644 index 000000000..84da5a255 --- /dev/null +++ b/java/src/site/markdown/cookbook/multiple-sessions.md @@ -0,0 +1,235 @@ +# Working with Multiple Sessions + +Manage multiple independent conversations simultaneously. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) to run these examples: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +## Example scenario + +You need to run multiple conversations in parallel, each with its own context and history. + +## Java + +**Usage:** +```bash +jbang MultipleSessions.java +``` + +**Code:** +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class MultipleSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Create multiple independent sessions + var session1 = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); + var session2 = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")).get(); + var session3 = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")).get(); + + // Set up event handlers for each session + session1.on(AssistantMessageEvent.class, msg -> + System.out.println("Session 1: " + msg.getData().content())); + session2.on(AssistantMessageEvent.class, msg -> + System.out.println("Session 2: " + msg.getData().content())); + session3.on(AssistantMessageEvent.class, msg -> + System.out.println("Session 3: " + msg.getData().content())); + + // Each session maintains its own conversation history + session1.send(new MessageOptions() + .setPrompt("You are helping with a Python project")); + session2.send(new MessageOptions() + .setPrompt("You are helping with a TypeScript project")); + session3.send(new MessageOptions() + .setPrompt("You are helping with a Go project")); + + // Follow-up messages stay in their respective contexts + session1.send(new MessageOptions() + .setPrompt("How do I create a virtual environment?")); + session2.send(new MessageOptions() + .setPrompt("How do I set up tsconfig?")); + session3.send(new MessageOptions() + .setPrompt("How do I initialize a module?")); + + // Wait for all sessions to complete + Thread.sleep(5000); + + // Clean up + session1.close(); + session2.close(); + session3.close(); + } + } +} +``` + +## Custom session IDs + +Use custom IDs for easier tracking: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSessionId("user-123-chat") + .setModel("gpt-5") +).get(); + +System.out.println(session.getSessionId()); // "user-123-chat" +``` + +## Listing sessions + +```java +var sessions = client.listSessions().get(); +for (var sessionInfo : sessions) { + System.out.println("Session: " + sessionInfo.getSessionId()); +} +``` + +## Deleting sessions + +```java +// Delete a specific session +try { + client.deleteSession("user-123-chat").get(); +} catch (Exception ex) { + System.err.println("Failed to delete session: " + ex.getMessage()); +} +``` + +## Managing session lifecycle with CompletableFuture + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import java.util.concurrent.CompletableFuture; +import java.util.List; + +public class ParallelSessions { + public static void runParallelSessions(CopilotClient client) throws Exception { + // Create sessions in parallel + var sessionFutures = List.of( + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")), + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5")), + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5")) + ); + + // Wait for all sessions to be created + CompletableFuture.allOf(sessionFutures.toArray(new CompletableFuture[0])) + .get(); + + // Get the created sessions + var session1 = sessionFutures.get(0).get(); + var session2 = sessionFutures.get(1).get(); + var session3 = sessionFutures.get(2).get(); + + // Send messages in parallel + var messageFutures = List.of( + session1.sendAndWait(new MessageOptions().setPrompt("Question 1")), + session2.sendAndWait(new MessageOptions().setPrompt("Question 2")), + session3.sendAndWait(new MessageOptions().setPrompt("Question 3")) + ); + + // Wait for all responses + CompletableFuture.allOf(messageFutures.toArray(new CompletableFuture[0])) + .get(); + + // Clean up + session1.close(); + session2.close(); + session3.close(); + } +} +``` + +## Providing a custom Executor for parallel sessions + +By default, `CompletableFuture` operations run on `ForkJoinPool.commonPool()`, +which has limited parallelism (typically `Runtime.availableProcessors() - 1` +threads). When multiple sessions block waiting for CLI responses, those threads +are unavailable for other workβ€”a condition known as *pool starvation*. + +Use `CopilotClientOptions.setExecutor(Executor)` to supply a dedicated thread +pool so that SDK work does not compete with the rest of your application for +common-pool threads: + +```java +//DEPS com.github:copilot-sdk-java:${project.version} +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ParallelSessionsWithExecutor { + public static void main(String[] args) throws Exception { + ExecutorService pool = Executors.newFixedThreadPool(4); + try { + var options = new CopilotClientOptions().setExecutor(pool); + try (CopilotClient client = new CopilotClient(options)) { + client.start().get(); + + var s1 = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get(); + var s2 = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get(); + var s3 = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("claude-sonnet-4.5")).get(); + + CompletableFuture.allOf( + s1.sendAndWait(new MessageOptions().setPrompt("Question 1")), + s2.sendAndWait(new MessageOptions().setPrompt("Question 2")), + s3.sendAndWait(new MessageOptions().setPrompt("Question 3")) + ).get(); + + s1.close(); + s2.close(); + s3.close(); + } + } finally { + pool.shutdown(); + } + } +} +``` + +Passing `null` (or omitting `setExecutor` entirely) keeps the default +`ForkJoinPool.commonPool()` behaviour. The executor is used for all internal +`CompletableFuture.runAsync` / `supplyAsync` callsβ€”including client start/stop, +tool-call dispatch, permission dispatch, user-input dispatch, and hooks. + +## Use cases + +- **Multi-user applications**: One session per user +- **Multi-task workflows**: Separate sessions for different tasks +- **A/B testing**: Compare responses from different models +- **Parallel processing**: Process multiple requests simultaneously diff --git a/java/src/site/markdown/cookbook/persisting-sessions.md b/java/src/site/markdown/cookbook/persisting-sessions.md new file mode 100644 index 000000000..ef80fd3d0 --- /dev/null +++ b/java/src/site/markdown/cookbook/persisting-sessions.md @@ -0,0 +1,293 @@ +# Session Persistence and Resumption + +Save and restore conversation sessions across application restarts. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) to run these examples: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +## Example scenario + +You want users to be able to continue a conversation even after closing and reopening your application. + +## Creating a session with a custom ID + +**Usage:** +```bash +jbang PersistingSessions.java +``` + +**Code:** +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class PersistingSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Create session with a memorable ID + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSessionId("user-123-conversation") + .setModel("gpt-5") + ).get(); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println(msg.getData().content()) + ); + + session.sendAndWait(new MessageOptions() + .setPrompt("Let's discuss TypeScript generics")).get(); + + // Session ID is preserved + System.out.println("Session ID: " + session.getSessionId()); + + // Close session but keep data on disk + session.close(); + } + } +} +``` + +## Resuming a session + +```java +public class ResumeSession { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Resume the previous session + var session = client.resumeSession("user-123-conversation", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println(msg.getData().content()) + ); + + // Previous context is restored + session.sendAndWait(new MessageOptions() + .setPrompt("What were we discussing?")).get(); + + session.close(); + } + } +} +``` + +## Listing available sessions + +```java +public class ListSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var sessions = client.listSessions().get(); + for (var sessionInfo : sessions) { + System.out.println("Session: " + sessionInfo.getSessionId()); + } + } + } +} +``` + +## Deleting a session permanently + +```java +public class DeleteSession { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Remove session and all its data from disk + client.deleteSession("user-123-conversation").get(); + System.out.println("Session deleted"); + } + } +} +``` + +## Getting session history + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.UserMessageEvent; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; + +public class SessionHistory { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.resumeSession("user-123-conversation", new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + var messages = session.getMessages().get(); + for (var event : messages) { + // Print different event types appropriately + if (event instanceof AssistantMessageEvent msg) { + System.out.printf("[assistant] %s%n", msg.getData().content()); + } else if (event instanceof UserMessageEvent userMsg) { + System.out.printf("[user] %s%n", userMsg.getData().content()); + } else { + System.out.printf("[%s]%n", event.getType()); + } + } + + session.close(); + } + } +} +``` + +## Complete example with session management + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import java.util.Scanner; + +public class SessionManager { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient(); + var scanner = new Scanner(System.in)) { + + client.start().get(); + + System.out.println("Session Manager"); + System.out.println("1. Create new session"); + System.out.println("2. Resume existing session"); + System.out.println("3. List sessions"); + System.out.print("Choose an option: "); + + int choice = scanner.nextInt(); + scanner.nextLine(); // consume newline + + CopilotSession session = null; + + switch (choice) { + case 1: + System.out.print("Enter session ID: "); + String sessionId = scanner.nextLine(); + session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSessionId(sessionId) + .setModel("gpt-5") + ).get(); + System.out.println("Created session: " + sessionId); + break; + + case 2: + System.out.print("Enter session ID to resume: "); + String resumeId = scanner.nextLine(); + try { + session = client.resumeSession(resumeId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + System.out.println("Resumed session: " + resumeId); + } catch (Exception ex) { + System.err.println("Failed to resume session: " + ex.getMessage()); + return; + } + break; + + case 3: + var sessions = client.listSessions().get(); + System.out.println("\nAvailable sessions:"); + for (var s : sessions) { + System.out.println(" - " + s.getSessionId()); + } + return; + + default: + System.out.println("Invalid choice"); + return; + } + + if (session != null) { + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nCopilot: " + msg.getData().content()) + ); + + // Interactive conversation loop + System.out.println("\nStart chatting (type 'exit' to quit):"); + while (true) { + System.out.print("\nYou: "); + String input = scanner.nextLine(); + + if (input.equalsIgnoreCase("exit")) { + break; + } + + session.send(new MessageOptions().setPrompt(input)); + Thread.sleep(2000); // Give time for response + } + + session.close(); + } + } + } +} +``` + +## Checking if a session exists + +```java +public class CheckSession { + public static boolean sessionExists(CopilotClient client, String sessionId) { + try { + var sessions = client.listSessions().get(); + return sessions.stream() + .anyMatch(s -> s.getSessionId().equals(sessionId)); + } catch (Exception ex) { + return false; + } + } + + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + String sessionId = "user-123-conversation"; + + if (sessionExists(client, sessionId)) { + System.out.println("Session exists, resuming..."); + var session = client.resumeSession(sessionId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + // ... use session ... + session.close(); + } else { + System.out.println("Session doesn't exist, creating new one..."); + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setSessionId(sessionId).setModel("gpt-5") + ).get(); + // ... use session ... + session.close(); + } + } + } +} +``` + +## Best practices + +1. **Use meaningful session IDs**: Include user ID or context in the session ID (e.g., "user-123-chat", "task-456-review") +2. **Handle missing sessions**: Check if a session exists before resuming +3. **Clean up old sessions**: Periodically delete sessions that are no longer needed +4. **Error handling**: Always wrap resume operations in try-catch blocks +5. **Workspace awareness**: Sessions are tied to workspace paths, ensure consistency diff --git a/java/src/site/markdown/cookbook/pr-visualization.md b/java/src/site/markdown/cookbook/pr-visualization.md new file mode 100644 index 000000000..1036bb0f7 --- /dev/null +++ b/java/src/site/markdown/cookbook/pr-visualization.md @@ -0,0 +1,236 @@ +# Generating PR Age Charts + +Build an interactive CLI tool that visualizes pull request age distribution for a GitHub repository using Copilot's built-in capabilities. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) to run this example: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Linux/macOS (using curl) +curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Windows (using Scoop) +scoop install jbang +``` + +## Example scenario + +You want to understand how long PRs have been open in a repository. This tool detects the current Git repo or accepts a repo as input, then lets Copilot fetch PR data via the GitHub MCP Server and generate a chart image. + +## Usage + +```bash +# Auto-detect from current git repo +jbang PRVisualization.java + +# Specify a repo explicitly +jbang PRVisualization.java github/copilot-sdk +``` + +## Full example: PRVisualization.java + +```java +//DEPS com.github:copilot-sdk-java:1.0.0-beta-java.4 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.ToolExecutionStartEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.regex.Pattern; + +public class PRVisualization { + + public static void main(String[] args) throws Exception { + System.out.println("πŸ” PR Age Chart Generator\n"); + + // Determine the repository + String repo; + if (args.length > 0) { + repo = args[0]; + System.out.println("πŸ“¦ Using specified repo: " + repo); + } else if (isGitRepo()) { + String detected = getGitHubRemote(); + if (detected != null && !detected.isEmpty()) { + repo = detected; + System.out.println("πŸ“¦ Detected GitHub repo: " + repo); + } else { + System.out.println("⚠️ Git repo found but no GitHub remote detected."); + repo = promptForRepo(); + } + } else { + System.out.println("πŸ“ Not in a git repository."); + repo = promptForRepo(); + } + + if (repo == null || !repo.contains("/")) { + System.err.println("❌ Invalid repo format. Expected: owner/repo"); + System.exit(1); + } + + String[] parts = repo.split("/", 2); + String owner = parts[0]; + String repoName = parts[1]; + + // Create Copilot client + try (var client = new CopilotClient()) { + client.start().get(); + + String cwd = System.getProperty("user.dir"); + var systemMessage = String.format(""" + + You are analyzing pull requests for the GitHub repository: %s/%s + The current working directory is: %s + + + + - Use the GitHub MCP Server tools to fetch PR data + - Use your file and code execution tools to generate charts + - Save any generated images to the current working directory + - Be concise in your responses + + """, owner, repoName, cwd); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5") + .setSystemMessage(new SystemMessageConfig().setContent(systemMessage)) + ).get(); + + // Set up event handling + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nπŸ€– " + msg.getData().content() + "\n") + ); + + session.on(ToolExecutionStartEvent.class, evt -> + System.out.println(" βš™οΈ " + evt.getData().toolName()) + ); + + // Initial prompt - let Copilot figure out the details + System.out.println("\nπŸ“Š Starting analysis...\n"); + + String prompt = String.format(""" + Fetch the open pull requests for %s/%s from the last week. + Calculate the age of each PR in days. + Then generate a bar chart image showing the distribution of PR ages + (group them into sensible buckets like <1 day, 1-3 days, etc.). + Save the chart as "pr-age-chart.png" in the current directory. + Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale. + """, owner, repoName); + + session.send(new MessageOptions().setPrompt(prompt)); + + // Wait a bit for initial processing + Thread.sleep(10000); + + // Interactive loop + System.out.println("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n"); + System.out.println("Examples:"); + System.out.println(" - \"Expand to the last month\""); + System.out.println(" - \"Show me the 5 oldest PRs\""); + System.out.println(" - \"Generate a pie chart instead\""); + System.out.println(" - \"Group by author instead of age\""); + System.out.println(); + + try (var reader = new BufferedReader(new InputStreamReader(System.in))) { + while (true) { + System.out.print("You: "); + String input = reader.readLine(); + if (input == null) break; + input = input.trim(); + + if (input.isEmpty()) continue; + if (input.equalsIgnoreCase("exit") || input.equalsIgnoreCase("quit")) { + System.out.println("πŸ‘‹ Goodbye!"); + break; + } + + session.send(new MessageOptions().setPrompt(input)); + Thread.sleep(2000); // Give time for response + } + } + + session.close(); + } + } + + // ============================================================================ + // Git & GitHub Detection + // ============================================================================ + + private static boolean isGitRepo() { + try { + Process proc = Runtime.getRuntime().exec(new String[]{"git", "rev-parse", "--git-dir"}); + return proc.waitFor() == 0; + } catch (Exception e) { + return false; + } + } + + private static String getGitHubRemote() { + try { + Process proc = Runtime.getRuntime().exec(new String[]{"git", "remote", "get-url", "origin"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String remoteURL = reader.readLine(); + if (remoteURL == null) return null; + remoteURL = remoteURL.trim(); + + // Handle SSH: git@github.com:owner/repo.git + var sshPattern = Pattern.compile("git@github\\.com:(.+/.+?)(?:\\.git)?$"); + var sshMatcher = sshPattern.matcher(remoteURL); + if (sshMatcher.find()) { + return sshMatcher.group(1); + } + + // Handle HTTPS: https://github.com/owner/repo.git + var httpsPattern = Pattern.compile("https://github\\.com/(.+/.+?)(?:\\.git)?$"); + var httpsMatcher = httpsPattern.matcher(remoteURL); + if (httpsMatcher.find()) { + return httpsMatcher.group(1); + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private static String promptForRepo() throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + System.out.print("Enter GitHub repo (owner/repo): "); + String line = reader.readLine(); + if (line == null) { + throw new EOFException("End of input while reading repository name"); + } + return line.trim(); + } +} +``` + +## How it works + +1. **Repository detection**: Checks command-line argument β†’ git remote β†’ prompts user +2. **No custom tools**: Relies entirely on Copilot CLI's built-in capabilities: + - **GitHub MCP Server** - Fetches PR data from GitHub + - **File tools** - Saves generated chart images + - **Code execution** - Generates charts using Python/matplotlib or other methods +3. **Interactive session**: After initial analysis, user can ask for adjustments + +## Why this approach? + +| Aspect | Custom Tools | Built-in Copilot | +| --------------- | ----------------- | --------------------------------- | +| Code complexity | High | **Minimal** | +| Maintenance | You maintain | **Copilot maintains** | +| Flexibility | Fixed logic | **AI decides best approach** | +| Chart types | What you coded | **Any type Copilot can generate** | +| Data grouping | Hardcoded buckets | **Intelligent grouping** | diff --git a/java/src/site/markdown/documentation.md b/java/src/site/markdown/documentation.md new file mode 100644 index 000000000..7b0c958e3 --- /dev/null +++ b/java/src/site/markdown/documentation.md @@ -0,0 +1,737 @@ +# GitHub Copilot SDK for Java - Documentation + +This guide covers common use cases for the GitHub Copilot SDK for Java. For complete API details, see the [Javadoc](apidocs/index.html). + +## Table of Contents + +- [Basic Usage](#Basic_Usage) +- [Handling Responses](#Handling_Responses) +- [Troubleshooting Event Handling](#Troubleshooting_Event_Handling) +- [Event Types Reference](#Event_Types_Reference) +- [Streaming Responses](#Streaming_Responses) +- [Session Operations](#Session_Operations) +- [Choosing a Model](#Choosing_a_Model) +- [Reasoning Effort](#Reasoning_Effort) +- [Tool Filtering](#Tool_Filtering) +- [Working Directory](#Working_Directory) +- [Connection State & Diagnostics](#Connection_State__Diagnostics) +- [Message Delivery Mode](#Message_Delivery_Mode) +- [Session Management](#Session_Management) +- [SessionConfig Reference](#SessionConfig_Reference) + - [Cloning SessionConfig](#Cloning_SessionConfig) + +--- + +## Basic Usage + +Create a client, start a session, and send a message: + +```java +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. + +--- + +## Handling Responses + +### Simple Request-Response + +For straightforward interactions, use `sendAndWait()`: + +```java +var response = session.sendAndWait("What is the capital of France?").get(); +System.out.println(response.getData().content()); +``` + +### Event-Based Processing + +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](advanced.html#Custom_Event_Error_Handler) guide. + +## Troubleshooting Event Handling + +### Symptoms of policy misconfiguration + +- You registered multiple `session.on(...)` handlers, but only the first one runs +- A handler throws once and later handlers stop receiving events +- You expected "best effort" fan-out, but dispatch halts on errors + +### Fix + +Set the event error policy to suppress-and-continue: + +```java +session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); +``` + +Optionally add a custom error handler for observability: + +```java +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. + +```java +var done = new CompletableFuture(); + +// 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 completion +``` + +You can also use a single handler for all events: + +```java +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 -> { } + } +}); +``` + +### Key Event Types + +| Event | Description | +|-------|-------------| +| `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](#Event_Types_Reference) below. + +--- + +## Event Types Reference + +The SDK supports event types organized by category. All events extend `SessionEvent`. + +### Session Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### Assistant Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### Tool Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### User Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `UserMessageEvent` | `user.message` | User sent a message | +| `PendingMessagesModifiedEvent` | `pending_messages.modified` | Pending message queue changed | + +### Subagent Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### Other Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### External Tool Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `ExternalToolRequestedEvent` | `external_tool.requested` | An external tool invocation was requested | +| `ExternalToolCompletedEvent` | `external_tool.completed` | An external tool invocation completed | + +### Permission Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `PermissionRequestedEvent` | `permission.requested` | A permission request was issued | +| `PermissionCompletedEvent` | `permission.completed` | A permission request was resolved | + +### Command Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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 | + +### Elicitation Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `ElicitationRequestedEvent` | `elicitation.requested` | An elicitation (UI dialog) request was received | + +### Capability Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `CapabilitiesChangedEvent` | `capabilities.changed` | Session capabilities were updated | + +### Plan Mode Events + +| Event | Type String | Description | +|-------|-------------|-------------| +| `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](apidocs/com/github/copilot/sdk/generated/package-summary.html) for detailed event data structures. + +--- + +## Streaming Responses + +Enable streaming to receive response chunks as they're generated: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-4.1") + .setStreaming(true) +).get(); + +var done = new CompletableFuture(); + +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(); +``` + +--- + +## Session Operations + +### Get Conversation History + +Retrieve all messages and events from a session using `getMessages()`: + +```java +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: +- Displaying conversation history in a UI +- Persisting conversations for later review +- Debugging and logging session state + +### Abort Current Operation + +Cancel a long-running operation using `abort()`: + +```java +// 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: +- User-initiated cancellation in interactive applications +- Timeout handling in automated pipelines +- Graceful shutdown when application is closing + +### Custom Timeout + +Use `sendAndWait` with a custom timeout for CI/CD pipelines: + +```java +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(); + } +} +``` + +--- + +## Choosing a Model + +### List Available Models + +Query available models before creating a session: + +```java +var models = client.listModels().get(); + +for (var model : models) { + System.out.printf("%s (%s)%n", model.getId(), model.getName()); +} +``` + +### Use a Specific Model + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4") +).get(); +``` + +--- + +## Reasoning Effort + +For models that support it, control how much effort the model spends reasoning before responding: + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("o3-mini") + .setReasoningEffort("high") +).get(); +``` + +| Level | Description | +|-------|-------------| +| `"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. + +--- + +## Tool Filtering + +Control which built-in tools the assistant can use with allowlists and blocklists. + +### Allowlist (Available Tools) + +Restrict the session to only specific tools: + +```java +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. + +### Blocklist (Excluded Tools) + +Allow all tools except specific ones: + +```java +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. + +### Combining with Custom Tools + +Tool filtering applies to built-in tools. Your custom tools (registered via `setTools()`) are always available: + +```java +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(); +``` + +--- + +## Working Directory + +Set the working directory for file operations in the session: + +```java +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`. + +--- + +## Connection State & Diagnostics + +### Checking Connection State + +Query the client's connection state at any time without making a server call: + +```java +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: + +| State | Description | +|-------|-------------| +| `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) | + +### State Transitions + +``` + start() + DISCONNECTED ──────────► CONNECTING + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ success β”‚ β”‚ failure + β–Ό β”‚ β–Ό + CONNECTED β”‚ ERROR + β”‚ β”‚ + stop() / β”‚ β”‚ + forceStop() β”‚ β”‚ + β–Ό β”‚ + DISCONNECTED β—„β”€β”€β”€β”˜ + β”‚ + β”‚ start() (retry) + β–Ό + CONNECTING +``` + +- **DISCONNECTED β†’ CONNECTING:** `start()` was called +- **CONNECTING β†’ CONNECTED:** Server connection and protocol handshake succeeded +- **CONNECTING β†’ ERROR:** Connection or protocol verification failed +- **CONNECTED β†’ DISCONNECTED:** `stop()` or `forceStop()` was called, or `close()` via try-with-resources +- **DISCONNECTED β†’ CONNECTING:** `start()` can be called again to retry + +### Server Status + +Get CLI version and protocol information: + +```java +var status = client.getStatus().get(); +System.out.println("CLI version: " + status.getVersion()); +System.out.println("Protocol version: " + status.getProtocolVersion()); +``` + +### Authentication Status + +Check whether the current connection is authenticated and how: + +```java +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()); +} +``` + +### Ping + +Verify server connectivity: + +```java +var pong = client.ping("hello").get(); +System.out.println("Server responded, protocol version: " + pong.protocolVersion()); +``` + +See [ConnectionState](apidocs/com/github/copilot/sdk/ConnectionState.html), [GetStatusResponse](apidocs/com/github/copilot/sdk/json/GetStatusResponse.html), and [GetAuthStatusResponse](apidocs/com/github/copilot/sdk/json/GetAuthStatusResponse.html) Javadoc for details. + +--- + +## Message Delivery Mode + +Control how messages are delivered to the session: + +```java +// 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(); +``` + +| Mode | Description | +|------|-------------| +| `"enqueue"` | Queue the message for processing (default) | +| `"immediate"` | Process the message immediately | + +--- + +## Session Management + +### Multiple Concurrent Sessions + +```java +var session1 = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") +).get(); + +var session2 = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4") +).get(); + +// Send messages concurrently +var future1 = session1.sendAndWait("Summarize REST APIs"); +var future2 = session2.sendAndWait("Summarize GraphQL"); + +System.out.println("GPT: " + future1.get().getData().content()); +System.out.println("Claude: " + future2.get().getData().content()); +``` + +### Resume a Previous Session + +```java +// Get the last session ID +var lastSessionId = client.getLastSessionId().get(); + +// Or list all sessions +var sessions = client.listSessions().get(); + +// Resume a session +var session = client.resumeSession(lastSessionId, new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); +``` + +### Resume Options + +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. + +| Option | Description | +|--------|-------------| +| `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** + +```java +// 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](apidocs/com/github/copilot/sdk/json/ResumeSessionConfig.html) Javadoc for complete options. + +### Clean Up Sessions + +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: + +```java +// 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(); +``` + +--- + +## SessionConfig Reference + +Complete list of all `SessionConfig` options for `createSession()`: + +| Option | Type | Description | Guide | +|--------|------|-------------|-------| +| `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](#Choosing_a_Model) | +| `reasoningEffort` | String | Reasoning depth: `"low"`, `"medium"`, `"high"`, `"xhigh"` | [Reasoning Effort](#Reasoning_Effort) | +| `tools` | List<ToolDefinition> | Custom tools the assistant can invoke | [Custom Tools](advanced.html#Custom_Tools) | +| `systemMessage` | SystemMessageConfig | Customize assistant behavior | [System Messages](advanced.html#System_Messages) | +| `availableTools` | List<String> | Allowlist of built-in tool names | [Tool Filtering](#Tool_Filtering) | +| `excludedTools` | List<String> | Blocklist of built-in tool names | [Tool Filtering](#Tool_Filtering) | +| `provider` | ProviderConfig | BYOK API provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | +| `onPermissionRequest` | PermissionHandler | Handler for permission requests | [Permission Handling](advanced.html#Permission_Handling) | +| `onUserInputRequest` | UserInputHandler | Handler for user input requests | [User Input Handling](advanced.html#User_Input_Handling) | +| `hooks` | SessionHooks | Lifecycle and tool execution hooks | [Session Hooks](hooks.html) | +| `workingDirectory` | String | Working directory for file operations | [Working Directory](#Working_Directory) | +| `streaming` | boolean | Enable streaming response chunks | [Streaming Responses](#Streaming_Responses) | +| `mcpServers` | Map<String, Object> | MCP server configurations | [MCP Servers](mcp.html) | +| `customAgents` | List<CustomAgentConfig> | Custom agent definitions | [Custom Agents](advanced.html#Custom_Agents) | +| `agent` | String | Pre-select a custom agent at session start | [Custom Agents](advanced.html#Custom_Agents) | +| `infiniteSessions` | InfiniteSessionConfig | Auto-compaction for long conversations | [Infinite Sessions](advanced.html#Infinite_Sessions) | +| `skillDirectories` | List<String> | Directories to load skills from | [Skills](advanced.html#Skills_Configuration) | +| `disabledSkills` | List<String> | Skills to disable by name | [Skills](advanced.html#Skills_Configuration) | +| `configDir` | String | Custom configuration directory | [Config Dir](advanced.html#Custom_Configuration_Directory) | +| `commands` | List<CommandDefinition> | Slash command definitions | [Slash Commands](advanced.html#Slash_Commands) | +| `onElicitationRequest` | ElicitationHandler | Handler for incoming elicitation requests | [Elicitation](advanced.html#Elicitation_UI_Dialogs) | +| `onEvent` | Consumer<SessionEvent> | Event handler registered before session creation | [Early Event Registration](advanced.html#Early_Event_Registration) | + +### Cloning SessionConfig + +Use `clone()` to copy a base configuration before making per-session changes: + +```java +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](apidocs/com/github/copilot/sdk/json/SessionConfig.html) Javadoc for full details. + +--- + +## Next Steps + +- πŸ“– **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, System Messages, Infinite Sessions, Skills +- πŸ“– **[Session Hooks](hooks.html)** - Intercept tool execution and session lifecycle events +- πŸ“– **[MCP Servers](mcp.html)** - Integrate external tools via Model Context Protocol +- πŸ“– **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/java/src/site/markdown/getting-started.md b/java/src/site/markdown/getting-started.md new file mode 100644 index 000000000..724b1a2dd --- /dev/null +++ b/java/src/site/markdown/getting-started.md @@ -0,0 +1,378 @@ +!# Build Your First Copilot-Powered App + +In this tutorial, you'll use the GitHub Copilot SDK for Java to build a command-line assistant. You'll start with the basics, add streaming responses, then add custom tools - giving Copilot the ability to call your code. + +**What you'll build:** + +``` +You: What's the weather like in Seattle? +Copilot: Let me check the weather for Seattle... + Currently 62Β°F and cloudy with a chance of rain. + Typical Seattle weather! + +You: How about Tokyo? +Copilot: In Tokyo it's 75Β°F and sunny. Great day to be outside! +``` + +## Prerequisites + +Before you begin, make sure you have: + +- **Java 17+** installed +- **GitHub Copilot CLI** installed and authenticated ([Installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)) + +Verify the CLI is working: + +```bash +copilot --version +``` + +## Step 1: Install the SDK + +> If you want to test a _-SNAPSHOT_ version, you may have to clone the project's repository and install the library locally with _mvn install_. + +### Apache Maven + +Add the dependency to your `pom.xml`: + +```xml + + com.github + copilot-sdk-java + ${project.version} + +``` + +### Gradle + +Add the dependency to your `build.gradle`: + +```groovy +implementation 'com.github:copilot-sdk-java:${project.version}' +``` + +### JBang (Quick Start) + +For the fastest way to try the SDK without setting up a project, use [JBang](https://www.jbang.dev/). Create a file named `HelloCopilot.java` with the following header, and then proceed to Step 2 by appending the proposed content into this same file. + +```bash +//DEPS com.github:copilot-sdk-java:${project.version} +``` + +## Step 2: Send Your First Message + +Create a new file and add the following code. This is the simplest way to use the SDK. + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class HelloCopilot { + public static void main(String[] args) throws Exception { + 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( + new MessageOptions().setPrompt("What is 2 + 2?") + ).get(); + + System.out.println(response.getData().content()); + } + } +} +``` + +Run the code with your preferred build tool or IDE. + +Or run it directly with JBang: + +```bash +jbang HelloCopilot.java +``` + +**You should see:** + +``` +2 + 2 = 4 +``` + +Congratulations! You just built your first Copilot-powered app. + +## Step 3: Add Streaming Responses + +Right now, you wait for the complete response before seeing anything. Let's make it interactive by streaming the response as it's generated. + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import java.util.concurrent.CompletableFuture; + +public class StreamingExample { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-4.1") + .setStreaming(true) + ).get(); + + var done = new CompletableFuture(); + + // Listen for response chunks + session.on(AssistantMessageDeltaEvent.class, delta -> { + System.out.print(delta.getData().deltaContent()); + }); + session.on(SessionIdleEvent.class, idle -> { + System.out.println(); // New line when done + done.complete(null); + }); + + session.send( + new MessageOptions().setPrompt("Tell me a short joke") + ).get(); + + done.get(); // Wait for completion + } + } +} +``` + +Run the code again. You'll see the response appear word by word. + +## Step 4: Add a Custom Tool + +Now for the powerful part. Let's give Copilot the ability to call your code by defining a custom tool. We'll create a simple weather lookup tool. + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CompletableFuture; + +public class ToolExample { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Define a tool that Copilot can call + var getWeather = ToolDefinition.create( + "get_weather", + "Get the current weather for a city", + Map.of( + "type", "object", + "properties", Map.of( + "city", Map.of( + "type", "string", + "description", "The city name" + ) + ), + "required", List.of("city") + ), + invocation -> { + var params = invocation.getArguments(); + var city = (String) params.get("city"); + + // In a real app, you'd call a weather API here + String[] conditions = {"sunny", "cloudy", "rainy", "partly cloudy"}; + int temp = new Random().nextInt(30) + 50; + var condition = conditions[new Random().nextInt(conditions.length)]; + + return CompletableFuture.completedFuture(Map.of( + "city", city, + "temperature", temp + "Β°F", + "condition", condition + )); + } + ); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-4.1") + .setStreaming(true) + .setTools(List.of(getWeather)) + ).get(); + + var done = new CompletableFuture(); + + session.on(AssistantMessageDeltaEvent.class, delta -> { + System.out.print(delta.getData().deltaContent()); + }); + session.on(SessionIdleEvent.class, idle -> { + System.out.println(); + done.complete(null); + }); + + session.send( + new MessageOptions().setPrompt("What's the weather like in Seattle and Tokyo?") + ).get(); + + done.get(); + } + } +} +``` + +Run it and you'll see Copilot call your tool to get weather data, then respond with the results! + +## Step 5: Build an Interactive Assistant + +Let's put it all together into a useful interactive assistant: + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +public class WeatherAssistant { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient(); + var scanner = new Scanner(System.in)) { + + client.start().get(); + + var getWeather = ToolDefinition.create( + "get_weather", + "Get the current weather for a city", + Map.of( + "type", "object", + "properties", Map.of( + "city", Map.of( + "type", "string", + "description", "The city name" + ) + ), + "required", List.of("city") + ), + invocation -> { + var params = invocation.getArguments(); + var city = (String) params.get("city"); + String[] conditions = {"sunny", "cloudy", "rainy", "partly cloudy"}; + int temp = new Random().nextInt(30) + 50; + var condition = conditions[new Random().nextInt(conditions.length)]; + return CompletableFuture.completedFuture(Map.of( + "city", city, + "temperature", temp + "Β°F", + "condition", condition + )); + } + ); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-4.1") + .setStreaming(true) + .setTools(List.of(getWeather)) + ).get(); + + System.out.println("🌀️ Weather Assistant (type 'exit' to quit)"); + System.out.println(" Try: 'What's the weather in Paris?'\n"); + + var done = new AtomicReference>(); + + // Register listener once, outside the loop + session.on(AssistantMessageDeltaEvent.class, delta -> { + System.out.print(delta.getData().deltaContent()); + }); + session.on(SessionIdleEvent.class, idle -> { + System.out.println(); + var f = done.get(); + if (f != null) f.complete(null); + }); + + while (true) { + System.out.print("You: "); + String input = scanner.nextLine(); + + if ("exit".equalsIgnoreCase(input.trim())) { + break; + } + + done.set(new CompletableFuture<>()); + + System.out.print("Assistant: "); + session.send(new MessageOptions().setPrompt(input)).get(); + done.get().get(); + } + } + } +} +``` + +## What's Next? + +You've learned the core concepts of the Copilot SDK: + +- βœ… Creating a client and session +- βœ… Sending messages and receiving responses +- βœ… Streaming for real-time output +- βœ… Custom tools for extending Copilot's capabilities +- βœ… Building interactive applications + +**Explore more:** + +- [Documentation](documentation.html) - Basic usage and session management +- [Advanced Usage](advanced.html) - Tools, BYOK, infinite sessions, and more +- [MCP Servers](mcp.html) - Connect to Model Context Protocol servers + +## Troubleshooting + +### "Copilot CLI not found" + +Make sure the GitHub Copilot CLI is installed and in your PATH: + +```bash +copilot --version +``` + +If not installed, follow the [GitHub Copilot CLI installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli). + +### "Authentication failed" + +The CLI needs to be authenticated with your GitHub account: + +```bash +copilot auth login +``` + +### "Connection timeout" + +Check your internet connection and firewall settings. The SDK communicates with GitHub's Copilot service. + +--- + +## Next Steps + +- πŸ“– **[Documentation](documentation.html)** - Core concepts, events, streaming, session management +- πŸ“– **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, System Messages, Custom Agents +- πŸ“– **[Session Hooks](hooks.html)** - Intercept tool execution and session lifecycle events +- πŸ“– **[Setup & Deployment](setup.html)** - OAuth, backend services, scaling +- πŸ“– **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/java/src/site/markdown/hooks.md b/java/src/site/markdown/hooks.md new file mode 100644 index 000000000..392f3fbf9 --- /dev/null +++ b/java/src/site/markdown/hooks.md @@ -0,0 +1,427 @@ +# Session Hooks + +Session hooks allow you to intercept and modify tool execution, user prompts, and session lifecycle events. Use hooks to implement custom logic like logging, security controls, or context injection. + +--- + +## Overview + +| Hook | When It's Called | Can Modify | +|------|------------------|------------| +| [Pre-Tool Use](#Pre-Tool_Use_Hook) | Before a tool executes | Tool arguments, permission decision | +| [Post-Tool Use](#Post-Tool_Use_Hook) | After a tool executes | Tool result, additional context | +| [User Prompt Submitted](#User_Prompt_Submitted_Hook) | When user sends a message | Nothing (observation only) | +| [Session Start](#Session_Start_Hook) | When session begins | Nothing (observation only) | +| [Session End](#Session_End_Hook) | When session ends | Nothing (observation only) | +| [Checking Whether Hooks Are Registered](#Checking_Whether_Hooks_Are_Registered) | Before session creation | Whether any handlers are configured | + +--- + +## Quick Start + +Register hooks when creating a session: + +```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) + .setModel("gpt-4.1") + .setHooks(hooks) +).get(); +``` + +--- + +## Pre-Tool Use Hook + +Called **before** a tool executes. Use this to: +- Approve, deny, or prompt for tool execution +- Modify tool arguments +- Add context for the LLM +- Suppress tool output from being shown + +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `getSessionId()` | `String` | Runtime session ID of the session that triggered the hook | +| `getToolName()` | `String` | Name of the tool being called | +| `getToolArgs()` | `JsonNode` | Arguments passed to the tool | +| `getCwd()` | `String` | Current working directory | +| `getTimestamp()` | `long` | Timestamp in milliseconds | + +### Output + +| Field | Type | Description | +|-------|------|-------------| +| `setPermissionDecision(String)` | `"allow"`, `"deny"`, `"ask"` | Whether to execute the tool | +| `setPermissionDecisionReason(String)` | `String` | Reason shown to user/LLM | +| `setModifiedArgs(JsonNode)` | `JsonNode` | Modified arguments (optional) | +| `setAdditionalContext(String)` | `String` | Extra context for the LLM | +| `setSuppressOutput(Boolean)` | `Boolean` | Hide output from display | + +### Example: Security Gate + +Block dangerous tool calls: + +```java +var hooks = new SessionHooks() + .setOnPreToolUse((input, invocation) -> { + String tool = input.getToolName(); + + // Block file deletion + if (tool.equals("delete_file")) { + return CompletableFuture.completedFuture( + PreToolUseHookOutput.deny("File deletion is not allowed") + ); + } + + // Require confirmation for shell commands + if (tool.equals("run_terminal_cmd")) { + return CompletableFuture.completedFuture(PreToolUseHookOutput.ask()); + } + + // Allow everything else + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }); +``` + +### Example: Argument Modification + +Inject context into tool arguments: + +```java +var hooks = new SessionHooks() + .setOnPreToolUse((input, invocation) -> { + if (input.getToolName().equals("search_code")) { + // Add project root to search path + var mapper = new ObjectMapper(); + var modifiedArgs = mapper.createObjectNode(); + modifiedArgs.put("path", "/my/project/src"); + modifiedArgs.set("query", input.getToolArgs().get("query")); + + return CompletableFuture.completedFuture( + PreToolUseHookOutput.withModifiedArgs("allow", modifiedArgs) + ); + } + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }); +``` + +--- + +## Post-Tool Use Hook + +Called **after** a tool executes. Use this to: +- Log tool results +- Modify the result shown to the LLM +- Add additional context based on results +- Suppress output from display + +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `getSessionId()` | `String` | Runtime session ID of the session that triggered the hook | +| `getToolName()` | `String` | Name of the tool that was called | +| `getToolArgs()` | `JsonNode` | Arguments that were passed | +| `getToolResult()` | `JsonNode` | Result from the tool | +| `getCwd()` | `String` | Current working directory | +| `getTimestamp()` | `long` | Timestamp in milliseconds | + +### Output + +| Field | Type | Description | +|-------|------|-------------| +| `setModifiedResult(String)` | `String` | Modified result for the LLM | +| `setAdditionalContext(String)` | `String` | Extra context for the LLM | +| `setSuppressOutput(Boolean)` | `Boolean` | Hide output from display | + +### Example: Result Logging + +Log all tool executions: + +```java +var hooks = new SessionHooks() + .setOnPostToolUse((input, invocation) -> { + System.out.printf("[%d] %s completed%n", + input.getTimestamp(), + input.getToolName()); + System.out.println("Result: " + input.getToolResult()); + + return CompletableFuture.completedFuture(null); + }); +``` + +### Example: Result Enrichment + +Add context to file read results: + +```java +var hooks = new SessionHooks() + .setOnPostToolUse((input, invocation) -> { + if (input.getToolName().equals("read_file")) { + String context = "Note: This file was last modified 2 hours ago."; + return CompletableFuture.completedFuture( + new PostToolUseHookOutput(null, context, null) + ); + } + return CompletableFuture.completedFuture(null); + }); +``` + +--- + +## User Prompt Submitted Hook + +Called when the user submits a prompt, before the LLM processes it. This is an observation hook - you cannot modify the prompt. + +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `sessionId()` | `String` | Runtime session ID of the session that triggered the hook | +| `prompt()` | `String` | The user's prompt text | +| `timestamp()` | `long` | Timestamp in milliseconds | + +### Output + +Return `null` - this hook is observation-only. + +### Example: Prompt Logging + +```java +var hooks = new SessionHooks() + .setOnUserPromptSubmitted((input, invocation) -> { + System.out.println("User asked: " + input.prompt()); + + // Track prompts for analytics + analytics.track("user_prompt", Map.of( + "sessionId", invocation.getSessionId(), + "promptLength", input.prompt().length() + )); + + return CompletableFuture.completedFuture(null); + }); +``` + +--- + +## Session Start Hook + +Called when a session starts (either new or resumed). + +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `sessionId()` | `String` | Runtime session ID of the session that triggered the hook | +| `source()` | `String` | `"startup"`, `"resume"`, or `"new"` | +| `timestamp()` | `long` | Timestamp in milliseconds | + +### Output + +Return `null` - this hook is observation-only. + +### Example: Session Initialization + +```java +var hooks = new SessionHooks() + .setOnSessionStart((input, invocation) -> { + System.out.println("Session started: " + invocation.getSessionId()); + System.out.println("Source: " + input.source()); + + // Initialize session-specific resources + sessionResources.put(invocation.getSessionId(), new ResourceManager()); + + return CompletableFuture.completedFuture(null); + }); +``` + +--- + +## Session End Hook + +Called when a session ends. + +### Input + +| Field | Type | Description | +|-------|------|-------------| +| `sessionId()` | `String` | Runtime session ID of the session that triggered the hook | +| `reason()` | `String` | Why the session ended | +| `timestamp()` | `long` | Timestamp in milliseconds | + +### Output + +Return `null` - this hook is observation-only. + +### Example: Session Cleanup + +```java +var hooks = new SessionHooks() + .setOnSessionEnd((input, invocation) -> { + System.out.println("Session ended: " + input.reason()); + + // Clean up session resources + var resources = sessionResources.remove(invocation.getSessionId()); + if (resources != null) { + resources.close(); + } + + return CompletableFuture.completedFuture(null); + }); +``` + +--- + +## Complete Example + +Combining multiple hooks for comprehensive session control: + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; +import java.util.concurrent.CompletableFuture; + +public class HooksExample { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var hooks = new SessionHooks() + // Security: control tool execution + .setOnPreToolUse((input, invocation) -> { + System.out.println("β†’ " + input.getToolName()); + + // Deny dangerous operations + if (input.getToolName().contains("delete")) { + return CompletableFuture.completedFuture( + PreToolUseHookOutput.deny("Deletion not allowed") + ); + } + + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }) + + // Logging: track tool results + .setOnPostToolUse((input, invocation) -> { + System.out.println("← " + input.getToolName() + " completed"); + return CompletableFuture.completedFuture(null); + }) + + // Analytics: track user prompts + .setOnUserPromptSubmitted((input, invocation) -> { + System.out.println("User: " + input.prompt()); + return CompletableFuture.completedFuture(null); + }) + + // Lifecycle: initialization and cleanup + .setOnSessionStart((input, invocation) -> { + System.out.println("Session started (" + input.source() + ")"); + return CompletableFuture.completedFuture(null); + }) + .setOnSessionEnd((input, invocation) -> { + System.out.println("Session ended: " + input.reason()); + return CompletableFuture.completedFuture(null); + }); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-4.1") + .setHooks(hooks) + ).get(); + + var response = session.sendAndWait("List files in /tmp").get(); + System.out.println(response.getData().content()); + + session.close(); + } + } +} +``` + +--- + +## Hook Invocation Object + +All hook handlers receive a `HookInvocation` object as the second parameter: + +| Method | Description | +|--------|-------------| +| `getSessionId()` | The session ID where the hook was triggered | + +This allows you to correlate hooks with specific sessions when managing multiple concurrent sessions. + +## Checking Whether Hooks Are Registered + +Use `hasHooks()` to quickly verify that at least one hook handler is configured: + +```java +var hooks = new SessionHooks() + .setOnPreToolUse((input, invocation) -> + CompletableFuture.completedFuture(PreToolUseHookOutput.allow())); + +if (hooks.hasHooks()) { + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setHooks(hooks) + ).get(); +} +``` + +--- + +## Error Handling + +If a hook throws an exception, the SDK logs the error and continues with default behavior: +- Pre-tool hooks default to allowing execution +- Post-tool hooks have no effect on the result +- Lifecycle hooks are observation-only + +To handle errors gracefully in your hooks: + +```java +.setOnPreToolUse((input, invocation) -> { + try { + // Your logic here + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + } catch (Exception e) { + logger.error("Hook error", e); + // Fail-safe: deny if something goes wrong + return CompletableFuture.completedFuture( + PreToolUseHookOutput.deny("Internal error") + ); + } +}) +``` + +--- + +## See Also + +- [SessionHooks Javadoc](apidocs/com/github/copilot/sdk/json/SessionHooks.html) +- [PreToolUseHookInput Javadoc](apidocs/com/github/copilot/sdk/json/PreToolUseHookInput.html) +- [PreToolUseHookOutput Javadoc](apidocs/com/github/copilot/sdk/json/PreToolUseHookOutput.html) +- [PostToolUseHookInput Javadoc](apidocs/com/github/copilot/sdk/json/PostToolUseHookInput.html) +- [PostToolUseHookOutput Javadoc](apidocs/com/github/copilot/sdk/json/PostToolUseHookOutput.html) + +--- + +## Next Steps + +- πŸ“– **[Documentation](documentation.html)** - Core concepts, events, session management +- πŸ“– **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP Servers, Custom Agents +- πŸ“– **[MCP Servers](mcp.html)** - Integrate external tools via Model Context Protocol +- πŸ“– **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/java/src/site/markdown/index.md b/java/src/site/markdown/index.md new file mode 100644 index 000000000..a097da69e --- /dev/null +++ b/java/src/site/markdown/index.md @@ -0,0 +1,123 @@ +# GitHub Copilot SDK for Java + +> ℹ️ **Public Preview:** This is the official Java SDK for GitHub Copilot. This repository treats the official .NET and Node.js SDKs for GitHub Copilot as reference implementations. These SDKs are all officially supported as GitHub open source projects. The Java implementation follows the backward compatibility guarantees offered by the reference implementations. While in public preview, minor breaking changes may still occur between releases. + +Welcome to the documentation for the **GitHub Copilot SDK for Java** β€” a Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build AI-powered applications and agentic workflows. + +## Getting Started + +### Requirements + +- Java 17 or later +- GitHub Copilot CLI 1.0.17 or later installed and in PATH (or provide custom `cliPath`) + +### Installation + +Add the dependency to your project: + +> **Note:** If this is a `-SNAPSHOT` version, clone the project repository and run `./mvnw install` locally first so the artifact is available in your local Maven cache. Otherwise, the version is available on Maven Central. + +**Maven:** + +```xml + + com.github + copilot-sdk-java + ${project.version} + +``` + +**Gradle:** + +```groovy +implementation 'com.github:copilot-sdk-java:${project.version}' +``` + +### Quick Example + +```java +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import java.util.concurrent.CompletableFuture; + +public class Example { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("claude-sonnet-4.5") + ).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?")).get(); + done.get(); + } + } +} +``` + +## Documentation + +| Document | Description | +|----------|-------------| +| [Documentation](documentation.html) | Basic usage, streaming, handling responses, and session management | +| [Advanced Usage](advanced.html) | Tools, BYOK, MCP servers, infinite sessions, skills, and more | +| [MCP Servers](mcp.html) | Integrating Model Context Protocol servers | +| [Javadoc](apidocs/index.html) | Generated API documentation | + +## Try it with JBang + +You can quickly try the SDK without setting up a full project using [JBang](https://www.jbang.dev/): + +```bash +# Install JBang (if not already installed) +# macOS: brew install jbang +# Linux/Windows: curl -Ls https://sh.jbang.dev | bash -s - app setup + +# Create a simple script +cat > hello-copilot.java << 'EOF' +//DEPS com.github:copilot-sdk-java:${project.version} +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import java.util.concurrent.CompletableFuture; + +class hello { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + var done = new CompletableFuture(); + session.on(AssistantMessageEvent.class, msg -> { + System.out.print(msg.getData().content()); + }); + session.on(SessionIdleEvent.class, idle -> done.complete(null)); + session.send(new MessageOptions().setPrompt("Say hello!")).get(); + done.get(); + } + } +} +EOF + +# Run it +jbang hello-copilot.java +``` + +## Source Code + +- [GitHub Repository](https://github.com/github/copilot-sdk-java) +- [Issue Tracker](https://github.com/github/copilot-sdk-java/issues) +- [Contributing Guide](https://github.com/github/copilot-sdk-java/blob/main/CONTRIBUTING.md) diff --git a/java/src/site/markdown/mcp.md b/java/src/site/markdown/mcp.md new file mode 100644 index 000000000..8f830da57 --- /dev/null +++ b/java/src/site/markdown/mcp.md @@ -0,0 +1,126 @@ +# MCP Servers + +Extend the AI's capabilities with external tools using the [Model Context Protocol](https://modelcontextprotocol.io/). + +--- + +## Quick Start + +Give the AI access to your filesystem. + +```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(); + +var result = session.sendAndWait("List files in the directory").get(); +System.out.println(result.getData().content()); +``` + +> **Tip:** Browse the [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) for community servers like GitHub, SQLite, and Puppeteer. + +--- + +## Local Servers + +Run a tool as a subprocess (stdin/stdout communication). + +```java +var server = new HashMap(); +server.put("type", "local"); +server.put("command", "node"); +server.put("args", List.of("./mcp-server.js")); +server.put("env", Map.of("DEBUG", "true")); +server.put("cwd", "./servers"); +server.put("tools", List.of("*")); +``` + +| Option | Description | +|--------|-------------| +| `command` | Executable to run | +| `args` | Command-line arguments | +| `env` | Environment variables | +| `cwd` | Working directory | +| `tools` | `["*"]` for all, `[]` for none, or specific tool names | +| `timeout` | Timeout in milliseconds | + +--- + +## Remote Servers + +Connect to a cloud-hosted MCP server via HTTP. + +```java +Map server = Map.of( + "type", "http", + "url", "https://api.githubcopilot.com/mcp/", + "headers", Map.of("Authorization", "Bearer " + token), + "tools", List.of("*") +); +``` + +| Option | Description | +|--------|-------------| +| `type` | `"http"` or `"sse"` | +| `url` | Server endpoint | +| `headers` | HTTP headers for authentication | +| `tools` | Tools to enable | +| `timeout` | Timeout in milliseconds | + +--- + +## Multiple Servers + +Combine tools from several sources. + +```java +var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setMcpServers(Map.of( + "filesystem", filesystemServer, + "github", githubServer, + "database", sqliteServer + )) +).get(); +``` + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Tools not invoked | Set `tools` to `["*"]` or list specific tool names | +| Server not found | Verify `command` path is correct and executable | +| Connection refused | Check URL and authentication headers | +| Timeout errors | Increase `timeout` or check server performance | + +**Debug tips:** +- Test your MCP server independently before integrating +- Enable verbose logging in the server +- Start with a simple tool to verify the integration + +--- + +## Resources + +- [MCP Specification](https://modelcontextprotocol.io/) +- [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) +- [GitHub MCP Server](https://github.com/github/github-mcp-server) + +--- + +## Next Steps + +- πŸ“– **[Documentation](documentation.html)** - Core concepts, events, session management +- πŸ“– **[Advanced Usage](advanced.html)** - Tools, BYOK, System Messages, Custom Agents +- πŸ“– **[Session Hooks](hooks.html)** - Intercept tool execution and session lifecycle events +- πŸ“– **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/java/src/site/markdown/setup.md b/java/src/site/markdown/setup.md new file mode 100644 index 000000000..224fc1948 --- /dev/null +++ b/java/src/site/markdown/setup.md @@ -0,0 +1,435 @@ +# Setup & Deployment Guide + +This guide explains how to configure the Copilot SDK for different deployment scenarios β€” from local development to production multi-user applications. + +## Quick Reference + +| Scenario | Configuration | Guide Section | +|----------|--------------|---------------| +| Local development | Default (no options) | [Local CLI](#local-cli) | +| Multi-user app | `setGitHubToken(userToken)` | [GitHub OAuth](#github-oauth-authentication) | +| Server deployment | `setCliUrl("host:port")` | [Backend Services](#backend-services) | +| Custom CLI location | `setCliPath("/path/to/copilot")` | [Bundled CLI](#bundled-cli) | +| Own model keys | Provider configuration | [BYOK](advanced.html#Bring_Your_Own_Key_BYOK) | + +## Local CLI + +The simplest setup uses the Copilot CLI already signed in on your development machine. + +**Use when:** Building personal projects, prototyping, or learning the SDK. + +```java +try (var client = new CopilotClient()) { + client.start().get(); + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") + ).get(); + // Use session... +} +``` + +**How it works:** The SDK automatically spawns the CLI process and uses credentials from your system keychain. + +**Requirements:** +- Copilot CLI installed and signed in (`copilot auth login`) +- Active Copilot subscription + +See [Getting Started](getting-started.html) for a complete tutorial. + +## GitHub OAuth Authentication + +For multi-user applications where users authenticate with their GitHub accounts. + +**Use when:** Building apps where users have GitHub accounts and Copilot subscriptions. + +### Basic Setup + +After obtaining a user's GitHub OAuth token, pass it to the SDK: + +```java +var options = new CopilotClientOptions() + .setGitHubToken(userAccessToken) + .setUseLoggedInUser(false); + +try (var client = new CopilotClient(options)) { + client.start().get(); + var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") + ).get(); + // Requests are made on behalf of the authenticated user +} +``` + +### OAuth Flow Integration + +Your application handles the OAuth flow: + +1. Create a GitHub OAuth App in your GitHub settings +2. Redirect users to GitHub's authorization URL +3. Exchange the authorization code for an access token +4. Pass the token to `CopilotClientOptions.setGitHubToken()` + +### Per-User Client Management + +Each authenticated user should get their own client instance: + +```java +private final Map clients = new ConcurrentHashMap<>(); + +public CopilotClient getClientForUser(String userId, String gitHubToken) { + return clients.computeIfAbsent(userId, id -> { + var options = new CopilotClientOptions() + .setGitHubToken(gitHubToken) + .setUseLoggedInUser(false); + var client = new CopilotClient(options); + try { + client.start().get(); + } catch (Exception e) { + throw new RuntimeException("Failed to start client for user: " + userId, e); + } + return client; + }); +} +``` + +### Token Types + +| Token Prefix | Description | Supported | +|--------------|-------------|-----------| +| `gho_` | OAuth user access token | βœ… | +| `ghu_` | GitHub App user access token | βœ… | +| `github_pat_` | Fine-grained personal access token | βœ… | + +**Note:** Token lifecycle management (storage, refresh, expiration) is your application's responsibility. + +## Backend Services + +Run the SDK in server-side applications by connecting to an external CLI server. + +**Use when:** Building web backends, APIs, microservices, or any server-side workload. + +### Architecture + +Instead of spawning a CLI process, your application connects to a separately-running CLI server: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Your Backend β”‚ β”‚ CLI Server β”‚ +β”‚ β”‚ β”‚ (headless) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ SDK β”œβ”€β”€β”Όβ”€β”€β”€β”€β”€β”€β–Ίβ”‚ β”‚JSON-RPC β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ TCP β”‚ β”‚:4321 β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Start the CLI Server + +Run the CLI in headless server mode: + +```bash +copilot server --port 4321 +``` + +Or with authentication: + +```bash +export GITHUB_TOKEN=your_token +copilot server --port 4321 +``` + +### Connect from Your Application + +Configure the SDK to connect to the external server: + +```java +var options = new CopilotClientOptions() + .setCliUrl("localhost:4321"); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // Client connects to the external server +} +``` + +### Multiple SDK Clients, One Server + +Multiple application instances can share a single CLI server: + +```java +// Use an explicit connection token so all clients can authenticate +var token = "my-shared-secret"; +var client1 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); +var client2 = new CopilotClient(new CopilotClientOptions() + .setCliUrl("cli-server:4321").setTcpConnectionToken(token)); +// Both connect to the same CLI server +``` + +### Connection Token (TCP Security) + +When the SDK spawns the CLI in TCP mode, a random connection token is generated automatically +to protect the loopback listener. You can also provide an explicit token: + +```java +var options = new CopilotClientOptions() + .setUseStdio(false) + .setTcpConnectionToken("my-secret-token"); +``` + +> **Note:** `tcpConnectionToken` cannot be used with `useStdio = true`. + +### Deployment Patterns + +**Container deployment:** +```yaml +# docker-compose.yml +services: + cli-server: + image: copilot-cli:latest + command: copilot server --port 4321 + environment: + - GITHUB_TOKEN=${GITHUB_TOKEN} + ports: + - "4321:4321" + + backend: + image: your-backend:latest + environment: + - CLI_URL=cli-server:4321 + depends_on: + - cli-server +``` + +**Kubernetes deployment:** +```yaml +apiVersion: v1 +kind: Service +metadata: + name: copilot-cli +spec: + selector: + app: copilot-cli + ports: + - port: 4321 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: copilot-cli +spec: + replicas: 1 + template: + spec: + containers: + - name: cli + image: copilot-cli:latest + args: ["server", "--port", "4321"] + env: + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: copilot-auth + key: token +``` + +## Bundled CLI + +Package the Copilot CLI with your application so users don't need to install it separately. + +**Use when:** Distributing desktop applications or standalone tools. + +### Configuration + +Point the SDK to your bundled CLI binary: + +```java +var options = new CopilotClientOptions() + .setCliPath("./bundled/copilot"); // Relative to working directory + +try (var client = new CopilotClient(options)) { + client.start().get(); + // SDK uses the bundled CLI +} +``` + +### Packaging + +1. Download the appropriate CLI binary for your target platform +2. Include it in your application bundle: + ``` + my-app/ + β”œβ”€β”€ bin/ + β”‚ └── copilot # CLI binary + β”œβ”€β”€ lib/ + β”‚ └── my-app.jar + └── run.sh + ``` +3. Configure the path in your application + +### Platform-Specific Binaries + +For cross-platform applications, detect the platform and use the appropriate binary: + +```java +private String getCliPathForPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + + if (os.contains("win")) { + return "./bin/copilot-windows-" + arch + ".exe"; + } else if (os.contains("mac")) { + return "./bin/copilot-darwin-" + arch; + } else { + return "./bin/copilot-linux-" + arch; + } +} + +var options = new CopilotClientOptions() + .setCliPath(getCliPathForPlatform()); +``` + +## Scaling & Multi-Tenancy + +For applications serving many concurrent users, consider these patterns: + +### Session Isolation + +Each user's sessions are automatically isolated within their client instance. For strongest isolation, use one CLI server per user: + +```java +// Pattern: Isolated CLI per user (requires CLI server per user) +public CopilotClient createIsolatedClient(String userId, int port) { + // Start CLI server for this user: copilot server --port {port} + var options = new CopilotClientOptions() + .setCliUrl("localhost:" + port); + return new CopilotClient(options); +} +``` + +### Resource Management + +For high-concurrency scenarios: + +```java +// Use a client pool with bounded resources +public class CopilotClientPool { + private final Semaphore permits; + private final CopilotClient sharedClient; + + public CopilotClientPool(int maxConcurrentSessions) { + this.permits = new Semaphore(maxConcurrentSessions); + this.sharedClient = new CopilotClient(/* options */); + } + + public T withSession(SessionConfig config, + Function action) throws Exception { + permits.acquire(); + try { + var session = sharedClient.createSession(config).get(); + try { + return action.apply(session); + } finally { + session.close(); + } + } finally { + permits.release(); + } + } +} +``` + +### Horizontal Scaling + +When scaling beyond a single server: + +1. Run multiple CLI servers (one per app server or shared) +2. Use load balancing at the application tier +3. Each app server connects to its assigned CLI server via `setCliUrl()` + +## Configuration Reference + +Complete list of `CopilotClientOptions` settings: + +| Option | Type | Description | Default | +|--------|------|-------------|---------| +| `cliPath` | String | Path to CLI executable | `"copilot"` from PATH | +| `cliUrl` | String | External CLI server URL | `null` (spawn process) | +| `cliArgs` | String[] | Extra CLI arguments | `null` | +| `copilotHome` | String | Base directory for Copilot data | `null` (~/.copilot) | +| `gitHubToken` | String | GitHub OAuth token | `null` | +| `useLoggedInUser` | Boolean | Use system credentials | `true` | +| `useStdio` | boolean | Use stdio transport | `true` | +| `port` | int | TCP port for CLI | `0` (random) | +| `tcpConnectionToken` | String | Connection token for TCP mode | `null` (auto-generated) | +| `autoStart` | boolean | Auto-start server | `true` | +| `autoRestart` | boolean | Auto-restart on crash | `true` | +| `logLevel` | String | CLI log level | `"info"` | +| `environment` | Map | Environment variables | inherited | +| `cwd` | String | Working directory | current dir | +| `onListModels` | Supplier | Custom model listing implementation | `null` (use CLI) | + +### Extra CLI Arguments + +Pass additional command-line arguments to the CLI process: + +```java +var options = new CopilotClientOptions() + .setCliArgs(new String[]{"--verbose", "--no-telemetry"}); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // CLI process receives the extra flags +} +``` + +### Custom Environment Variables + +Set environment variables for the CLI process (merged with the inherited system environment): + +```java +var options = new CopilotClientOptions() + .setEnvironment(Map.of( + "HTTPS_PROXY", "http://proxy.example.com:8080", + "NO_PROXY", "localhost,127.0.0.1" + )); + +try (var client = new CopilotClient(options)) { + client.start().get(); + // CLI process uses the proxy settings +} +``` + +This is useful for configuring proxy servers, custom CA certificates, or any environment-specific settings the CLI needs. + +## Best Practices + +### Development + +- Use default configuration (local CLI) for fastest iteration +- Enable debug logging: `setLogLevel("debug")` +- Test with multiple models to ensure compatibility + +### Production + +- Use external CLI servers (`setCliUrl`) for better resource management +- Implement health checks on the CLI server endpoint +- Monitor CLI server resource usage (CPU, memory) +- Use connection pooling for high-concurrency scenarios +- Implement proper token refresh for OAuth-based auth +- Set appropriate timeouts for session operations + +### Security + +- Never log or expose GitHub tokens +- Use environment variables for tokens in production +- Regularly rotate tokens +- Implement proper access controls for multi-user apps +- Validate user input before sending to sessions + +## Next Steps + +- **[Getting Started](getting-started.html)** - Complete tutorial +- **[Documentation](documentation.html)** - Core concepts +- **[Advanced Usage](advanced.html)** - Tools, BYOK, MCP servers +- **[API Javadoc](apidocs/index.html)** - Complete API reference diff --git a/java/src/site/resources/css/site.css b/java/src/site/resources/css/site.css new file mode 100644 index 000000000..3135895b6 --- /dev/null +++ b/java/src/site/resources/css/site.css @@ -0,0 +1,319 @@ +/* ===== Custom theme for GitHub Copilot SDK for Java ===== */ +/* Layered on top of Maven Fluido Skin (Bootstrap 2.x) */ + +/* ===== Typography & Base ===== */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + color: #24292f; + background: #f6f8fa; + -webkit-font-smoothing: antialiased; +} + +a { + color: #0969da; +} + +a:hover { + color: #0550ae; +} + +code, pre { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + +/* ===== Navbar ===== */ +.navbar .navbar-inner { + background: #24292f; + background-image: none; + border: none; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); +} + +.navbar .brand, +.navbar .nav > li > a, +.navbar .nav > li > a:hover, +.navbar .nav .dropdown-toggle { + color: #e6edf3; + text-shadow: none; + font-weight: 600; +} + +.navbar .nav > li > a:hover, +.navbar .nav > .active > a { + color: #fff; + background: rgba(255, 255, 255, 0.08); +} + +.navbar .nav .dropdown-menu { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + padding: 8px 0; +} + +.navbar .nav .dropdown-menu li > a { + color: #24292f; + padding: 8px 16px; + font-weight: 500; +} + +.navbar .nav .dropdown-menu li > a:hover { + background: rgba(102, 126, 234, 0.06); + color: #0969da; +} + +/* ===== GitHub Ribbon ===== */ +.github-fork-ribbon:before { + background-color: #667eea !important; + background-image: linear-gradient(135deg, #667eea, #764ba2) !important; +} + +.github-fork-ribbon:after { + content: 'View on GitHub' !important; +} + +/* ===== Breadcrumbs ===== */ +#breadcrumbs { + background: #fff; + border-bottom: 1px solid #d0d7de; + padding: 10px 20px; +} + +#breadcrumbs .breadcrumb { + background: transparent; + margin: 0; +} + +/* ===== Left Sidebar ===== */ +#leftColumn .well { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + box-shadow: none; +} + +#leftColumn .nav-list > li > a { + color: #24292f; + border-radius: 6px; + margin: 2px 0; + font-weight: 500; +} + +#leftColumn .nav-list > li > a:hover { + background: rgba(102, 126, 234, 0.06); + color: #0969da; +} + +#leftColumn .nav-list > .active > a, +#leftColumn .nav-list > .active > a:hover { + background: linear-gradient(135deg, #667eea, #764ba2); + color: #fff; +} + +#leftColumn .nav-header { + color: #57606a; + font-weight: 700; + text-transform: uppercase; + font-size: 0.8em; + letter-spacing: 0.5px; + padding: 8px 14px 4px; +} + +/* ===== Main Content ===== */ +#bodyColumn { + line-height: 1.7; +} + +#bodyColumn h1, +#bodyColumn h2, +#bodyColumn h3, +#bodyColumn h4 { + color: #24292f; + font-weight: 700; +} + +#bodyColumn h2 { + border-bottom: 1px solid #d0d7de; + padding-bottom: 8px; + margin-top: 32px; +} + +/* ===== Code Blocks ===== */ +#bodyColumn pre { + background: #eef1f6; + color: #24292f; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 16px 20px; + font-size: 0.88em; + line-height: 1.6; + overflow-x: auto; +} + +#bodyColumn code { + background: rgba(102, 126, 234, 0.1); + color: #24292f; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; +} + +#bodyColumn pre code { + background: transparent; + color: inherit; + padding: 0; + border-radius: 0; +} + +/* ===== Alert Boxes (from markdown) ===== */ +#bodyColumn .alert, +#bodyColumn blockquote { + border-radius: 10px; + border-left: 4px solid; + padding: 16px 20px; + margin: 20px 0; +} + +#bodyColumn .alert-info, +#bodyColumn blockquote { + background: rgba(102, 126, 234, 0.06); + border-left-color: #667eea; + color: #4a5067; +} + +#bodyColumn .alert-warning { + background: #fff8c5; + border-left-color: #d4a72c; + color: #6a5300; +} + +#bodyColumn .alert-danger, +#bodyColumn .alert-error { + background: #ffeef0; + border-left-color: #cf222e; + color: #82071e; +} + +#bodyColumn .alert-success { + background: #dafbe1; + border-left-color: #1a7f37; + color: #116329; +} + +/* ===== Tables ===== */ +#bodyColumn table { + border-collapse: separate; + border-spacing: 0; + border: 1px solid #d0d7de; + border-radius: 10px; + overflow: hidden; + width: 100%; + margin: 20px 0; +} + +#bodyColumn table thead th { + background: #f6f8fa; + color: #24292f; + font-weight: 700; + border-bottom: 2px solid #d0d7de; + padding: 12px 16px; + text-align: left; +} + +#bodyColumn table tbody td { + padding: 10px 16px; + border-bottom: 1px solid #eaeef2; +} + +#bodyColumn table tbody tr:last-child td { + border-bottom: none; +} + +#bodyColumn table tbody tr:hover { + background: rgba(102, 126, 234, 0.03); +} + +/* ===== Badges / Labels ===== */ +.label, .badge { + font-weight: 600; + border-radius: 100px; + padding: 3px 10px; + font-size: 0.8em; +} + +.label-info, .badge-info { + background: linear-gradient(135deg, #667eea, #764ba2); +} + +/* ===== Cards (for section-like divs) ===== */ +#bodyColumn .section { + background: #fff; + border: 1px solid #d0d7de; + border-radius: 10px; + padding: 24px 28px; + margin-bottom: 24px; +} + +#bodyColumn .section .section { + background: transparent; + border: none; + border-radius: 0; + padding: 0; + margin-bottom: 16px; +} + +/* ===== Footer ===== */ +#footer { + background: #fff; + border-top: 1px solid #d0d7de; + color: #57606a; + font-size: 0.88em; + padding: 24px 0; +} + +#footer a { + color: #57606a; + font-weight: 500; +} + +#footer a:hover { + color: #0969da; +} + +/* ===== Powered By (add GitHub Copilot) ===== */ +#poweredBy { + text-align: center; +} + +#poweredBy::after { + content: ''; + display: block; + margin: 8px auto 0; + width: 240px; + height: 120px; + background-image: url('../images/github-copilot.jpg'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + border-radius: 6px; +} + +/* ===== Scrollbar (subtle) ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #d0d7de; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #8b949e; +} diff --git a/java/src/site/resources/image.png b/java/src/site/resources/image.png new file mode 100644 index 000000000..bb83912a3 Binary files /dev/null and b/java/src/site/resources/image.png differ diff --git a/java/src/site/resources/images/github-copilot.jpg b/java/src/site/resources/images/github-copilot.jpg new file mode 100644 index 000000000..75efcdba2 Binary files /dev/null and b/java/src/site/resources/images/github-copilot.jpg differ diff --git a/java/src/site/site.xml b/java/src/site/site.xml new file mode 100644 index 000000000..d012c0335 --- /dev/null +++ b/java/src/site/site.xml @@ -0,0 +1,80 @@ + + + + + + org.apache.maven.skins + maven-fluido-skin + 2.1.0 + + + + + true + + GitHub Copilot SDK for Java + index.html + + + github/copilot-sdk-java + right + gray + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/src/test/java/com/github/copilot/sdk/AgentInfoTest.java b/java/src/test/java/com/github/copilot/sdk/AgentInfoTest.java new file mode 100644 index 000000000..0893773e7 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/AgentInfoTest.java @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.AgentInfo; + +/** + * Unit tests for {@link AgentInfo} getters, setters, and fluent chaining. + */ +class AgentInfoTest { + + @Test + void defaultValuesAreNull() { + var agent = new AgentInfo(); + assertNull(agent.getName()); + assertNull(agent.getDisplayName()); + assertNull(agent.getDescription()); + } + + @Test + void nameGetterSetter() { + var agent = new AgentInfo(); + agent.setName("coder"); + assertEquals("coder", agent.getName()); + } + + @Test + void displayNameGetterSetter() { + var agent = new AgentInfo(); + agent.setDisplayName("Code Assistant"); + assertEquals("Code Assistant", agent.getDisplayName()); + } + + @Test + void descriptionGetterSetter() { + var agent = new AgentInfo(); + agent.setDescription("Helps with coding tasks"); + assertEquals("Helps with coding tasks", agent.getDescription()); + } + + @Test + void fluentChainingReturnsThis() { + var agent = new AgentInfo().setName("coder").setDisplayName("Code Assistant") + .setDescription("Helps with coding tasks"); + + assertEquals("coder", agent.getName()); + assertEquals("Code Assistant", agent.getDisplayName()); + assertEquals("Helps with coding tasks", agent.getDescription()); + } + + @Test + void fluentChainingReturnsSameInstance() { + var agent = new AgentInfo(); + assertSame(agent, agent.setName("test")); + assertSame(agent, agent.setDisplayName("Test")); + assertSame(agent, agent.setDescription("A test agent")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/AskUserTest.java b/java/src/test/java/com/github/copilot/sdk/AskUserTest.java new file mode 100644 index 000000000..a2ad13b18 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/AskUserTest.java @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.UserInputRequest; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Tests for user input handler (ask_user) functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/ask_user/. + *

+ */ +public class AskUserTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that user input handler is invoked when model uses ask_user tool. + * + * @see Snapshot: + * ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool + */ + @Test + void testShouldInvokeUserInputHandlerWhenModelUsesAskUserTool() throws Exception { + ctx.configureForTest("ask_user", "should_invoke_user_input_handler_when_model_uses_ask_user_tool"); + + var userInputRequests = new ArrayList(); + final String[] sessionIdHolder = new String[1]; + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + + // Return the first choice if available, otherwise a freeform answer + String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) + ? request.getChoices().get(0) + : "freeform answer"; + boolean wasFreeform = request.getChoices() == null || request.getChoices().isEmpty(); + + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(wasFreeform)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + sessionIdHolder[0] = session.getSessionId(); + + session.sendAndWait(new MessageOptions().setPrompt( + "Ask me to choose between 'Option A' and 'Option B' using the ask_user tool. Wait for my response before continuing.")) + .get(60, TimeUnit.SECONDS); + + // Should have received at least one user input request + assertFalse(userInputRequests.isEmpty(), "Should have received user input requests"); + + // The request should have a question + assertTrue(userInputRequests.stream().anyMatch(r -> r.getQuestion() != null && !r.getQuestion().isEmpty()), + "User input request should have a question"); + } + } + + /** + * Verifies that choices are received in user input requests. + * + * @see Snapshot: ask_user/should_receive_choices_in_user_input_request + */ + @Test + void testShouldReceiveChoicesInUserInputRequest() throws Exception { + ctx.configureForTest("ask_user", "should_receive_choices_in_user_input_request"); + + var userInputRequests = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); + + // Pick the first choice + String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) + ? request.getChoices().get(0) + : "default"; + + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(false)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.sendAndWait(new MessageOptions().setPrompt( + "Use the ask_user tool to ask me to pick between exactly two options: 'Red' and 'Blue'. These should be provided as choices. Wait for my answer.")) + .get(60, TimeUnit.SECONDS); + + // Should have received a request + assertFalse(userInputRequests.isEmpty(), "Should have received user input requests"); + + // At least one request should have choices + assertTrue(userInputRequests.stream().anyMatch(r -> r.getChoices() != null && !r.getChoices().isEmpty()), + "At least one request should have choices"); + } + } + + /** + * Verifies that freeform user input responses are handled. + * + * @see Snapshot: ask_user/should_handle_freeform_user_input_response + */ + @Test + void testShouldHandleFreeformUserInputResponse() throws Exception { + ctx.configureForTest("ask_user", "should_handle_freeform_user_input_response"); + + final var userInputRequests = new ArrayList(); + String freeformAnswer = "This is my custom freeform answer that was not in the choices"; + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + userInputRequests.add(request); + + // Return a freeform answer (not from choices) + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(freeformAnswer).setWasFreeform(true)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + var response = session.sendAndWait(new MessageOptions().setPrompt( + "Ask me a question using ask_user and then include my answer in your response. The question should be 'What is your favorite color?'")) + .get(60, TimeUnit.SECONDS); + + // Should have received a request + assertFalse(userInputRequests.isEmpty(), "Should have received user input requests"); + + // The model's response should be defined + assertNotNull(response, "Response should not be null"); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CapiProxy.java b/java/src/test/java/com/github/copilot/sdk/CapiProxy.java new file mode 100644 index 000000000..09c4e2016 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CapiProxy.java @@ -0,0 +1,477 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Manages a replaying proxy server for E2E tests. + * + *

+ * This spawns the shared test harness server from test/harness/server.ts which + * acts as a replaying proxy to AI endpoints. It captures and stores + * request/response pairs in YAML snapshot files and replays stored responses on + * subsequent runs for deterministic testing. + *

+ * + *

+ * Usage example: + *

+ * + *
+ * {@code
+ * CapiProxy proxy = new CapiProxy();
+ * String proxyUrl = proxy.start();
+ *
+ * // Configure for a specific test
+ * proxy.configure("test/snapshots/tools/my_test.yaml", workDir);
+ *
+ * // ... run tests with proxyUrl ...
+ *
+ * // Get captured exchanges
+ * List> exchanges = proxy.getExchanges();
+ *
+ * proxy.stop();
+ * }
+ * 
+ */ +public class CapiProxy implements AutoCloseable { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)(?:\\s+(\\{.*\\}))?$"); + + private Process process; + private String proxyUrl; + private String connectProxyUrl; + private String caFilePath; + private final HttpClient httpClient; + private BufferedReader stdoutReader; + + public CapiProxy() { + this.httpClient = HttpClient.newHttpClient(); + } + + /** + * Starts the proxy server and returns its URL. + * + * @return the proxy URL (e.g., "http://localhost:12345") + * @throws IOException + * if the server fails to start + * @throws InterruptedException + * if the startup is interrupted + */ + public String start() throws IOException, InterruptedException { + if (proxyUrl != null) { + return proxyUrl; + } + + // Find the repo root by looking for the test/harness directory + Path harnessDir = findHarnessDirectory(); + if (harnessDir == null) { + throw new IOException("Could not find test/harness directory. " + + "Make sure you are running from within the copilot-sdk repository."); + } + + // Start the harness server using npx tsx + // On Windows, npx is installed as npx.cmd which requires cmd /c to launch + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); + var pb = isWindows + ? new ProcessBuilder("cmd", "/c", "npx", "tsx", "server.ts") + : new ProcessBuilder("npx", "tsx", "server.ts"); + pb.directory(harnessDir.toFile()); + pb.redirectErrorStream(false); + // Tell the replaying proxy to fail fast on unmatched requests rather than + // forwarding them to the real API. Without this, unmatched requests hit the + // live API with a fake token and crash the proxy's JSON parser. + pb.environment().put("GITHUB_ACTIONS", "true"); + + process = pb.start(); + + // Read stdout to get the listening URL + // Note: We keep the reader open to avoid closing the process input stream + stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + // Also consume stderr in a background thread to prevent blocking + Thread stderrThread = new Thread(() -> { + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String errLine; + while ((errLine = errReader.readLine()) != null) { + System.err.println("[CapiProxy stderr] " + errLine); + } + } catch (IOException e) { + // Ignore + } + }); + stderrThread.setDaemon(true); + stderrThread.start(); + + String line = stdoutReader.readLine(); + if (line == null) { + // Try to get error info + StringBuilder errInfo = new StringBuilder(); + try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String errLine; + while ((errLine = errReader.readLine()) != null) { + errInfo.append(errLine).append("\n"); + } + } + process.destroyForcibly(); + throw new IOException("Failed to read proxy URL - server may have crashed. Stderr: " + errInfo); + } + + Matcher matcher = LISTENING_PATTERN.matcher(line); + if (!matcher.find()) { + process.destroyForcibly(); + throw new IOException("Unexpected proxy output: " + line); + } + + String url = matcher.group(1); + + // Parse optional metadata (CONNECT proxy details) + String metadata = matcher.group(2); + if (metadata != null && !metadata.isEmpty()) { + try { + Map meta = MAPPER.readValue(metadata, new TypeReference>() { + }); + connectProxyUrl = meta.get("connectProxyUrl"); + caFilePath = meta.get("caFilePath"); + } catch (Exception e) { + process.destroyForcibly(); + throw new IOException("Failed to parse proxy startup metadata: " + metadata, e); + } + } + + // Only set proxyUrl after all parsing succeeds to avoid inconsistent state + proxyUrl = url; + return proxyUrl; + } + + /** + * Configures the proxy for a specific test file. + * + * @param filePath + * the path to the YAML snapshot file (relative to repo root) + * @param workDir + * the working directory for path normalization + * @throws IOException + * if the configuration fails + * @throws InterruptedException + * if the request is interrupted + */ + public void configure(String filePath, String workDir) throws IOException, InterruptedException { + configure(filePath, workDir, null); + } + + /** + * Configures the proxy for a specific test file. + * + * @param filePath + * the path to the YAML snapshot file (relative to repo root) + * @param workDir + * the working directory for path normalization + * @param testInfo + * optional test information (file and line number) + * @throws IOException + * if the configuration fails + * @throws InterruptedException + * if the request is interrupted + */ + public void configure(String filePath, String workDir, TestInfo testInfo) throws IOException, InterruptedException { + if (proxyUrl == null) { + throw new IllegalStateException("Proxy not started"); + } + + Map config = new java.util.HashMap<>(); + config.put("filePath", filePath); + config.put("workDir", workDir); + if (testInfo != null) { + config.put("testInfo", Map.of("file", testInfo.file(), "line", testInfo.line())); + } + + String body = MAPPER.writeValueAsString(config); + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/config")) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("Proxy config failed with status " + response.statusCode() + ": " + response.body()); + } + } + + /** + * Gets the captured HTTP exchanges from the proxy. + * + * @return list of exchange maps containing request/response data + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public List> getExchanges() throws IOException, InterruptedException { + if (proxyUrl == null) { + throw new IllegalStateException("Proxy not started"); + } + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/exchanges")).GET().build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("Failed to get exchanges: " + response.statusCode()); + } + + return MAPPER.readValue(response.body(), new TypeReference>>() { + }); + } + + /** + * Configures the proxy to return a specific Copilot user response for a given + * token. Used for per-session authentication tests. + * + * @param token + * the GitHub token to configure + * @param login + * the user login to return + * @param copilotPlan + * the Copilot plan to return + * @param apiUrl + * the API URL for the user endpoints + * @param telemetryUrl + * the telemetry URL for the user endpoints + * @param analyticsTrackingId + * the analytics tracking ID for the user + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void setCopilotUserByToken(String token, String login, String copilotPlan, String apiUrl, + String telemetryUrl, String analyticsTrackingId) throws IOException, InterruptedException { + if (proxyUrl == null) { + throw new IllegalStateException("Proxy not started"); + } + + Map payload = new java.util.HashMap<>(); + payload.put("token", token); + Map responseMap = new java.util.HashMap<>(); + responseMap.put("login", login); + responseMap.put("copilotPlan", copilotPlan); + responseMap.put("endpoints", Map.of("api", apiUrl, "telemetry", telemetryUrl)); + responseMap.put("analyticsTrackingId", analyticsTrackingId); + payload.put("response", responseMap); + + String body = MAPPER.writeValueAsString(payload); + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/copilot-user-config")) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException( + "Failed to set copilot user config: " + response.statusCode() + ": " + response.body()); + } + } + + /** + * Stops the proxy server gracefully. + * + * @throws IOException + * if the stop request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void stop() throws IOException, InterruptedException { + stop(false); + } + + /** + * Stops the proxy server. + * + * @param skipWritingCache + * if true, won't write captured exchanges to disk + * @throws IOException + * if the stop request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void stop(boolean skipWritingCache) throws IOException, InterruptedException { + if (process == null) { + return; + } + + // Send stop request to the server + if (proxyUrl != null) { + try { + String stopUrl = proxyUrl + "/stop"; + if (skipWritingCache) { + stopUrl += "?skipWritingCache=true"; + } + + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(stopUrl)) + .POST(HttpRequest.BodyPublishers.noBody()).build(); + + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + // Best effort - ignore errors + } + } + + // Wait for the process to exit + process.waitFor(5, TimeUnit.SECONDS); + if (process.isAlive()) { + process.destroyForcibly(); + } + + // Close the stdout reader + if (stdoutReader != null) { + try { + stdoutReader.close(); + } catch (IOException e) { + // Ignore + } + stdoutReader = null; + } + + process = null; + proxyUrl = null; + connectProxyUrl = null; + caFilePath = null; + } + + /** + * Gets the proxy URL. + * + * @return the proxy URL, or null if not started + */ + public String getProxyUrl() { + return proxyUrl; + } + + /** + * Gets the CONNECT proxy URL for HTTPS interception. + * + * @return the CONNECT proxy URL, or null if not available + */ + public String getConnectProxyUrl() { + return connectProxyUrl; + } + + /** + * Gets the CA file path for trusting the CONNECT proxy's certificate. + * + * @return the CA file path, or null if not available + */ + public String getCaFilePath() { + return caFilePath; + } + + /** + * Checks if the proxy process is still alive and responsive. This does both a + * process alive check AND an HTTP health check. + * + * @return true if the proxy is running and responsive, false otherwise + */ + public boolean isAlive() { + if (process == null || !process.isAlive()) { + return false; + } + + // Also verify the proxy is responsive via HTTP + if (proxyUrl != null) { + try { + java.net.HttpURLConnection conn = (java.net.HttpURLConnection) new java.net.URL(proxyUrl + "/exchanges") + .openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(1000); + conn.setReadTimeout(1000); + int responseCode = conn.getResponseCode(); + conn.disconnect(); + return responseCode == 200; + } catch (Exception e) { + // If HTTP check fails, the proxy is not responsive + return false; + } + } + + return true; + } + + /** + * Restarts the proxy server. This stops the current instance (if any) and + * starts a new one. + * + * @return the new proxy URL + * @throws IOException + * if the server fails to start + * @throws InterruptedException + * if the startup is interrupted + */ + public String restart() throws IOException, InterruptedException { + try { + stop(true); // Skip writing cache on restart + } catch (Exception e) { + // Best effort - force cleanup + if (process != null) { + process.destroyForcibly(); + process = null; + } + proxyUrl = null; + } + return start(); + } + + @Override + public void close() throws Exception { + stop(); + } + + /** + * Finds the test/harness directory by walking up from the current directory. + */ + private Path findHarnessDirectory() { + // First, check for copilot.sdk.dir system property (set by Maven during tests) + String sdkDir = System.getProperty("copilot.sdk.dir"); + if (sdkDir != null && !sdkDir.isEmpty()) { + Path harnessDir = Paths.get(sdkDir).resolve("test").resolve("harness"); + if (harnessDir.toFile().exists() && harnessDir.resolve("server.ts").toFile().exists()) { + return harnessDir; + } + } + + // Fallback: walk up the directory tree looking for test/harness + Path current = Paths.get(System.getProperty("user.dir")); + while (current != null) { + Path harnessDir = current.resolve("test").resolve("harness"); + if (harnessDir.toFile().exists() && harnessDir.resolve("server.ts").toFile().exists()) { + return harnessDir; + } + current = current.getParent(); + } + + return null; + } + + /** + * Test information record for configuring the proxy. + */ + public record TestInfo(String file, int line) { + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java b/java/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java new file mode 100644 index 000000000..90e6dcc3c --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CliServerManagerTest.java @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.TelemetryConfig; + +/** + * Unit tests for {@link CliServerManager} covering parseCliUrl, + * connectToServer, resolveCliCommand, and ProcessInfo coverage gaps identified + * by JaCoCo. + */ +class CliServerManagerTest { + + // ===== parseCliUrl tests ===== + + @Test + void parseCliUrlWithPortNumber() { + URI uri = CliServerManager.parseCliUrl("8080"); + assertEquals("http://localhost:8080", uri.toString()); + } + + @Test + void parseCliUrlWithHostColonPort() { + URI uri = CliServerManager.parseCliUrl("myhost:9090"); + assertEquals("https://myhost:9090", uri.toString()); + } + + @Test + void parseCliUrlWithHttpPrefix() { + URI uri = CliServerManager.parseCliUrl("http://example.com:3000"); + assertEquals("http://example.com:3000", uri.toString()); + } + + @Test + void parseCliUrlWithHttpsPrefix() { + URI uri = CliServerManager.parseCliUrl("https://secure.host:443"); + assertEquals("https://secure.host:443", uri.toString()); + } + + @Test + void parseCliUrlWithHostOnly() { + URI uri = CliServerManager.parseCliUrl("copilot.example.com"); + assertEquals("https://copilot.example.com", uri.toString()); + } + + // ===== connectToServer tests ===== + + @Test + void connectToServerTcpMode() throws Exception { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // Start a temporary server socket to connect to + try (ServerSocket ss = new ServerSocket(0)) { + int port = ss.getLocalPort(); + JsonRpcClient client = manager.connectToServer(null, "localhost", port); + assertNotNull(client); + client.close(); + } + } + + private static Process startBlockingProcess() throws IOException { + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + return (isWindows ? new ProcessBuilder("cmd", "/c", "more") : new ProcessBuilder("cat")).start(); + } + + @Test + void connectToServerStdioMode() throws Exception { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // Create a dummy process for stdio mode + Process process = startBlockingProcess(); + try { + JsonRpcClient client = manager.connectToServer(process, null, null); + assertNotNull(client); + client.close(); + } finally { + process.destroyForcibly(); + } + } + + @Test + void connectToServerNoProcessNoHost() { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + var ex = assertThrows(IllegalStateException.class, () -> manager.connectToServer(null, null, null)); + assertTrue(ex.getMessage().contains("Cannot connect")); + } + + @Test + void connectToServerNullHostNonNullPort() { + var options = new CopilotClientOptions(); + var manager = new CliServerManager(options); + + // tcpHost is null but tcpPort is non-null β†’ falls to process check β†’ process + // null β†’ exception + var ex = assertThrows(IllegalStateException.class, () -> manager.connectToServer(null, null, 8080)); + assertTrue(ex.getMessage().contains("Cannot connect")); + } + + // ===== ProcessInfo record tests ===== + + @Test + void processInfoRecord() { + var info = new CliServerManager.ProcessInfo(null, 12345); + assertNull(info.process()); + assertEquals(12345, info.port()); + } + + @Test + void processInfoWithNullPort() { + var info = new CliServerManager.ProcessInfo(null, null); + assertNull(info.process()); + assertNull(info.port()); + } + + // ===== resolveCliCommand tests (via startCliServer) ===== + // resolveCliCommand is private, so we test indirectly through startCliServer + // with specific cliPath values. + + // On Windows, "/nonexistent/copilot" is not an absolute path (no drive letter), + // so resolveCliCommand wraps it with "cmd /c" and ProcessBuilder.start() + // succeeds + // (launching cmd.exe). Use a Windows-absolute path to ensure IOException. + private static final String NONEXISTENT_CLI = System.getProperty("os.name").toLowerCase().contains("win") + ? "C:\\nonexistent\\copilot" + : "/nonexistent/copilot"; + + @Test + void startCliServerWithJsFile() throws Exception { + // Using a .js file path causes resolveCliCommand to prepend "node" + // node is on PATH so the process starts, but the script doesn't exist + // so node exits quickly β€” verifying the .js branch was taken + var options = new CopilotClientOptions().setCliPath("/nonexistent/script.js").setUseStdio(true); + var manager = new CliServerManager(options); + + try { + var info = manager.startCliServer(); + // If process started, clean it up + info.process().destroyForcibly(); + } catch (IOException e) { + // Expected β€” node may fail or not be present; either way the branch is hit + assertNotNull(e); + } + } + + @Test + void startCliServerWithCliArgs() throws Exception { + // Test that cliArgs are included in the command + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setCliArgs(new String[]{"--extra-flag"}) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithExplicitPort() throws Exception { + // Test the explicit port branch (useStdio=false, port > 0) + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setUseStdio(false).setPort(9999); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithGitHubToken() throws Exception { + // Test the github token branch + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setGitHubToken("ghp_test123") + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithUseLoggedInUserExplicit() throws Exception { + // Test the explicit useLoggedInUser=false branch (adds --no-auto-login) + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setUseLoggedInUser(false) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithGitHubTokenAndNoExplicitUseLoggedInUser() throws Exception { + // When gitHubToken is set and useLoggedInUser is null, defaults to false + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setGitHubToken("ghp_test123") + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithNullCliPath() throws Exception { + // Test the default cliPath branch (defaults to "copilot" when not set) + var options = new CopilotClientOptions().setUseStdio(true); + var manager = new CliServerManager(options); + + // "copilot" likely doesn't exist in the test env β€” that's fine + try { + var info = manager.startCliServer(); + info.process().destroyForcibly(); + } catch (IOException e) { + // Expected if "copilot" is not on PATH + assertNotNull(e); + } + } + + @Test + void startCliServerWithTelemetryAllOptions() throws Exception { + // The telemetry env vars are applied before ProcessBuilder.start() + // so even with a nonexistent CLI path, the telemetry code path is exercised + var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/telemetry.log") + .setExporterType("otlp-http").setSourceName("test-app").setCaptureContent(true); + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setTelemetry(telemetry).setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithTelemetryCaptureContentFalse() throws Exception { + // Test the false branch of getCaptureContent() + var telemetry = new TelemetryConfig().setCaptureContent(false); + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setTelemetry(telemetry).setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithSessionIdleTimeout() throws Exception { + // Test that --session-idle-timeout flag is included when option is set + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setSessionIdleTimeoutSeconds(600) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } + + @Test + void startCliServerWithZeroSessionIdleTimeout() throws Exception { + // Zero timeout should not add the flag (treated as disabled) + var options = new CopilotClientOptions().setCliPath(NONEXISTENT_CLI).setSessionIdleTimeoutSeconds(0) + .setUseStdio(true); + var manager = new CliServerManager(options); + + var ex = assertThrows(IOException.class, () -> manager.startCliServer()); + assertNotNull(ex); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/java/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java new file mode 100644 index 000000000..edc503f94 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for closed-session guard functionality in CopilotSession. + * + *

+ * Verifies that all public methods that interact with session state throw + * IllegalStateException when invoked after close(), and that close() itself is + * idempotent. + *

+ */ +public class ClosedSessionGuardTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that send(String) throws IllegalStateException after session is + * terminated. + */ + @Test + void testSendStringThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.send("test message"); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that send(MessageOptions) throws IllegalStateException after session + * is terminated. + */ + @Test + void testSendOptionsThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.send(new MessageOptions().setPrompt("test message")); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that sendAndWait(String) throws IllegalStateException after session + * is terminated. + */ + @Test + void testSendAndWaitStringThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait("test message"); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that sendAndWait(MessageOptions) throws IllegalStateException after + * session is terminated. + */ + @Test + void testSendAndWaitOptionsThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait(new MessageOptions().setPrompt("test message")); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that sendAndWait(MessageOptions, long) throws IllegalStateException + * after session is terminated. + */ + @Test + void testSendAndWaitWithTimeoutThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait(new MessageOptions().setPrompt("test message"), 5000); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that on(Consumer) throws IllegalStateException after session is + * terminated. + */ + @Test + void testOnConsumerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.on(evt -> { + // Handler should never be registered + }); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that on(Class, Consumer) throws IllegalStateException after session + * is terminated. + */ + @Test + void testOnTypedConsumerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.on(AssistantMessageEvent.class, msg -> { + // Handler should never be registered + }); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that getMessages() throws IllegalStateException after session is + * terminated. + */ + @Test + void testGetMessagesThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.getMessages(); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that abort() throws IllegalStateException after session is + * terminated. + */ + @Test + void testAbortThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.abort(); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that setEventErrorHandler() throws IllegalStateException after + * session is terminated. + */ + @Test + void testSetEventErrorHandlerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.setEventErrorHandler((event, ex) -> { + // Handler should never be set + }); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that setEventErrorPolicy() throws IllegalStateException after + * session is terminated. + */ + @Test + void testSetEventErrorPolicyThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + }); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); + } + } + + /** + * Verifies that getSessionId() still works after session is terminated (it's + * just a field read). + */ + @Test + void testGetSessionIdWorksAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionIdBeforeClose = session.getSessionId(); + session.close(); + + String sessionIdAfterClose = session.getSessionId(); + assertEquals(sessionIdBeforeClose, sessionIdAfterClose, "Session ID should remain accessible after close"); + } + } + + /** + * Verifies that getWorkspacePath() still works after session is terminated + * (it's just a field read). + */ + @Test + void testGetWorkspacePathWorksAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String pathBeforeClose = session.getWorkspacePath(); + session.close(); + + String pathAfterClose = session.getWorkspacePath(); + assertEquals(pathBeforeClose, pathAfterClose, "Workspace path should remain accessible after close"); + } + } + + /** + * Verifies that close() is idempotent and can be called multiple times safely. + */ + @Test + void testCloseIsIdempotent() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + // First close should succeed + assertDoesNotThrow(() -> session.close()); + + // Second close should also succeed (no-op) + assertDoesNotThrow(() -> session.close()); + + // Third close should also succeed (no-op) + assertDoesNotThrow(() -> session.close()); + } + } + + /** + * Verifies that try-with-resources double-close scenario works correctly. + */ + @Test + void testTryWithResourcesDoubleClose() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + try (session) { + // Manual close within try-with-resources + session.close(); + // Automatic close will happen at end of block + } // Second close happens here + + // Should be able to verify it's closed + assertThrows(IllegalStateException.class, () -> { + session.send("test"); + }); + } + } + + /** + * Verifies that setModel() throws IllegalStateException after session is + * terminated. + */ + @Test + void testSetModelThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + + assertThrows(IllegalStateException.class, () -> { + session.setModel("gpt-4.1"); + }); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CommandsTest.java b/java/src/test/java/com/github/copilot/sdk/CommandsTest.java new file mode 100644 index 000000000..6bddbed28 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CommandsTest.java @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CommandContext; +import com.github.copilot.sdk.json.CommandDefinition; +import com.github.copilot.sdk.json.CommandHandler; +import com.github.copilot.sdk.json.CommandWireDefinition; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Unit tests for the Commands feature (CommandDefinition, CommandContext, + * SessionConfig.commands, ResumeSessionConfig.commands, and the wire + * representation). + * + *

+ * Ported from {@code CommandsTests.cs} in the reference implementation dotnet + * SDK. + *

+ */ +class CommandsTest { + + @Test + void commandDefinitionHasRequiredProperties() { + CommandHandler handler = context -> CompletableFuture.completedFuture(null); + var cmd = new CommandDefinition().setName("deploy").setDescription("Deploy the app").setHandler(handler); + + assertEquals("deploy", cmd.getName()); + assertEquals("Deploy the app", cmd.getDescription()); + assertNotNull(cmd.getHandler()); + } + + @Test + void commandContextHasAllProperties() { + var ctx = new CommandContext().setSessionId("session-1").setCommand("/deploy production") + .setCommandName("deploy").setArgs("production"); + + assertEquals("session-1", ctx.getSessionId()); + assertEquals("/deploy production", ctx.getCommand()); + assertEquals("deploy", ctx.getCommandName()); + assertEquals("production", ctx.getArgs()); + } + + @Test + void sessionConfigCommandsAreCloned() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler))); + + var clone = config.clone(); + + assertNotNull(clone.getCommands()); + assertEquals(1, clone.getCommands().size()); + assertEquals("deploy", clone.getCommands().get(0).getName()); + + // Collections should be independent β€” clone list is a copy + assertNotSame(config.getCommands(), clone.getCommands()); + } + + @Test + void resumeConfigCommandsAreCloned() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCommands(List.of(new CommandDefinition().setName("deploy").setHandler(handler))); + + var clone = config.clone(); + + assertNotNull(clone.getCommands()); + assertEquals(1, clone.getCommands().size()); + assertEquals("deploy", clone.getCommands().get(0).getName()); + } + + @Test + void buildCreateRequestIncludesCommandWireDefinitions() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands( + List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler), + new CommandDefinition().setName("rollback").setHandler(handler))); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNotNull(request.getCommands()); + assertEquals(2, request.getCommands().size()); + assertEquals("deploy", request.getCommands().get(0).getName()); + assertEquals("Deploy", request.getCommands().get(0).getDescription()); + assertEquals("rollback", request.getCommands().get(1).getName()); + assertNull(request.getCommands().get(1).getDescription()); + } + + @Test + void buildResumeRequestIncludesCommandWireDefinitions() { + CommandHandler handler = ctx -> CompletableFuture.completedFuture(null); + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCommands( + List.of(new CommandDefinition().setName("deploy").setDescription("Deploy").setHandler(handler))); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertNotNull(request.getCommands()); + assertEquals(1, request.getCommands().size()); + assertEquals("deploy", request.getCommands().get(0).getName()); + assertEquals("Deploy", request.getCommands().get(0).getDescription()); + } + + @Test + void buildCreateRequestWithNoCommandsHasNullCommandsList() { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getCommands()); + } + + @Test + void commandWireDefinitionHasNameAndDescription() { + var wire = new CommandWireDefinition("deploy", "Deploy the app"); + + assertEquals("deploy", wire.getName()); + assertEquals("Deploy the app", wire.getDescription()); + } + + @Test + void commandWireDefinitionNullDescriptionAllowed() { + var wire = new CommandWireDefinition("rollback", null); + + assertEquals("rollback", wire.getName()); + assertNull(wire.getDescription()); + } + + @Test + void commandWireDefinitionFluentSetters() { + var wire = new CommandWireDefinition(); + wire.setName("status"); + wire.setDescription("Show deployment status"); + + assertEquals("status", wire.getName()); + assertEquals("Show deployment status", wire.getDescription()); + } + + @Test + void commandWireDefinitionFluentSettersChaining() { + var wire = new CommandWireDefinition().setName("logs").setDescription("View application logs"); + + assertEquals("logs", wire.getName()); + assertEquals("View application logs", wire.getDescription()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CompactionTest.java b/java/src/test/java/com/github/copilot/sdk/CompactionTest.java new file mode 100644 index 000000000..306eeb6c7 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CompactionTest.java @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionCompactionCompleteEvent; +import com.github.copilot.sdk.generated.SessionCompactionStartEvent; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for compaction and infinite sessions functionality. + * + *

+ * These tests verify that sessions can trigger compaction with low thresholds + * and emit appropriate events. Snapshots are stored in + * test/snapshots/compaction/. + *

+ */ +public class CompactionTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that compaction is triggered with low threshold and emits events. + * + *

+ * Disabled due to flakiness β€” compaction timing is non-deterministic and the + * snapshot cannot reliably match across platforms. The reference implementation + * (nodejs) also skips this test. See copilot-sdk#1227. + * + * @see Snapshot: + * compaction/should_trigger_compaction_with_low_threshold_and_emit_events + */ + @Test + @Disabled("Flaky: compaction timing varies by platform β€” see https://github.com/github/copilot-sdk/issues/1227") + @Timeout(value = 300, unit = TimeUnit.SECONDS) + void testShouldTriggerCompactionWithLowThresholdAndEmitEvents() throws Exception { + ctx.configureForTest("compaction", "should_trigger_compaction_with_low_threshold_and_emit_events"); + + // Create session with very low compaction thresholds to trigger compaction + // quickly + var infiniteConfig = new InfiniteSessionConfig().setEnabled(true) + // Trigger background compaction at 0.5% context usage (~1000 tokens) + .setBackgroundCompactionThreshold(0.005) + // Block at 1% to ensure compaction runs + .setBufferExhaustionThreshold(0.01); + + var config = new SessionConfig().setInfiniteSessions(infiniteConfig) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var events = new ArrayList(); + var compactionCompleteLatch = new CountDownLatch(1); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.on(event -> { + events.add(event); + if (event instanceof SessionCompactionCompleteEvent) { + compactionCompleteLatch.countDown(); + } + }); + + // Send multiple messages to fill up the context window + // With such low thresholds, even a few messages should trigger compaction + session.sendAndWait(new MessageOptions().setPrompt("Tell me a story about a dragon. Be detailed.")).get(60, + TimeUnit.SECONDS); + session.sendAndWait( + new MessageOptions().setPrompt("Continue the story with more details about the dragon's castle.")) + .get(60, TimeUnit.SECONDS); + session.sendAndWait(new MessageOptions().setPrompt("Now describe the dragon's treasure in great detail.")) + .get(60, TimeUnit.SECONDS); + + // Wait for compaction to complete - it may arrive slightly after sendAndWait + // returns due to async event delivery from the CLI + assertTrue(compactionCompleteLatch.await(30, TimeUnit.SECONDS), + "Should have received a compaction complete event within 30 seconds"); + long compactionStartCount = events.stream().filter(e -> e instanceof SessionCompactionStartEvent).count(); + long compactionCompleteCount = events.stream().filter(e -> e instanceof SessionCompactionCompleteEvent) + .count(); + + // Should have triggered compaction at least once + assertTrue(compactionStartCount >= 1, + "Should have triggered compaction start at least once, got: " + compactionStartCount); + assertTrue(compactionCompleteCount >= 1, + "Should have triggered compaction complete at least once, got: " + compactionCompleteCount); + + // Compaction should have succeeded + SessionCompactionCompleteEvent lastCompactionComplete = events.stream() + .filter(e -> e instanceof SessionCompactionCompleteEvent) + .map(e -> (SessionCompactionCompleteEvent) e).reduce((first, second) -> second).orElse(null); + + assertNotNull(lastCompactionComplete); + assertTrue(lastCompactionComplete.getData().success(), "Compaction should have succeeded"); + + // Verify the session still works after compaction + AssistantMessageEvent answer = session + .sendAndWait(new MessageOptions().setPrompt("What was the story about?")).get(60, TimeUnit.SECONDS); + + assertNotNull(answer); + assertNotNull(answer.getData().content()); + // Should remember it was about a dragon (context preserved via summary) + assertTrue(answer.getData().content().toLowerCase().contains("dragon"), + "Should remember the story was about a dragon: " + answer.getData().content()); + + session.close(); + } + } + + /** + * Verifies that compaction events are not emitted when infinite sessions is + * disabled. + * + * @see Snapshot: + * compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled + */ + @Test + void testShouldNotEmitCompactionEventsWhenInfiniteSessionsDisabled() throws Exception { + ctx.configureForTest("compaction", "should_not_emit_compaction_events_when_infinite_sessions_disabled"); + + var infiniteConfig = new InfiniteSessionConfig().setEnabled(false); + + var config = new SessionConfig().setInfiniteSessions(infiniteConfig) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var compactionEvents = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.on(event -> { + if (event instanceof SessionCompactionStartEvent || event instanceof SessionCompactionCompleteEvent) { + compactionEvents.add(event); + } + }); + + session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, TimeUnit.SECONDS); + + // Should not have any compaction events when disabled + assertEquals(0, compactionEvents.size(), + "Should not have any compaction events when infinite sessions is disabled"); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/java/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java new file mode 100644 index 000000000..09bd3ee38 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -0,0 +1,408 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.DefaultAgentConfig; +import com.github.copilot.sdk.json.ExitPlanModeResult; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.ModelInfo; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; +import com.github.copilot.sdk.json.TelemetryConfig; + +class ConfigCloneTest { + + @Test + void copilotClientOptionsCloneBasic() { + CopilotClientOptions original = new CopilotClientOptions(); + original.setCliPath("/usr/local/bin/copilot"); + original.setLogLevel("debug"); + original.setPort(9000); + original.setGitHubToken("ghp_test"); + original.setUseLoggedInUser(false); + original.setCopilotHome("/custom/copilot/home"); + original.setRemote(true); + original.setSessionIdleTimeoutSeconds(600); + original.setUseStdio(false); + original.setTcpConnectionToken("my-token-123"); + + CopilotClientOptions cloned = original.clone(); + + assertEquals(original.getCliPath(), cloned.getCliPath()); + assertEquals(original.getLogLevel(), cloned.getLogLevel()); + assertEquals(original.getPort(), cloned.getPort()); + assertEquals(original.getGitHubToken(), cloned.getGitHubToken()); + assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser()); + assertEquals(original.getCopilotHome(), cloned.getCopilotHome()); + assertEquals(original.isRemote(), cloned.isRemote()); + assertEquals(original.getSessionIdleTimeoutSeconds(), cloned.getSessionIdleTimeoutSeconds()); + assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken()); + } + + @Test + void copilotClientOptionsArrayIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + String[] args = {"--flag1", "--flag2"}; + original.setCliArgs(args); + + CopilotClientOptions cloned = original.clone(); + + // Mutate the source array after set β€” should not affect original or clone + args[0] = "--changed"; + + assertEquals("--flag1", original.getCliArgs()[0]); + assertEquals("--flag1", cloned.getCliArgs()[0]); + + // getCliArgs() returns a copy, so mutating it should not affect internals + original.getCliArgs()[0] = "--mutated"; + assertEquals("--flag1", original.getCliArgs()[0]); + } + + @Test + void copilotClientOptionsEnvironmentIndependence() { + CopilotClientOptions original = new CopilotClientOptions(); + Map env = new HashMap<>(); + env.put("KEY1", "value1"); + original.setEnvironment(env); + + CopilotClientOptions cloned = original.clone(); + + // Mutate the source map after set β€” should not affect original or clone + env.put("KEY2", "value2"); + + assertEquals(1, original.getEnvironment().size()); + assertEquals(1, cloned.getEnvironment().size()); + + // getEnvironment() returns a copy, so mutating it should not affect internals + original.getEnvironment().put("KEY3", "value3"); + assertEquals(1, original.getEnvironment().size()); + } + + @Test + void copilotClientOptionsOnListModelsCloned() { + CopilotClientOptions original = new CopilotClientOptions(); + List models = List.of(new ModelInfo()); + original.setOnListModels(() -> CompletableFuture.completedFuture(models)); + + CopilotClientOptions cloned = original.clone(); + + assertNotNull(cloned.getOnListModels()); + assertSame(original.getOnListModels(), cloned.getOnListModels()); + } + + @Test + void sessionConfigCloneBasic() { + SessionConfig original = new SessionConfig(); + original.setSessionId("my-session"); + original.setClientName("my-app"); + original.setModel("gpt-4o"); + original.setStreaming(true); + + SessionConfig cloned = original.clone(); + + assertEquals(original.getSessionId(), cloned.getSessionId()); + assertEquals(original.getClientName(), cloned.getClientName()); + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void sessionConfigListIndependence() { + SessionConfig original = new SessionConfig(); + List toolList = new ArrayList<>(); + toolList.add("grep"); + toolList.add("bash"); + original.setAvailableTools(toolList); + original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b"))); + + SessionConfig cloned = original.clone(); + + // Mutate the original list directly to test independence + toolList.add("web"); + + // The cloned config should be unaffected by mutations to the original list + assertEquals(2, cloned.getAvailableTools().size()); + assertEquals(3, original.getAvailableTools().size()); + assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories()); + } + + @Test + void sessionConfigAgentAndOnEventCloned() { + Consumer handler = event -> { + }; + SessionConfig original = new SessionConfig(); + original.setAgent("my-agent"); + original.setOnEvent(handler); + + SessionConfig cloned = original.clone(); + + assertEquals("my-agent", cloned.getAgent()); + assertSame(handler, cloned.getOnEvent()); + } + + @Test + void resumeSessionConfigCloneBasic() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setModel("o1"); + original.setStreaming(false); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals(original.getModel(), cloned.getModel()); + assertEquals(original.isStreaming(), cloned.isStreaming()); + } + + @Test + void resumeSessionConfigAgentAndOnEventCloned() { + Consumer handler = event -> { + }; + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setAgent("my-agent"); + original.setOnEvent(handler); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals("my-agent", cloned.getAgent()); + assertSame(handler, cloned.getOnEvent()); + } + + @Test + void messageOptionsCloneBasic() { + MessageOptions original = new MessageOptions(); + original.setPrompt("What is 2+2?"); + original.setMode("immediate"); + + MessageOptions cloned = original.clone(); + + assertEquals(original.getPrompt(), cloned.getPrompt()); + assertEquals(original.getMode(), cloned.getMode()); + } + + @Test + void sessionConfigEnableSessionTelemetryCopied() { + SessionConfig original = new SessionConfig(); + original.setEnableSessionTelemetry(false); + + SessionConfig cloned = original.clone(); + + assertFalse(cloned.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void sessionConfigEnableSessionTelemetryDefaultIsNull() { + SessionConfig original = new SessionConfig(); + + SessionConfig cloned = original.clone(); + + assertTrue(cloned.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void resumeSessionConfigEnableSessionTelemetryCopied() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setEnableSessionTelemetry(false); + + ResumeSessionConfig cloned = original.clone(); + + assertFalse(cloned.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void resumeSessionConfigEnableSessionTelemetryDefaultIsNull() { + ResumeSessionConfig original = new ResumeSessionConfig(); + + ResumeSessionConfig cloned = original.clone(); + + assertTrue(cloned.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void clonePreservesNullFields() { + CopilotClientOptions opts = new CopilotClientOptions(); + CopilotClientOptions optsClone = opts.clone(); + assertNull(optsClone.getCliPath()); + + SessionConfig cfg = new SessionConfig(); + SessionConfig cfgClone = cfg.clone(); + assertNull(cfgClone.getModel()); + + MessageOptions msg = new MessageOptions(); + MessageOptions msgClone = msg.clone(); + assertNull(msgClone.getMode()); + } + + @Test + @SuppressWarnings("deprecation") + void copilotClientOptionsDeprecatedAutoRestart() { + CopilotClientOptions opts = new CopilotClientOptions(); + assertFalse(opts.isAutoRestart()); + opts.setAutoRestart(true); + assertTrue(opts.isAutoRestart()); + } + + @Test + void copilotClientOptionsSetCliArgsNullClearsExisting() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setCliArgs(new String[]{"--flag1"}); + assertNotNull(opts.getCliArgs()); + + // Setting null should clear the existing array + opts.setCliArgs(null); + assertNotNull(opts.getCliArgs()); + assertEquals(0, opts.getCliArgs().length); + } + + @Test + void copilotClientOptionsSetEnvironmentNullClearsExisting() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setEnvironment(Map.of("KEY", "VALUE")); + assertNotNull(opts.getEnvironment()); + + // Setting null should clear the existing map (clears in-place β†’ returns empty + // map) + opts.setEnvironment(null); + var env = opts.getEnvironment(); + assertTrue(env == null || env.isEmpty()); + } + + @Test + @SuppressWarnings("deprecation") + void copilotClientOptionsDeprecatedGithubToken() { + CopilotClientOptions opts = new CopilotClientOptions(); + opts.setGithubToken("ghp_deprecated_token"); + assertEquals("ghp_deprecated_token", opts.getGithubToken()); + assertEquals("ghp_deprecated_token", opts.getGitHubToken()); + } + + @Test + void copilotClientOptionsSetTelemetry() { + var telemetry = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318"); + var opts = new CopilotClientOptions(); + opts.setTelemetry(telemetry); + assertSame(telemetry, opts.getTelemetry()); + } + + @Test + void copilotClientOptionsClearUseLoggedInUser() { + var opts = new CopilotClientOptions(); + opts.setUseLoggedInUser(true); + opts.clearUseLoggedInUser(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); + } + + @Test + void resumeSessionConfigAllSetters() { + var config = new ResumeSessionConfig(); + + var sysMsg = new SystemMessageConfig(); + config.setSystemMessage(sysMsg); + assertSame(sysMsg, config.getSystemMessage()); + + config.setAvailableTools(List.of("bash", "read_file")); + assertEquals(List.of("bash", "read_file"), config.getAvailableTools()); + + config.setExcludedTools(List.of("write_file")); + assertEquals(List.of("write_file"), config.getExcludedTools()); + + config.setReasoningEffort("high"); + assertEquals("high", config.getReasoningEffort()); + + config.setWorkingDirectory("/project/src"); + assertEquals("/project/src", config.getWorkingDirectory()); + + config.setConfigDir("/home/user/.config/copilot"); + assertEquals("/home/user/.config/copilot", config.getConfigDir()); + + config.setSkillDirectories(List.of("/skills/custom")); + assertEquals(List.of("/skills/custom"), config.getSkillDirectories()); + + config.setDisabledSkills(List.of("some-skill")); + assertEquals(List.of("some-skill"), config.getDisabledSkills()); + + var infiniteConfig = new InfiniteSessionConfig().setEnabled(true); + config.setInfiniteSessions(infiniteConfig); + assertSame(infiniteConfig, config.getInfiniteSessions()); + } + + @Test + void sessionConfigNewFieldsCloned() { + SessionConfig original = new SessionConfig(); + original.setGitHubToken("ghp_per_session_token"); + DefaultAgentConfig defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); + original.setDefaultAgent(defaultAgent); + + SessionConfig cloned = original.clone(); + + assertEquals("ghp_per_session_token", cloned.getGitHubToken()); + assertSame(defaultAgent, cloned.getDefaultAgent()); + } + + @Test + void resumeSessionConfigNewFieldsCloned() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setGitHubToken("ghp_per_session_token"); + DefaultAgentConfig defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); + original.setDefaultAgent(defaultAgent); + + ResumeSessionConfig cloned = original.clone(); + + assertEquals("ghp_per_session_token", cloned.getGitHubToken()); + assertSame(defaultAgent, cloned.getDefaultAgent()); + } + + @Test + void copilotClientOptionsSessionIdleTimeoutCloned() { + CopilotClientOptions original = new CopilotClientOptions(); + original.setSessionIdleTimeoutSeconds(600); + + CopilotClientOptions cloned = original.clone(); + + assertEquals(600, cloned.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void sessionConfigCloneCopiesModeSwitchHandlers() { + SessionConfig original = new SessionConfig(); + original.setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + original.setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); + } + + @Test + void resumeSessionConfigCloneCopiesModeSwitchHandlers() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + original.setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java new file mode 100644 index 000000000..14ed8ca89 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CopilotClientTest.java @@ -0,0 +1,536 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PingResponse; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.SessionLifecycleEventTypes; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Optional; + +/** + * Tests for CopilotClient. + * + * Note: These tests require the Copilot CLI to be installed. Set the + * COPILOT_CLI_PATH environment variable to the path to the CLI, or run 'npm + * install' in the nodejs directory. + */ +public class CopilotClientTest { + + private static String cliPath; + + @BeforeAll + static void setup() { + cliPath = TestUtil.findCliPath(); + } + + @Test + void testClientConstruction() { + var client = new CopilotClient(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + client.close(); + } + + @Test + void testClientConstructionWithOptions() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setLogLevel("debug").setAutoStart(false); + + var client = new CopilotClient(options); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + client.close(); + } + + @Test + void testCliUrlAutoCorrectsUseStdio() { + var options = new CopilotClientOptions().setCliUrl("localhost:3000").setUseStdio(true); + + // Should NOT throw - useStdio is auto-corrected to false when cliUrl is set + var client = new CopilotClient(options); + assertFalse(options.isUseStdio(), "useStdio should be auto-corrected to false when cliUrl is set"); + client.close(); + } + + @Test + void testCliUrlOnlyConstruction() { + var options = new CopilotClientOptions().setCliUrl("localhost:4321"); + + // Should work without explicitly setting useStdio to false + var client = new CopilotClient(options); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + assertFalse(options.isUseStdio(), "useStdio should be auto-corrected to false when cliUrl is set"); + client.close(); + } + + @Test + void testCliUrlMutualExclusionWithCliPath() { + var options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testStartAndConnectUsingStdio() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + assertEquals(ConnectionState.CONNECTED, client.getState()); + + PingResponse pong = client.ping("test message").get(); + assertEquals("pong: test message", pong.message()); + assertTrue(pong.timestamp() >= 0); + + client.stop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + var options = new CopilotClientOptions().setCliPath(cliPath) + .setCliArgs(new String[]{"--nonexistent-flag-for-testing"}).setUseStdio(true); + + try (var client = new CopilotClient(options)) { + Exception ex = assertThrows(Exception.class, () -> client.start().get()); + Throwable root = ex instanceof ExecutionException && ex.getCause() != null ? ex.getCause() : ex; + String message = root.getMessage(); + assertNotNull(message); + assertTrue(message.toLowerCase().contains("stderr") || message.toLowerCase().contains("unexpectedly"), + "Error should include stderr or unexpected exit details: " + message); + } + } + + @Test + void testStartAndConnectUsingTcp() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(false))) { + client.start().get(); + assertEquals(ConnectionState.CONNECTED, client.getState()); + + PingResponse pong = client.ping("test message").get(); + assertEquals("pong: test message", pong.message()); + + client.stop().get(); + } + } + + @Test + void testForceStopWithoutCleanup() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { + client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + client.forceStop().get(); + + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testGitHubTokenOptionAccepted() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token"); + + assertEquals("gho_test_token", options.getGitHubToken()); + } + + @Test + void testUseLoggedInUserDefaultsToNull() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli"); + + assertTrue(options.getUseLoggedInUser().isEmpty()); + } + + @Test + void testExplicitUseLoggedInUserFalse() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setUseLoggedInUser(false); + + assertEquals(Optional.of(false), options.getUseLoggedInUser()); + } + + @Test + void testExplicitUseLoggedInUserTrueWithGitHubToken() { + var options = new CopilotClientOptions().setCliPath("/path/to/cli").setGitHubToken("gho_test_token") + .setUseLoggedInUser(true); + + assertEquals(Optional.of(true), options.getUseLoggedInUser()); + } + + @Test + void testGitHubTokenWithCliUrlThrows() { + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setGitHubToken("gho_test_token"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testUseLoggedInUserWithCliUrlThrows() { + var options = new CopilotClientOptions().setCliUrl("localhost:8080").setUseLoggedInUser(false); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testSessionIdleTimeoutSecondsDefaultsToNull() { + var options = new CopilotClientOptions(); + + assertTrue(options.getSessionIdleTimeoutSeconds().isEmpty()); + } + + @Test + void testSessionIdleTimeoutSecondsOptionAccepted() { + var options = new CopilotClientOptions().setSessionIdleTimeoutSeconds(600); + + assertEquals(600, options.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void testTcpConnectionTokenWithUseStdioThrows() { + var options = new CopilotClientOptions().setUseStdio(true).setTcpConnectionToken("my-token"); + + assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options)); + } + + @Test + void testTcpConnectionTokenAcceptedInTcpMode() { + var options = new CopilotClientOptions().setUseStdio(false).setTcpConnectionToken("my-token"); + + // Should not throw + try (var client = new CopilotClient(options)) { + assertNotNull(client); + } + } + + @Test + void testCopilotHomeOptionSetOnOptions() { + var options = new CopilotClientOptions().setCopilotHome("/custom/home"); + + assertEquals("/custom/home", options.getCopilotHome()); + } + + // ===== onLifecycle tests ===== + + /** + * Gets the internal LifecycleEventManager from a CopilotClient via reflection + * so we can dispatch events for testing. + */ + private static LifecycleEventManager getLifecycleManager(CopilotClient client) throws Exception { + Field f = CopilotClient.class.getDeclaredField("lifecycleManager"); + f.setAccessible(true); + return (LifecycleEventManager) f.get(client); + } + + private static SessionLifecycleEvent lifecycleEvent(String type) { + var e = new SessionLifecycleEvent(); + e.setType(type); + e.setSessionId("test-session-id"); + return e; + } + + @Test + void testOnLifecycleWildcardReceivesAllEvents() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + client.onLifecycle(received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + + assertEquals(2, received.size()); + assertEquals(SessionLifecycleEventTypes.CREATED, received.get(0).getType()); + assertEquals(SessionLifecycleEventTypes.DELETED, received.get(1).getType()); + } + } + + @Test + void testOnLifecycleTypedReceivesOnlyMatchingEvents() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + client.onLifecycle(SessionLifecycleEventTypes.CREATED, received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + + assertEquals(1, received.size()); + assertEquals(SessionLifecycleEventTypes.CREATED, received.get(0).getType()); + } + } + + @Test + void testOnLifecycleUnsubscribeStopsDelivery() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + AutoCloseable sub = client.onLifecycle(received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + assertEquals(1, received.size()); + + sub.close(); + + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.DELETED)); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + } + + @Test + void testOnLifecycleTypedUnsubscribeStopsDelivery() throws Exception { + try (var client = new CopilotClient()) { + var received = new ArrayList(); + AutoCloseable sub = client.onLifecycle(SessionLifecycleEventTypes.UPDATED, received::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.UPDATED)); + assertEquals(1, received.size()); + + sub.close(); + + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.UPDATED)); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + } + + @Test + void testOnLifecycleMultipleHandlers() throws Exception { + try (var client = new CopilotClient()) { + var wildcard = new ArrayList(); + var typed = new ArrayList(); + + client.onLifecycle(wildcard::add); + client.onLifecycle(SessionLifecycleEventTypes.CREATED, typed::add); + + LifecycleEventManager mgr = getLifecycleManager(client); + mgr.dispatch(lifecycleEvent(SessionLifecycleEventTypes.CREATED)); + + assertEquals(1, wildcard.size()); + assertEquals(1, typed.size()); + } + } + + // ===== getState() coverage ===== + + @Test + void testGetStateErrorAfterFailedStart() throws Exception { + // Use a non-existent CLI path to trigger a startup failure + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Manually start to trigger the error + CompletableFuture startFuture = client.start(); + + // Wait for the start to fail + try { + startFuture.get(); + } catch (ExecutionException e) { + // Expected + } + + assertEquals(ConnectionState.ERROR, client.getState()); + } + } + + @Test + void testGetStateConnectingDuringStart() throws Exception { + // Use a non-existent CLI path; the future won't complete immediately + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Start is async - grab state before completion + client.start(); + + // The state should be either CONNECTING or ERROR depending on timing + ConnectionState state = client.getState(); + assertTrue(state == ConnectionState.CONNECTING || state == ConnectionState.ERROR, + "State should be CONNECTING or ERROR, was: " + state); + } + } + + // ===== ensureConnected throws when autoStart=false and not connected ===== + + @Test + void testEnsureConnectedThrowsWhenNotStartedAndAutoStartDisabled() { + var options = new CopilotClientOptions().setAutoStart(false); + + try (var client = new CopilotClient(options)) { + // Calling ping (which calls ensureConnected) without start() should throw + assertThrows(IllegalStateException.class, () -> client.ping("test")); + } + } + + // ===== close() idempotency ===== + + @Test + void testCloseIsIdempotent() { + var client = new CopilotClient(); + + // First close + client.close(); + // Second close should not throw + assertDoesNotThrow(() -> client.close()); + } + + @Test + void testCloseAfterFailedStart() throws Exception { + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + var client = new CopilotClient(options); + + CompletableFuture startFuture = client.start(); + try { + startFuture.get(); + } catch (ExecutionException e) { + // Expected + } + + // close() after a failed start should not throw + assertDoesNotThrow(() -> client.close()); + } + + // ===== stop() with no connection ===== + + @Test + void testStopWithNoConnectionCompletes() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + // stop() without start() should complete without error + client.stop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testForceStopWithNoConnectionCompletes() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false))) { + // forceStop() without start() should complete without error + client.forceStop().get(); + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + @Test + void testCloseSessionAfterStoppingClientDoesNotThrow() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) { + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + // Stop the client first (which closes the RPC connection) + client.stop().get(); + + // Then close the session - should not throw even though RPC is closed + assertDoesNotThrow(() -> session.close(), "Closing session after client.stop() should not throw exception"); + + // Verify session is terminated + assertThrows(IllegalStateException.class, () -> session.send("test"), + "Session should be terminated after close()"); + } + } + + // ===== start() idempotency ===== + + @Test + void testStartIsIdempotentSingleConnectionAttempt() throws Exception { + var options = new CopilotClientOptions().setCliPath("/nonexistent/path/to/cli").setAutoStart(false); + + try (var client = new CopilotClient(options)) { + client.start(); + client.start(); + + // Both calls should result in the same state (single connection attempt) + ConnectionState state = client.getState(); + assertTrue(state == ConnectionState.CONNECTING || state == ConnectionState.ERROR, + "State should be CONNECTING or ERROR after start(), was: " + state); + } + } + + // ===== null options defaulting ===== + + @Test + void testNullOptionsDefaultsToEmpty() { + try (var client = new CopilotClient(null)) { + assertEquals(ConnectionState.DISCONNECTED, client.getState()); + } + } + + // ===== OnListModels ===== + + @Test + void testListModels_WithCustomHandler_CallsHandler() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("my-custom-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + try (var client = new CopilotClient(options)) { + var models = client.listModels().get(); + assertEquals(1, callCount[0]); + assertEquals(1, models.size()); + assertEquals("my-custom-model", models.get(0).getId()); + } + } + + @Test + void testListModels_WithCustomHandler_CachesResults() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("cached-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + try (var client = new CopilotClient(options)) { + client.listModels().get(); + client.listModels().get(); + assertEquals(1, callCount[0], "Handler should be called only once due to caching"); + } + } + + @Test + void testListModels_WithCustomHandler_WorksWithoutStart() throws Exception { + var customModels = new ArrayList(); + var model = new com.github.copilot.sdk.json.ModelInfo(); + model.setId("no-start-model"); + customModels.add(model); + + var callCount = new int[]{0}; + var options = new CopilotClientOptions().setOnListModels(() -> { + callCount[0]++; + return CompletableFuture.completedFuture(new ArrayList<>(customModels)); + }); + + // No start() needed when onListModels is provided + try (var client = new CopilotClient(options)) { + var models = client.listModels().get(); + assertEquals(1, callCount[0]); + assertEquals(1, models.size()); + assertEquals("no-start-model", models.get(0).getId()); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java new file mode 100644 index 000000000..6a2f75809 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -0,0 +1,955 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AbortEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.generated.SessionStartEvent; +import com.github.copilot.sdk.generated.ToolExecutionStartEvent; +import com.github.copilot.sdk.generated.UserMessageEvent; +import com.github.copilot.sdk.generated.rpc.SessionRpc; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.DefaultAgentConfig; +import com.github.copilot.sdk.json.SystemMessageConfig; +import com.github.copilot.sdk.json.ToolDefinition; + +/** + * Tests for CopilotSession. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/session/. + *

+ */ +public class CopilotSessionTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that a session can be created and closed properly. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents_createAndDestroy() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); + + List messages = session.getMessages().get(); + assertFalse(messages.isEmpty()); + assertTrue(messages.get(0) instanceof SessionStartEvent); + + session.close(); + + // Session should no longer be accessible - now throws IllegalStateException + try { + session.getMessages().get(); + fail("Expected exception for closed session"); + } catch (Exception e) { + // After our changes, we now get IllegalStateException directly + String message = e.getMessage(); + String causeMessage = e.getCause() != null ? e.getCause().getMessage() : null; + boolean matchesClosed = message != null && message.toLowerCase().contains("closed"); + boolean matchesNotFound = causeMessage != null && causeMessage.toLowerCase().contains("not found"); + assertTrue(matchesClosed || matchesNotFound); + } + } + } + + /** + * Verifies that sessions maintain conversation state across multiple messages. + * + * @see Snapshot: session/should_have_stateful_conversation + */ + @Test + void testShouldHaveStatefulConversation() throws Exception { + ctx.configureForTest("session", "should_have_stateful_conversation"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response1 = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?"), 60000) + .get(90, TimeUnit.SECONDS); + + assertNotNull(response1); + assertTrue(response1.getData().content().contains("2"), + "Response should contain 2: " + response1.getData().content()); + + AssistantMessageEvent response2 = session + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"), 60000) + .get(90, TimeUnit.SECONDS); + + assertNotNull(response2); + assertTrue(response2.getData().content().contains("4"), + "Response should contain 4: " + response2.getData().content()); + + session.close(); + } + } + + /** + * Verifies that session events (user.message, assistant.message, session.idle) + * are properly received. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List receivedEvents = new ArrayList<>(); + CompletableFuture idleReceived = new CompletableFuture<>(); + + session.on(evt -> { + receivedEvents.add(evt); + if (evt instanceof SessionIdleEvent) { + idleReceived.complete(null); + } + }); + + session.send(new MessageOptions().setPrompt("What is 100+200?")).get(); + + idleReceived.get(60, TimeUnit.SECONDS); + + assertFalse(receivedEvents.isEmpty()); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof UserMessageEvent)); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof AssistantMessageEvent)); + assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof SessionIdleEvent)); + + // Find the assistant message + AssistantMessageEvent assistantMsg = receivedEvents.stream().filter(e -> e instanceof AssistantMessageEvent) + .map(e -> (AssistantMessageEvent) e).findFirst().orElse(null); + + assertNotNull(assistantMsg); + assertTrue(assistantMsg.getData().content().contains("300"), + "Response should contain 300: " + assistantMsg.getData().content()); + + session.close(); + } + } + + /** + * Verifies that send() returns immediately while events stream in background. + * + * @see Snapshot: + * session/send_returns_immediately_while_events_stream_in_background + */ + @Test + void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception { + ctx.configureForTest("session", "send_returns_immediately_while_events_stream_in_background"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + var events = new ArrayList(); + var lastMessage = new AtomicReference(); + var done = new CompletableFuture(); + + session.on(evt -> { + events.add(evt.getType()); + if (evt instanceof AssistantMessageEvent msg) { + lastMessage.set(msg); + } else if (evt instanceof SessionIdleEvent) { + done.complete(null); + } + }); + + // Use a slow command so we can verify send() returns before completion + // Use String convenience overload (covers send(String) path) + session.send("Run 'sleep 2 && echo done'").get(); + + // At this point, we might not have received session.idle yet + // The event handling happens asynchronously + + // Wait for completion + done.get(60, TimeUnit.SECONDS); + + assertTrue(events.contains("session.idle")); + assertTrue(events.contains("assistant.message")); + assertNotNull(lastMessage.get()); + assertTrue(lastMessage.get().getData().content().contains("done"), + "Response should contain done: " + lastMessage.get().getData().content()); + + session.close(); + } + } + + /** + * Verifies that sendAndWait blocks until session is idle and returns the final + * assistant message. + * + * @see Snapshot: + * session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message + */ + @Test + void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() throws Exception { + ctx.configureForTest("session", "sendandwait_blocks_until_session_idle_and_returns_final_assistant_message"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + var events = new ArrayList(); + session.on(evt -> events.add(evt.getType())); + + // Use String convenience overload (covers sendAndWait(String) path) + AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertEquals("assistant.message", response.getType()); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); + assertTrue(events.contains("session.idle")); + assertTrue(events.contains("assistant.message")); + + session.close(); + } + } + + /** + * Verifies that a session can be resumed using the same client. + * + * @see Snapshot: session/should_resume_a_session_using_the_same_client + */ + @Test + void testShouldResumeSessionUsingTheSameClient() throws Exception { + ctx.configureForTest("session", "should_resume_a_session_using_the_same_client"); + + try (CopilotClient client = ctx.createClient()) { + // Create initial session + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session1.getSessionId(); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("2"), + "Response should contain 2: " + answer.getData().content()); + + // Resume using the same client + CopilotSession session2 = client.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + // Verify resumed session has the previous messages + List messages = session2.getMessages().get(60, TimeUnit.SECONDS); + boolean hasAssistantMessage = messages.stream().filter(m -> m instanceof AssistantMessageEvent) + .map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().content().contains("2")); + assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2"); + + // Can continue the conversation statefully + AssistantMessageEvent answer2 = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("4"), + "Follow-up response should contain 4: " + answer2.getData().content()); + + session2.close(); + } + } + + /** + * Verifies that a session can be resumed using a new client. + * + * @see Snapshot: session/should_resume_a_session_using_a_new_client + */ + @Test + @Tag("isolated-resume") + void testShouldResumeSessionUsingNewClient() throws Exception { + ctx.configureForTest("session", "should_resume_a_session_using_a_new_client"); + + // Use a single try-with-resources for the first client to keep it alive + // throughout the test, matching the behavior of other SDK implementations + try (CopilotClient client1 = ctx.createClient()) { + // Create initial session + CopilotSession session1 = client1 + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session1.getSessionId(); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("2"), + "Response should contain 2: " + answer.getData().content()); + + // Resume using a new client (keeping client1 alive) + try (CopilotClient client2 = ctx.createClient()) { + CopilotSession session2 = client2.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + // When resuming with a new client, validate messages contain expected types + List messages = session2.getMessages().get(60, TimeUnit.SECONDS); + assertTrue(messages.stream().anyMatch(m -> m instanceof UserMessageEvent), + "Should contain user.message event"); + assertTrue(messages.stream().anyMatch(m -> "session.resume".equals(m.getType())), + "Should contain session.resume event"); + + // Can continue the conversation statefully + AssistantMessageEvent answer2 = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("4"), + "Follow-up response should contain 4: " + answer2.getData().content()); + + session2.close(); + } + } + } + + /** + * Verifies that sessions work with appended system message configuration. + * + * @see Snapshot: + * session/should_create_a_session_with_appended_systemmessage_config + */ + @Test + void testShouldCreateSessionWithAppendedSystemMessageConfig() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_appended_systemmessage_config"); + + try (CopilotClient client = ctx.createClient()) { + String systemMessageSuffix = "End each response with the phrase 'Have a nice day!'"; + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(new SystemMessageConfig().setContent(systemMessageSuffix) + .setMode(SystemMessageMode.APPEND)); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("GitHub"), + "Response should contain GitHub: " + response.getData().content()); + assertTrue(response.getData().content().contains("Have a nice day!"), + "Response should end with 'Have a nice day!': " + response.getData().content()); + session.close(); + } + } + + /** + * Verifies that sessions work with replaced system message configuration. + * + * @see Snapshot: + * session/should_create_a_session_with_replaced_systemmessage_config + */ + @Test + void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_replaced_systemmessage_config"); + + try (CopilotClient client = ctx.createClient()) { + String testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly."; + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage( + new SystemMessageConfig().setContent(testSystemMessage).setMode(SystemMessageMode.REPLACE)); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("Testy McTestface"), + "Response should contain 'Testy McTestface': " + response.getData().content()); + session.close(); + } + } + + /** + * Verifies that a session can be aborted during tool execution. + * + * @see Snapshot: session/should_abort_a_session + */ + @Test + void testShouldAbortSession() throws Exception { + ctx.configureForTest("session", "should_abort_a_session"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + assertNotNull(session.getSessionId()); + + // Set up wait for tool execution to start BEFORE sending + var toolStartFuture = new CompletableFuture(); + var sessionIdleFuture = new CompletableFuture(); + + session.on(evt -> { + if (evt instanceof ToolExecutionStartEvent toolStart && !toolStartFuture.isDone()) { + toolStartFuture.complete(toolStart); + } else if (evt instanceof SessionIdleEvent idle && !sessionIdleFuture.isDone()) { + sessionIdleFuture.complete(idle); + } + }); + + // Send a message that will trigger a long-running shell command + session.send(new MessageOptions() + .setPrompt("run the shell command 'sleep 100' (note this works on both bash and PowerShell)")) + .get(); + + // Wait for the tool to start executing + toolStartFuture.get(60, TimeUnit.SECONDS); + + // Abort the session while the tool is running + session.abort(); + + // Wait for session to become idle after abort + sessionIdleFuture.get(30, TimeUnit.SECONDS); + + // The session should still be alive and usable after abort + List messages = session.getMessages().get(60, TimeUnit.SECONDS); + assertFalse(messages.isEmpty()); + + // Verify an abort event exists in messages + assertTrue(messages.stream().anyMatch(m -> m instanceof AbortEvent), "Expected an abort event in messages"); + + // We should be able to send another message + AssistantMessageEvent answer = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("4"), + "Response should contain 4: " + answer.getData().content()); + + session.close(); + } + } + + /** + * Verifies that sessions can be created with available tools configuration. + * + * @see Snapshot: session/should_create_a_session_with_availabletools + */ + @Test + void testShouldCreateSessionWithAvailableTools() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_availabletools"); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setAvailableTools(List.of("view", "edit")); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + session.close(); + } + } + + /** + * Verifies that sessions can be created with excluded tools configuration. + * + * @see Snapshot: session/should_create_a_session_with_excludedtools + */ + @Test + void testShouldCreateSessionWithExcludedTools() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_excludedtools"); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setExcludedTools(List.of("view")); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("2"), + "Response should contain 2: " + response.getData().content()); + session.close(); + } + } + + /** + * Verifies that an error is thrown when resuming a non-existent session. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldThrowErrorWhenResumingNonExistentSession() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + try { + client.resumeSession("non-existent-session-id", + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + fail("Expected exception when resuming non-existent session"); + } catch (Exception e) { + // Should throw an error + assertTrue(e.getMessage() != null || e.getCause() != null, "Exception should have a message or cause"); + } + } + } + + /** + * Verifies that sessions can be created with a custom config directory. + * + * @see Snapshot: session/should_create_session_with_custom_config_dir + */ + @Test + void testShouldCreateSessionWithCustomConfigDir() throws Exception { + ctx.configureForTest("session", "should_create_session_with_custom_config_dir"); + + try (CopilotClient client = ctx.createClient()) { + String customConfigDir = ctx.getWorkDir().resolve("custom-config").toString(); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setConfigDir(customConfigDir); + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + assertTrue(session.getSessionId().matches("^[a-f0-9-]+$")); + + // Session should work normally with custom config dir + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("2"), + "Response should contain 2: " + response.getData().content()); + + session.close(); + } + } + + // This test validates client-side timeout behavior. The snapshot has no + // assistant response because the test expects timeout BEFORE completion. + // Note: In CI mode, the proxy logs "No cached response found" errors to + // stderr, but these are expected - the timeout still triggers correctly. + /** + * Verifies that sendAndWait throws an exception on timeout. + * + * @see Snapshot: session/sendandwait_throws_on_timeout + */ + @Test + void testSendAndWaitThrowsOnTimeout() throws Exception { + ctx.configureForTest("session", "sendandwait_throws_on_timeout"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + // Use a short timeout that will trigger before any response + try { + session.sendAndWait(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'"), 100).get(30, + TimeUnit.SECONDS); + fail("Expected timeout exception"); + } catch (Exception e) { + // Should throw a timeout-related error from sendAndWait + String message = e.getMessage() != null ? e.getMessage().toLowerCase() : ""; + String causeMessage = e.getCause() != null && e.getCause().getMessage() != null + ? e.getCause().getMessage().toLowerCase() + : ""; + assertTrue( + message.contains("timeout") || message.contains("sendandwait timed out") + || causeMessage.contains("timeout") || causeMessage.contains("sendandwait timed out"), + "Should throw timeout exception, got: " + e.getMessage() + + (e.getCause() != null ? " caused by: " + e.getCause().getMessage() : "")); + } + + session.close(); + } + } + + /** + * Verifies that sessions can be listed. + * + * @see Snapshot: session/should_list_sessions + */ + @Test + void testShouldListSessions() throws Exception { + ctx.configureForTest("session", "should_list_sessions"); + + try (CopilotClient client = ctx.createClient()) { + // Create two sessions and send one message to each (matches snapshot format) + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session1.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); + + CopilotSession session2 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session2.sendAndWait(new MessageOptions().setPrompt("Say goodbye")).get(60, TimeUnit.SECONDS); + + // Small delay to ensure session files are written to disk + Thread.sleep(200); + + // List all sessions + var sessions = client.listSessions().get(30, TimeUnit.SECONDS); + + // Should have at least the sessions we created + assertNotNull(sessions); + assertFalse(sessions.isEmpty(), "Should have at least 1 session"); + + // Our sessions should be in the list + var sessionIds = sessions.stream().map(s -> s.getSessionId()).toList(); + assertTrue(sessionIds.contains(session1.getSessionId()), "Session 1 should be in the list"); + assertTrue(sessionIds.contains(session2.getSessionId()), "Session 2 should be in the list"); + + session1.close(); + session2.close(); + } + } + + /** + * Verifies that sessions can be deleted. + * + * @see Snapshot: session/should_delete_session + */ + @Test + void testShouldDeleteSession() throws Exception { + ctx.configureForTest("session", "should_delete_session"); + + try (CopilotClient client = ctx.createClient()) { + // Create a session + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session.getSessionId(); + + session.sendAndWait(new MessageOptions().setPrompt("Hello")).get(60, TimeUnit.SECONDS); + + // Delete the session using the client API + // In CI mode with replaying proxy, session files may not be persisted, + // so we handle the "session not found" case as acceptable + try { + client.deleteSession(sessionId).get(30, TimeUnit.SECONDS); + } catch (Exception e) { + // In CI replay mode, session files don't exist - this is expected + if (System.getenv("CI") != null && e.getMessage() != null && e.getMessage().contains("not found")) { + return; // Test passes - CI mode doesn't persist sessions + } + throw e; + } + + // Trying to resume the deleted session should fail + try { + client.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + fail("Expected exception when resuming deleted session"); + } catch (Exception e) { + // Should throw an error indicating session not found + assertTrue(e.getMessage() != null || e.getCause() != null, "Exception should have a message or cause"); + } + } + } + + /** + * Verifies that sessions can be created with custom tools. + * + * @see Snapshot: session/should_create_session_with_custom_tool + */ + @Test + void testShouldCreateSessionWithCustomTool() throws Exception { + ctx.configureForTest("session", "should_create_session_with_custom_tool"); + + // Define a custom get_secret_number tool + Map parameters = new java.util.HashMap<>(); + Map properties = new java.util.HashMap<>(); + Map keyProp = new java.util.HashMap<>(); + keyProp.put("type", "string"); + keyProp.put("description", "Key"); + properties.put("key", keyProp); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", java.util.List.of("key")); + + ToolDefinition getSecretNumberTool = ToolDefinition.create("get_secret_number", "Gets the secret number", + parameters, (invocation) -> { + Map args = invocation.getArguments(); + String key = (String) args.get("key"); + // Return 54321 for ALPHA, 0 otherwise + int result = "ALPHA".equals(key) ? 54321 : 0; + return CompletableFuture.completedFuture(String.valueOf(result)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setTools(java.util.List.of(getSecretNumberTool))) + .get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("What is the secret number for key ALPHA?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("54321"), + "Response should contain 54321: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that getLastSessionId returns the ID of the most recently used + * session. + * + * @see Snapshot: session/should_get_last_session_id + */ + @Test + void testShouldGetLastSessionId() throws Exception { + ctx.configureForTest("session", "should_get_last_session_id"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); + String sessionId = session.getSessionId(); + session.close(); + + // Poll until getLastSessionId returns the expected value. + // Session state is persisted asynchronously; polling keeps fast + // machines fast and slow CI safe (mirrors Node.js/.NET patterns). + String lastId = null; + long deadline = System.currentTimeMillis() + 10_000; + while (System.currentTimeMillis() < deadline) { + long remaining = Math.max(1, deadline - System.currentTimeMillis()); + long iterationTimeout = Math.min(remaining, 500); + try { + lastId = client.getLastSessionId().get(iterationTimeout, TimeUnit.MILLISECONDS); + } catch (java.util.concurrent.TimeoutException ignored) { + // RPC call took longer than the per-iteration cap; retry + continue; + } + if (sessionId.equals(lastId)) { + break; + } + Thread.sleep(50); + } + assertNotNull(lastId, "Last session ID should not be null"); + assertEquals(sessionId, lastId, "Last session ID should match the current session ID"); + } + } + + /** + * Verifies that listSessions returns metadata with optional context + * information. + * + * @see Snapshot: session/should_list_sessions + */ + @Test + void testListSessionsIncludesContextWhenAvailable() throws Exception { + ctx.configureForTest("session", "should_list_sessions"); + + try (CopilotClient client = ctx.createClient()) { + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + var sessions = client.listSessions().get(30, TimeUnit.SECONDS); + assertNotNull(sessions); + + // List may be empty or contain sessions depending on test environment + // The main goal is to verify the API works and context field is accessible + for (var s : sessions) { + assertNotNull(s.getSessionId()); + // Context field is optional + if (s.getContext() != null) { + // When context is present, cwd should be non-null + assertNotNull(s.getContext().getCwd()); + } + } + + session.close(); + } + } + + /** + * Verifies that SessionListFilter works with fluent setters. + */ + @Test + void testSessionListFilterFluentAPI() throws Exception { + ctx.configureForTest("session", "should_list_sessions"); + + try (CopilotClient client = ctx.createClient()) { + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + var filter = new com.github.copilot.sdk.json.SessionListFilter().setCwd("/test/path") + .setRepository("owner/repo").setBranch("main").setGitRoot("/test"); + + assertEquals("/test/path", filter.getCwd()); + assertEquals("owner/repo", filter.getRepository()); + assertEquals("main", filter.getBranch()); + assertEquals("/test", filter.getGitRoot()); + + var filteredSessions = client.listSessions(filter).get(30, TimeUnit.SECONDS); + assertNotNull(filteredSessions); + + session.close(); + } + } + + /** + * Verifies that getSessionMetadata returns metadata for a known session ID. + * + * @see Snapshot: session/should_get_session_metadata_by_id + */ + @Test + void testShouldGetSessionMetadataById() throws Exception { + ctx.configureForTest("session", "should_get_session_metadata_by_id"); + + try (CopilotClient client = ctx.createClient()) { + var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + // Send a message to persist the session to disk + session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); + + // Poll until metadata becomes available; the CLI persists session + // state asynchronously so it may not be queryable immediately + // (mirrors .NET WaitForConditionAsync pattern). + var sessionId = session.getSessionId(); + com.github.copilot.sdk.json.SessionMetadata metadata = null; + long deadline = System.currentTimeMillis() + 10_000; + while (System.currentTimeMillis() < deadline) { + long remaining = Math.max(1, deadline - System.currentTimeMillis()); + long iterationTimeout = Math.min(remaining, 500); + try { + metadata = client.getSessionMetadata(sessionId).get(iterationTimeout, TimeUnit.MILLISECONDS); + } catch (java.util.concurrent.TimeoutException ignored) { + // RPC call took longer than the per-iteration cap; retry + continue; + } + if (metadata != null) { + break; + } + Thread.sleep(50); + } + assertNotNull(metadata, "Timed out waiting for getSessionMetadata() to return the persisted session"); + assertEquals(sessionId, metadata.getSessionId(), "Metadata session ID should match"); + + // A non-existent session should return null + var notFound = client.getSessionMetadata("non-existent-session-id").get(30, TimeUnit.SECONDS); + assertNull(notFound, "Non-existent session should return null"); + + session.close(); + } + } + + /** + * Verifies that {@link CopilotSession#getRpc()} returns a non-null + * {@link SessionRpc} wired to the session's ID and that all namespace fields + * are present. + */ + @Test + void testGetRpcReturnsSessionRpcWithCorrectSessionId() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + SessionRpc rpc = session.getRpc(); + assertNotNull(rpc, "getRpc() must not return null"); + assertNotNull(rpc.agent, "SessionRpc.agent must not be null"); + assertNotNull(rpc.model, "SessionRpc.model must not be null"); + assertNotNull(rpc.tools, "SessionRpc.tools must not be null"); + assertNotNull(rpc.permissions, "SessionRpc.permissions must not be null"); + assertNotNull(rpc.commands, "SessionRpc.commands must not be null"); + assertNotNull(rpc.ui, "SessionRpc.ui must not be null"); + + session.close(); + } + } + + /** + * Verifies that sessions can be created with defaultAgent.excludedTools + * configuration. + * + * @see Snapshot: + * session/should_create_a_session_with_defaultagent_excludedtools + */ + @Test + void testShouldCreateSessionWithDefaultAgentExcludedTools() throws Exception { + ctx.configureForTest("session", "should_create_a_session_with_defaultagent_excludedtools"); + + Map parameters = new java.util.HashMap<>(); + parameters.put("type", "object"); + parameters.put("properties", new java.util.HashMap<>()); + + ToolDefinition secretTool = ToolDefinition.create("secret_tool", "A secret tool hidden from the default agent", + parameters, (invocation) -> CompletableFuture.completedFuture("SECRET")); + + try (CopilotClient client = ctx.createClient()) { + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setTools(List.of(secretTool)) + .setDefaultAgent(new DefaultAgentConfig().setExcludedTools(List.of("secret_tool"))); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java new file mode 100644 index 000000000..3c83b8286 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/DataObjectCoverageTest.java @@ -0,0 +1,290 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.GetForegroundSessionResponse; +import com.github.copilot.sdk.json.McpHttpServerConfig; +import com.github.copilot.sdk.json.McpStdioServerConfig; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PostToolUseHookInput; +import com.github.copilot.sdk.json.PostToolUseHookOutput; +import com.github.copilot.sdk.json.PreToolUseHookInput; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SectionOverride; +import com.github.copilot.sdk.json.SetForegroundSessionRequest; +import com.github.copilot.sdk.json.SetForegroundSessionResponse; +import com.github.copilot.sdk.json.ToolBinaryResult; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * Unit tests for various data transfer objects and record types that were + * missing coverage, including hook output factory methods, record constructors, + * and getters for hook inputs. + */ +class DataObjectCoverageTest { + + // ===== PreToolUseHookOutput factory methods ===== + + @Test + void preToolUseHookOutputDenyWithReason() { + var output = PreToolUseHookOutput.deny("Security policy violation"); + assertEquals("deny", output.permissionDecision()); + assertEquals("Security policy violation", output.permissionDecisionReason()); + assertNull(output.modifiedArgs()); + } + + @Test + void preToolUseHookOutputAsk() { + var output = PreToolUseHookOutput.ask(); + assertEquals("ask", output.permissionDecision()); + assertNull(output.permissionDecisionReason()); + } + + @Test + void preToolUseHookOutputWithModifiedArgs() { + ObjectNode args = JsonNodeFactory.instance.objectNode(); + args.put("path", "/safe/path"); + + var output = PreToolUseHookOutput.withModifiedArgs("allow", args); + assertEquals("allow", output.permissionDecision()); + assertEquals(args, output.modifiedArgs()); + } + + // ===== PostToolUseHookOutput record ===== + + @Test + void postToolUseHookOutputRecord() { + var output = new PostToolUseHookOutput(null, "Extra context", false); + assertNull(output.modifiedResult()); + assertEquals("Extra context", output.additionalContext()); + assertFalse(output.suppressOutput()); + } + + // ===== ToolBinaryResult record ===== + + @Test + void toolBinaryResultRecord() { + var result = new ToolBinaryResult("base64data==", "image/png", "image", "A chart"); + assertEquals("base64data==", result.data()); + assertEquals("image/png", result.mimeType()); + assertEquals("image", result.type()); + assertEquals("A chart", result.description()); + } + + // ===== GetForegroundSessionResponse record ===== + + @Test + void getForegroundSessionResponseRecord() { + var response = new GetForegroundSessionResponse("session-123", "/home/user/project"); + assertEquals("session-123", response.sessionId()); + assertEquals("/home/user/project", response.workspacePath()); + } + + // ===== SetForegroundSessionRequest record ===== + + @Test + void setForegroundSessionRequestRecord() { + var request = new SetForegroundSessionRequest("session-123"); + assertEquals("session-123", request.sessionId()); + } + + // ===== SetForegroundSessionResponse record ===== + + @Test + void setForegroundSessionResponseRecord() { + var successResponse = new SetForegroundSessionResponse(true, null); + assertTrue(successResponse.success()); + assertNull(successResponse.error()); + + var errorResponse = new SetForegroundSessionResponse(false, "Session not found"); + assertFalse(errorResponse.success()); + assertEquals("Session not found", errorResponse.error()); + } + + // ===== ToolResultObject factory methods ===== + + @Test + void toolResultObjectErrorWithTextAndError() { + var result = ToolResultObject.error("partial output", "File not found"); + assertEquals("error", result.resultType()); + assertEquals("partial output", result.textResultForLlm()); + assertEquals("File not found", result.error()); + } + + @Test + void toolResultObjectFailure() { + var result = ToolResultObject.failure("Tool unavailable", "Unknown tool"); + assertEquals("failure", result.resultType()); + assertEquals("Tool unavailable", result.textResultForLlm()); + assertEquals("Unknown tool", result.error()); + } + + // ===== PermissionRequest additional setters ===== + + @Test + void permissionRequestSetExtensionData() { + var req = new PermissionRequest(); + req.setExtensionData(java.util.Map.of("key", "value")); + assertEquals("value", req.getExtensionData().get("key")); + } + + // ===== SectionOverride setContent ===== + + @Test + void sectionOverrideSetContent() { + var override = new SectionOverride(); + override.setContent("Custom content"); + assertEquals("Custom content", override.getContent()); + } + + // ===== PreToolUseHookInput getters ===== + + @Test + void preToolUseHookInputGetters() { + var input = new PreToolUseHookInput(); + // Default values + assertEquals(0L, input.getTimestamp()); + assertNull(input.getCwd()); + assertNull(input.getToolArgs()); + assertNull(input.getSessionId()); + } + + @Test + void preToolUseHookInputSessionIdRoundTrip() { + var input = new PreToolUseHookInput(); + input.setSessionId("session-abc"); + assertEquals("session-abc", input.getSessionId()); + } + + // ===== PostToolUseHookInput getters ===== + + @Test + void postToolUseHookInputGetters() { + var input = new PostToolUseHookInput(); + // Default values + assertEquals(0L, input.getTimestamp()); + assertNull(input.getCwd()); + assertNull(input.getToolArgs()); + assertNull(input.getSessionId()); + } + + @Test + void postToolUseHookInputSessionIdRoundTrip() { + var input = new PostToolUseHookInput(); + input.setSessionId("session-xyz"); + assertEquals("session-xyz", input.getSessionId()); + } + + // ===== CustomAgentConfig model field ===== + + @Test + void customAgentConfigModelGetterAndSetter() { + var cfg = new CustomAgentConfig(); + assertNull(cfg.getModel()); + + cfg.setModel("claude-haiku-4.5"); + assertEquals("claude-haiku-4.5", cfg.getModel()); + } + + @Test + void customAgentConfigModelFluentChaining() { + var cfg = new CustomAgentConfig().setName("reviewer").setModel("gpt-5").setDescription("Code reviewer"); + assertEquals("reviewer", cfg.getName()); + assertEquals("gpt-5", cfg.getModel()); + assertEquals("Code reviewer", cfg.getDescription()); + } + + @Test + void customAgentConfigModelSerializationRoundTrip() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var cfg = new CustomAgentConfig().setName("my-agent").setModel("claude-haiku-4.5"); + + var json = mapper.writeValueAsString(cfg); + assertTrue(json.contains("\"model\":\"claude-haiku-4.5\"")); + + var deserialized = mapper.readValue(json, CustomAgentConfig.class); + assertEquals("my-agent", deserialized.getName()); + assertEquals("claude-haiku-4.5", deserialized.getModel()); + } + + @Test + void customAgentConfigModelOmittedWhenNull() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var cfg = new CustomAgentConfig().setName("no-model-agent"); + + var json = mapper.writeValueAsString(cfg); + assertFalse(json.contains("\"model\"")); + } + + // ===== PermissionRequestResult setRules ===== + + @Test + void permissionRequestResultSetRules() { + var result = new PermissionRequestResult().setKind("allow"); + var rules = new java.util.ArrayList(); + rules.add("bash:read"); + rules.add("bash:write"); + result.setRules(rules); + assertEquals(2, result.getRules().size()); + assertEquals("bash:read", result.getRules().get(0)); + } + + @Test + void mcpHttpServerConfigCoversGettersAndFluentSetters() { + var headers = java.util.Map.of("Authorization", "Bearer token"); + var tools = java.util.List.of("*", "search"); + + var cfg = new McpHttpServerConfig().setUrl("https://mcp.example.com/sse").setHeaders(headers).setTools(tools) + .setTimeout(45); + + assertEquals("http", cfg.getType()); + assertEquals("https://mcp.example.com/sse", cfg.getUrl()); + assertEquals("Bearer token", cfg.getHeaders().get("Authorization")); + assertEquals(tools, cfg.getTools()); + assertEquals(45, cfg.getTimeout()); + } + + @Test + void mcpStdioServerConfigCoversGettersAndFluentSetters() { + var args = java.util.List.of("-y", "@modelcontextprotocol/server-filesystem"); + var env = java.util.Map.of("DEBUG", "1"); + var tools = java.util.List.of("*"); + + var cfg = new McpStdioServerConfig().setCommand("npx").setArgs(args).setEnv(env).setWorkingDirectory("/tmp") + .setTools(tools).setTimeout(30); + + assertEquals("stdio", cfg.getType()); + assertEquals("npx", cfg.getCommand()); + assertEquals(args, cfg.getArgs()); + assertEquals("1", cfg.getEnv().get("DEBUG")); + assertEquals("/tmp", cfg.getWorkingDirectory()); + assertEquals(tools, cfg.getTools()); + assertEquals(30, cfg.getTimeout()); + } + + @Test + void modelCapabilitiesOverrideCoversNestedSupportsAndLimits() { + var supports = new ModelCapabilitiesOverride.Supports().setVision(true).setReasoningEffort(false); + var limits = new ModelCapabilitiesOverride.Limits().setMaxPromptTokens(2048).setMaxOutputTokens(512) + .setMaxContextWindowTokens(8192); + + var override = new ModelCapabilitiesOverride().setSupports(supports).setLimits(limits); + + assertTrue(override.getSupports().getVision().orElse(false)); + assertFalse(override.getSupports().getReasoningEffort().orElse(true)); + assertEquals(2048, override.getLimits().getMaxPromptTokens().getAsInt()); + assertEquals(512, override.getLimits().getMaxOutputTokens().getAsInt()); + assertEquals(8192, override.getLimits().getMaxContextWindowTokens().getAsInt()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java b/java/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java new file mode 100644 index 000000000..bb8a6f07e --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/DocumentationSamplesTest.java @@ -0,0 +1,145 @@ +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +class DocumentationSamplesTest { + + @Test + void docsAndJbangSamplesUseRequiredPermissionHandler() throws IOException { + for (Path path : documentationFiles()) { + String content = stripStringsAndComments(Files.readString(path)); + assertFalse(hasConfigWithoutPermissionHandler(content, "SessionConfig"), + () -> path + " contains SessionConfig sample without setOnPermissionRequest"); + assertFalse(hasConfigWithoutPermissionHandler(content, "ResumeSessionConfig"), + () -> path + " contains ResumeSessionConfig sample without setOnPermissionRequest"); + assertFalse(hasSingleArgumentResumeSessionCall(content), + () -> path + " contains removed resumeSession(String) overload"); + } + } + + private static boolean hasConfigWithoutPermissionHandler(String content, String configType) { + String constructor = "new " + configType + "()"; + int fromIndex = 0; + while (true) { + int start = content.indexOf(constructor, fromIndex); + if (start < 0) { + return false; + } + int end = content.indexOf(';', start); + if (end < 0) { + end = content.length(); + } + if (!content.substring(start, end).contains("setOnPermissionRequest(")) { + return true; + } + fromIndex = start + constructor.length(); + } + } + + private static boolean hasSingleArgumentResumeSessionCall(String content) { + int fromIndex = 0; + while (true) { + int callStart = content.indexOf("resumeSession(", fromIndex); + if (callStart < 0) { + return false; + } + int index = callStart + "resumeSession(".length(); + int depth = 1; + int topLevelCommaCount = 0; + while (index < content.length() && depth > 0) { + char c = content.charAt(index); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } else if (c == ',' && depth == 1) { + topLevelCommaCount++; + } + index++; + } + + if (depth == 0 && topLevelCommaCount == 0) { + return true; + } + fromIndex = callStart + 1; + } + } + + private static String stripStringsAndComments(String input) { + StringBuilder out = new StringBuilder(input.length()); + int i = 0; + while (i < input.length()) { + char c = input.charAt(i); + if (c == '"' || c == '\'') { + char quote = c; + out.append(' '); + i++; + while (i < input.length()) { + char current = input.charAt(i); + out.append(' '); + if (current == '\\') { + i++; + if (i < input.length()) { + out.append(' '); + } + } else if (current == quote) { + i++; + break; + } + i++; + } + continue; + } + if (c == '/' && i + 1 < input.length()) { + char next = input.charAt(i + 1); + if (next == '/') { + out.append(' ').append(' '); + i += 2; + while (i < input.length() && input.charAt(i) != '\n') { + out.append(' '); + i++; + } + continue; + } + if (next == '*') { + out.append(' ').append(' '); + i += 2; + while (i + 1 < input.length() && !(input.charAt(i) == '*' && input.charAt(i + 1) == '/')) { + out.append(' '); + i++; + } + if (i + 1 < input.length()) { + out.append(' ').append(' '); + i += 2; + } + continue; + } + } + out.append(c); + i++; + } + return out.toString(); + } + + private static List documentationFiles() throws IOException { + Path root = Path.of("").toAbsolutePath(); + List files = new ArrayList<>(); + files.add(root.resolve("README.md")); + files.add(root.resolve("jbang-example.java")); + + try (Stream markdownFiles = Files.walk(root.resolve("src/site/markdown"))) { + markdownFiles.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".md")) + .forEach(files::add); + } + return files; + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java new file mode 100644 index 000000000..9680148ff --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/E2ETestContext.java @@ -0,0 +1,485 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.github.copilot.sdk.json.CopilotClientOptions; + +/** + * E2E test context that manages the test environment including the CapiProxy, + * working directories, and CLI path. + * + *

+ * This provides a complete test environment similar to the Node.js, .NET, Go, + * and Python SDK test harnesses. It manages: + *

+ *
    + *
  • A replaying CapiProxy for deterministic API responses
  • + *
  • Temporary home and work directories for test isolation
  • + *
  • Environment variables for the Copilot CLI
  • + *
+ * + *

+ * Usage example: + *

+ * + *
+ * {@code
+ * try (E2ETestContext ctx = E2ETestContext.create()) {
+ * 	ctx.configureForTest("tools", "my_test_name");
+ *
+ * 	try (CopilotClient client = ctx.createClient()) {
+ * 		CopilotSession session = client
+ * 				.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+ * 		// ... run test ...
+ * 	}
+ * }
+ * }
+ * 
+ */ +public class E2ETestContext implements AutoCloseable { + + private static final Logger LOG = Logger.getLogger(E2ETestContext.class.getName()); + private static final Pattern SNAKE_CASE = Pattern.compile("[^a-zA-Z0-9]"); + private static final Pattern USER_CONTENT_PATTERN = Pattern + .compile("^\\s+-\\s+role:\\s+user\\s*$\\s+content:\\s*(.+?)$", Pattern.MULTILINE); + + private final String cliPath; + private final Path homeDir; + private final Path workDir; + private String proxyUrl; + private final CapiProxy proxy; + private final Path repoRoot; + private Path currentSnapshotFile; + + private E2ETestContext(String cliPath, Path homeDir, Path workDir, String proxyUrl, CapiProxy proxy, + Path repoRoot) { + this.cliPath = cliPath; + this.homeDir = homeDir; + this.workDir = workDir; + this.proxyUrl = proxyUrl; + this.proxy = proxy; + this.repoRoot = repoRoot; + } + + /** + * Creates a new E2E test context. + * + * @return the test context + * @throws IOException + * if setup fails + * @throws InterruptedException + * if setup is interrupted + */ + public static E2ETestContext create() throws IOException, InterruptedException { + Path repoRoot = findRepoRoot(); + String cliPath = getCliPath(repoRoot); + + Path tempDir = Paths.get(System.getProperty("java.io.tmpdir")); + Path homeDir = Files.createTempDirectory(tempDir, "copilot-test-config-"); + Path workDir = Files.createTempDirectory(tempDir, "copilot-test-work-"); + + CapiProxy proxy = new CapiProxy(); + String proxyUrl = proxy.start(); + + return new E2ETestContext(cliPath, homeDir, workDir, proxyUrl, proxy, repoRoot); + } + + /** + * Gets the Copilot CLI path. + */ + public String getCliPath() { + return cliPath; + } + + /** + * Gets the temporary home directory for test isolation. + */ + public Path getHomeDir() { + return homeDir; + } + + /** + * Gets the temporary working directory for tests. + */ + public Path getWorkDir() { + return workDir; + } + + /** + * Gets the proxy URL. + */ + public String getProxyUrl() { + return proxyUrl; + } + + /** + * Configures the proxy for a specific test. + * + * @param testFile + * the test category folder (e.g., "tools", "session", "permissions") + * @param testName + * the test method name (will be converted to snake_case) + * @throws IOException + * if configuration fails + * @throws InterruptedException + * if configuration is interrupted + */ + public void configureForTest(String testFile, String testName) throws IOException, InterruptedException { + // Restart the proxy if it has crashed + ensureProxyAlive(); + + // Convert test method names to lowercase snake_case for snapshot filenames + // to avoid case collisions on case-insensitive filesystems (macOS/Windows) + String sanitizedName = SNAKE_CASE.matcher(testName).replaceAll("_").toLowerCase(); + Path snapshotFile = repoRoot.resolve("test").resolve("snapshots").resolve(testFile) + .resolve(sanitizedName + ".yaml"); + + // Validate snapshot exists - fail fast with a clear message + if (!Files.exists(snapshotFile)) { + Path snapshotsDir = repoRoot.resolve("test").resolve("snapshots").resolve(testFile); + String availableSnapshots = ""; + if (Files.exists(snapshotsDir)) { + try (var files = Files.list(snapshotsDir)) { + availableSnapshots = files.filter(p -> p.toString().endsWith(".yaml")) + .map(p -> p.getFileName().toString().replace(".yaml", "")).sorted() + .reduce((a, b) -> a + ", " + b).orElse(""); + } + } + throw new IOException(String.format( + "Snapshot file not found: %s%n" + "Category: %s, Test: %s (sanitized: %s)%n" + + "Available snapshots in '%s/': %s%n" + + "Ensure the snapshot exists and the test name matches exactly.", + snapshotFile, testFile, testName, sanitizedName, testFile, availableSnapshots)); + } + + this.currentSnapshotFile = snapshotFile; + proxy.configure(snapshotFile.toString(), workDir.toString()); + + // Log expected prompts to help debug prompt mismatch issues + List expectedPrompts = getExpectedUserPrompts(); + if (!expectedPrompts.isEmpty()) { + LOG.info(() -> String.format("Configured snapshot '%s/%s' expects prompts: %s", testFile, sanitizedName, + expectedPrompts)); + } + } + + /** + * Gets the expected user prompts from the current snapshot file. + *

+ * This is useful for debugging when tests fail with "No cached response found" + * errors from CapiProxy. The prompts in your test must match these exactly. + *

+ * + * @return list of expected user prompt strings, or empty list if none found + */ + public List getExpectedUserPrompts() { + if (currentSnapshotFile == null || !Files.exists(currentSnapshotFile)) { + return List.of(); + } + try { + String content = Files.readString(currentSnapshotFile); + List prompts = new ArrayList<>(); + Matcher matcher = USER_CONTENT_PATTERN.matcher(content); + while (matcher.find()) { + String prompt = matcher.group(1).trim(); + // Remove quotes if present + if ((prompt.startsWith("\"") && prompt.endsWith("\"")) + || (prompt.startsWith("'") && prompt.endsWith("'"))) { + prompt = prompt.substring(1, prompt.length() - 1); + } + if (!prompts.contains(prompt)) { + prompts.add(prompt); + } + } + return prompts; + } catch (IOException e) { + LOG.warning("Failed to read snapshot file: " + e.getMessage()); + return List.of(); + } + } + + /** + * Ensures the proxy is alive, restarting it if necessary. + * + * @throws IOException + * if the proxy cannot be restarted + * @throws InterruptedException + * if interrupted during restart + */ + public void ensureProxyAlive() throws IOException, InterruptedException { + if (!proxy.isAlive()) { + proxyUrl = proxy.restart(); + } + } + + /** + * Gets the captured HTTP exchanges from the proxy. + * + * @return list of exchange maps + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public List> getExchanges() throws IOException, InterruptedException { + return proxy.getExchanges(); + } + + /** + * Gets the environment variables needed for the Copilot CLI. + * + * @return map of environment variables + */ + public Map getEnvironment() { + Map env = new HashMap<>(System.getenv()); + env.put("COPILOT_API_URL", proxyUrl); + env.put("COPILOT_HOME", homeDir.toString()); + env.put("GH_CONFIG_DIR", homeDir.toString()); + env.put("XDG_CONFIG_HOME", homeDir.toString()); + env.put("XDG_STATE_HOME", homeDir.toString()); + + // Configure CONNECT proxy for HTTPS interception if available + String connectUrl = proxy.getConnectProxyUrl(); + String caFile = proxy.getCaFilePath(); + if (connectUrl != null && !connectUrl.isEmpty() && caFile != null && !caFile.isEmpty()) { + String noProxy = "127.0.0.1,localhost,::1"; + env.put("HTTP_PROXY", connectUrl); + env.put("HTTPS_PROXY", connectUrl); + env.put("http_proxy", connectUrl); + env.put("https_proxy", connectUrl); + env.put("NO_PROXY", noProxy); + env.put("no_proxy", noProxy); + env.put("NODE_EXTRA_CA_CERTS", caFile); + env.put("SSL_CERT_FILE", caFile); + env.put("REQUESTS_CA_BUNDLE", caFile); + env.put("CURL_CA_BUNDLE", caFile); + env.put("GIT_SSL_CAINFO", caFile); + env.put("GH_TOKEN", "fake-token-for-e2e-tests"); + env.put("GITHUB_TOKEN", "fake-token-for-e2e-tests"); + env.put("GH_ENTERPRISE_TOKEN", ""); + env.put("GITHUB_ENTERPRISE_TOKEN", ""); + } + + return env; + } + + /** + * Creates a CopilotClient configured for this test context. + * + * @return a new CopilotClient + */ + public CopilotClient createClient() { + CopilotClientOptions options = new CopilotClientOptions().setCliPath(cliPath).setCwd(workDir.toString()) + .setEnvironment(getEnvironment()).setGitHubToken("fake-token-for-e2e-tests"); + + return new CopilotClient(options); + } + + /** + * Creates a CopilotClient with the given options, applied on top of the default + * options for this test context. + * + * @param options + * options to apply; environment and cliPath will be set from the + * context if not already set + * @return a new CopilotClient + */ + public CopilotClient createClient(CopilotClientOptions options) { + if (options.getCliPath() == null) { + options.setCliPath(cliPath); + } + if (options.getCwd() == null) { + options.setCwd(workDir.toString()); + } + if (options.getEnvironment() == null || options.getEnvironment().isEmpty()) { + options.setEnvironment(getEnvironment()); + } + if (options.getGitHubToken() == null) { + options.setGitHubToken("fake-token-for-e2e-tests"); + } + + return new CopilotClient(options); + } + + /** + * Configures the proxy to return a specific Copilot user response for a given + * token. Used for per-session authentication tests. + * + * @param token + * the GitHub token + * @param login + * the user login + * @param copilotPlan + * the Copilot plan + * @param apiUrl + * the API URL for the user endpoints + * @param telemetryUrl + * the telemetry URL + * @param analyticsTrackingId + * the analytics tracking ID + * @throws IOException + * if the request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void setCopilotUserByToken(String token, String login, String copilotPlan, String apiUrl, + String telemetryUrl, String analyticsTrackingId) throws IOException, InterruptedException { + ensureProxyAlive(); + proxy.setCopilotUserByToken(token, login, copilotPlan, apiUrl, telemetryUrl, analyticsTrackingId); + } + + /** + * Initializes the proxy state without loading a snapshot. + *

+ * Use this for tests that need the proxy to be active (e.g., for per-session + * auth token resolution via {@code /copilot_internal/user}) but do not make AI + * completion requests and therefore have no snapshot to load. + *

+ *

+ * The proxy requires its internal {@code state} to be initialized before it can + * handle most endpoints. Without this call the proxy throws an error and + * returns HTTP 500 for any request that arrives before a {@code /config} POST + * has been made. + *

+ * + * @throws IOException + * if the proxy configuration request fails + * @throws InterruptedException + * if the request is interrupted + */ + public void initializeProxy() throws IOException, InterruptedException { + ensureProxyAlive(); + // Pass a non-existent snapshot path. The proxy initializes its state even when + // the file is absent (storedData simply remains undefined), which is fine for + // tests that never make AI chat-completion requests. + proxy.configure(workDir.resolve("no-snapshot.yaml").toString(), workDir.toString()); + } + + @Override + public void close() throws Exception { + proxy.stop(); + + // Clean up temp directories (best effort) + deleteRecursively(homeDir); + deleteRecursively(workDir); + } + + private static Path findRepoRoot() throws IOException { + // First, check for copilot.sdk.dir system property (set by Maven during tests) + String sdkDir = System.getProperty("copilot.sdk.dir"); + if (sdkDir != null && !sdkDir.isEmpty()) { + Path sdkPath = Paths.get(sdkDir); + if (Files.exists(sdkPath)) { + return sdkPath; + } + } + + // Fallback: search up from current directory + Path dir = Paths.get(System.getProperty("user.dir")); + while (dir != null) { + if (Files.exists(dir.resolve("nodejs")) && Files.exists(dir.resolve("test").resolve("harness"))) { + return dir; + } + dir = dir.getParent(); + } + throw new IOException("Could not find repository root. Either set copilot.sdk.dir system property " + + "or run from within the copilot-sdk repository."); + } + + private static String getCliPath(Path repoRoot) throws IOException { + // Try environment variable first (explicit override) + String envPath = System.getenv("COPILOT_CLI_PATH"); + if (envPath != null && !envPath.isEmpty()) { + return envPath; + } + + // Try test harness platform-specific binary (preferred as it has correct + // version) + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + String platform = os.contains("mac") ? "darwin" : os.contains("win") ? "win32" : "linux"; + String cpuArch = arch.contains("aarch64") || arch.contains("arm64") ? "arm64" : "x64"; + Path platformBinary = repoRoot + .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot"); + if (os.contains("win")) { + platformBinary = repoRoot + .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot.exe"); + } + if (Files.exists(platformBinary)) { + return platformBinary.toString(); + } + + // Try test harness npm-loader.js + Path harnessCliPath = repoRoot.resolve("test/harness/node_modules/@github/copilot/npm-loader.js"); + if (Files.exists(harnessCliPath)) { + return harnessCliPath.toString(); + } + + // Try nodejs installation + Path cliPath = repoRoot.resolve("nodejs/node_modules/@github/copilot/index.js"); + if (Files.exists(cliPath)) { + return cliPath.toString(); + } + + // Fallback: try to find 'copilot' in PATH + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + + throw new IOException("CLI not found. Either install 'copilot' globally, set COPILOT_CLI_PATH, " + + "or run 'npm install' in the nodejs directory or test/harness directory."); + } + + private static String findCopilotInPath() { + try { + String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which"; + ProcessBuilder pb = new ProcessBuilder(command, "copilot"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + int exitCode = process.waitFor(); + if (exitCode == 0 && line != null && !line.isEmpty()) { + return line.trim(); + } + } + } catch (Exception e) { + // Ignore - copilot not found in PATH + } + return null; + } + + private static void deleteRecursively(Path path) { + try { + if (Files.exists(path)) { + Files.walk(path).sorted((a, b) -> b.compareTo(a)) // Reverse order to delete children first + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + // Best effort + } + }); + } + } catch (IOException e) { + // Best effort + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ElicitationTest.java b/java/src/test/java/com/github/copilot/sdk/ElicitationTest.java new file mode 100644 index 000000000..1f6245127 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ElicitationTest.java @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.ElicitationContext; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationParams; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ElicitationSchema; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionCapabilities; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; + +/** + * Unit tests for the Elicitation feature and Session Capabilities. + * + *

+ * Ported from {@code ElicitationTests.cs} in the reference implementation + * dotnet SDK. + *

+ */ +class ElicitationTest { + + @Test + void sessionCapabilitiesTypesAreProperlyStructured() { + var capabilities = new SessionCapabilities().setUi(new SessionUiCapabilities().setElicitation(true)); + + assertNotNull(capabilities.getUi()); + assertTrue(capabilities.getUi().getElicitation().get()); + + // Test with null UI + var emptyCapabilities = new SessionCapabilities(); + assertNull(emptyCapabilities.getUi()); + } + + @Test + void defaultCapabilitiesAreEmpty() { + var capabilities = new SessionCapabilities(); + + assertNull(capabilities.getUi()); + } + + @Test + void elicitationResultActionValues() { + assertEquals("accept", ElicitationResultAction.ACCEPT.getValue()); + assertEquals("decline", ElicitationResultAction.DECLINE.getValue()); + assertEquals("cancel", ElicitationResultAction.CANCEL.getValue()); + } + + @Test + void elicitationResultHasActionAndContent() { + var content = Map.of("name", (Object) "Alice"); + var result = new ElicitationResult().setAction(ElicitationResultAction.ACCEPT).setContent(content); + + assertEquals(ElicitationResultAction.ACCEPT, result.getAction()); + assertEquals(content, result.getContent()); + } + + @Test + void elicitationSchemaHasTypeAndProperties() { + var properties = Map.of("name", (Object) Map.of("type", "string")); + var schema = new ElicitationSchema().setType("object").setProperties(properties).setRequired(List.of("name")); + + assertEquals("object", schema.getType()); + assertEquals(properties, schema.getProperties()); + assertEquals(List.of("name"), schema.getRequired()); + } + + @Test + void elicitationSchemaDefaultTypeIsObject() { + var schema = new ElicitationSchema(); + + assertEquals("object", schema.getType()); + } + + @Test + void elicitationContextHasAllProperties() { + var properties = Map.of("field", (Object) Map.of("type", "string")); + var schema = new ElicitationSchema().setProperties(properties); + + var ctx = new ElicitationContext().setSessionId("session-1").setMessage("Please enter your name") + .setRequestedSchema(schema).setMode("form").setElicitationSource("mcp-server").setUrl(null); + + assertEquals("session-1", ctx.getSessionId()); + assertEquals("Please enter your name", ctx.getMessage()); + assertEquals(schema, ctx.getRequestedSchema()); + assertEquals("form", ctx.getMode()); + assertEquals("mcp-server", ctx.getElicitationSource()); + assertNull(ctx.getUrl()); + } + + @Test + void elicitationParamsHasMessageAndSchema() { + var schema = new ElicitationSchema().setProperties(Map.of("field", (Object) Map.of("type", "string"))); + var params = new ElicitationParams().setMessage("Enter name").setRequestedSchema(schema); + + assertEquals("Enter name", params.getMessage()); + assertEquals(schema, params.getRequestedSchema()); + } + + @Test + void inputOptionsHasAllFields() { + var opts = new InputOptions().setTitle("My Title").setDescription("My Desc").setMinLength(1).setMaxLength(100) + .setFormat("email").setDefaultValue("default@example.com"); + + assertEquals("My Title", opts.getTitle()); + assertEquals("My Desc", opts.getDescription()); + assertEquals(1, opts.getMinLength().getAsInt()); + assertEquals(100, opts.getMaxLength().getAsInt()); + assertEquals("email", opts.getFormat()); + assertEquals("default@example.com", opts.getDefaultValue()); + } + + @Test + void sessionConfigOnElicitationRequestIsCloned() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var clone = config.clone(); + + // Handler reference is shared (not deep-cloned), but the field is copied + assertNotNull(clone.getOnElicitationRequest()); + assertSame(handler, clone.getOnElicitationRequest()); + } + + @Test + void resumeConfigOnElicitationRequestIsCloned() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var clone = config.clone(); + + assertNotNull(clone.getOnElicitationRequest()); + assertSame(handler, clone.getOnElicitationRequest()); + } + + @Test + void buildCreateRequestSetsRequestElicitationWhenHandlerPresent() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(Boolean.TRUE, request.getRequestElicitation()); + } + + @Test + void buildCreateRequestDoesNotSetRequestElicitationWhenNoHandler() { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + var request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getRequestElicitation()); + } + + @Test + void buildResumeRequestSetsRequestElicitationWhenHandlerPresent() { + ElicitationHandler handler = ctx -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.ACCEPT)); + + var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnElicitationRequest(handler); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertEquals(Boolean.TRUE, request.getRequestElicitation()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java b/java/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java new file mode 100644 index 000000000..8c606930a --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ErrorHandlingTest.java @@ -0,0 +1,239 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.logging.Logger; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionErrorEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; + +import java.util.Map; + +/** + * E2E tests for error handling scenarios. + *

+ * These tests verify that the SDK properly handles errors in various scenarios + * including tool errors, permission handler errors, and session errors. + *

+ */ +public class ErrorHandlingTest { + + private static final Logger LOG = Logger.getLogger(ErrorHandlingTest.class.getName()); + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that tool errors are handled gracefully and don't crash the session. + * + * @see Snapshot: tools/handles_tool_calling_errors + */ + @Test + void testHandlesToolCallingErrors_toolErrorDoesNotCrashSession() throws Exception { + LOG.info("Running test: testHandlesToolCallingErrors_toolErrorDoesNotCrashSession"); + ctx.configureForTest("tools", "handles_tool_calling_errors"); + + var allEvents = new ArrayList(); + + ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", + Map.of("type", "object", "properties", Map.of()), (invocation) -> { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RuntimeException("Location service unavailable")); + return future; + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(event -> allEvents.add(event)); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(60, TimeUnit.SECONDS); + + // Session should complete without crashing + assertNotNull(response, "Should receive a response even when tool fails"); + + // Should have received session.idle (indicating successful completion) + assertTrue(allEvents.stream().anyMatch(e -> e instanceof com.github.copilot.sdk.generated.SessionIdleEvent), + "Session should reach idle state after handling tool error"); + + session.close(); + } + } + + /** + * Verifies that returning a failure result from a tool is handled properly. + * + * @see Snapshot: tools/handles_tool_calling_errors + */ + @Test + void testHandlesToolCallingErrors_toolReturnsFailureResult() throws Exception { + LOG.info("Running test: testHandlesToolCallingErrors_toolReturnsFailureResult"); + ctx.configureForTest("tools", "handles_tool_calling_errors"); + + ToolDefinition failTool = ToolDefinition.create("get_user_location", "Gets the user's location", + Map.of("type", "object", "properties", Map.of()), (invocation) -> { + // Return a structured failure result via exception (matching the snapshot + // behavior) + return CompletableFuture.failedFuture(new RuntimeException("Location unavailable")); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(failTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response, "Should receive a response with failure result"); + + session.close(); + } + } + + /** + * Verifies that permission handler errors result in denied permission. + * + * @see Snapshot: permissions/should_handle_permission_handler_errors_gracefully + */ + @Test + void testShouldHandlePermissionHandlerErrorsGracefully_deniesPermission() throws Exception { + LOG.info("Running test: testShouldHandlePermissionHandlerErrorsGracefully_deniesPermission"); + ctx.configureForTest("permissions", "should_handle_permission_handler_errors_gracefully"); + + var errorEvents = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + throw new RuntimeException("Permission handler crashed"); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.on(SessionErrorEvent.class, errorEvents::add); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) + .get(60, TimeUnit.SECONDS); + + // Should complete despite the error + assertNotNull(response, "Should receive a response despite handler error"); + + // The response should indicate failure/inability + String content = response.getData().content().toLowerCase(); + assertTrue( + content.contains("fail") || content.contains("cannot") || content.contains("unable") + || content.contains("permission") || content.contains("denied"), + "Response should indicate permission was denied: " + content); + + session.close(); + } + } + + /** + * Verifies that session error events contain proper error information. + * + * @see Snapshot: permissions/permission_handler_errors + */ + @Test + void testPermissionHandlerErrors_sessionErrorEventContainsDetails() throws Exception { + LOG.info("Running test: testPermissionHandlerErrors_sessionErrorEventContainsDetails"); + ctx.configureForTest("permissions", "permission_handler_errors"); + + var errorEvents = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + throw new RuntimeException("Test error message"); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.on(SessionErrorEvent.class, error -> { + errorEvents.add(error); + // Verify error event has data + assertNotNull(error.getData(), "Error event should have data"); + }); + + try { + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) + .get(60, TimeUnit.SECONDS); + } catch (Exception e) { + // Error is expected in some cases + } + + session.close(); + } + + // Note: Whether error events are emitted depends on the CLI version and + // scenario + // This test verifies the handler can receive them when they occur + } + + /** + * Verifies that the session continues to work after a tool error. + * + * @see Snapshot: tools/handles_tool_calling_errors + */ + @Test + void testHandlesToolCallingErrors_sessionContinuesAfterToolError() throws Exception { + LOG.info("Running test: testHandlesToolCallingErrors_sessionContinuesAfterToolError"); + ctx.configureForTest("tools", "handles_tool_calling_errors"); + + ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", + Map.of("type", "object", "properties", Map.of()), (invocation) -> { + return CompletableFuture.failedFuture(new RuntimeException("Service unavailable")); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + // First request that will cause tool error + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response, "Should receive first response"); + + // Session should still be usable - the sendAndWait completed + // This verifies the session didn't enter an error state + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/EventFidelityTest.java b/java/src/test/java/com/github/copilot/sdk/EventFidelityTest.java new file mode 100644 index 000000000..60b8e1327 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/EventFidelityTest.java @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantUsageEvent; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.SessionUsageInfoEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for event fidelity β€” verifying the shape, ordering, and presence of + * key events emitted from the runtime. + * + *

+ * Snapshots are stored in {@code test/snapshots/event_fidelity/}. + *

+ */ +public class EventFidelityTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that an {@code assistant.usage} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_assistant_usage_event_after_model_call + */ + @Test + void testShouldEmitAssistantUsageEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_assistant_usage_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageEvents = events.stream().filter(e -> e instanceof AssistantUsageEvent) + .map(e -> (AssistantUsageEvent) e).toList(); + + assertFalse(usageEvents.isEmpty(), "Should have received an assistant.usage event after model call"); + + AssistantUsageEvent lastUsage = usageEvents.get(usageEvents.size() - 1); + assertNotNull(lastUsage.getData().model(), "Usage event should have a model field"); + assertFalse(lastUsage.getData().model().isEmpty(), "Model field should not be empty"); + + session.close(); + } + } + + /** + * Verifies that a {@code session.usage_info} event is emitted after the model + * processes a prompt. + * + * @see Snapshot: + * event_fidelity/should_emit_session_usage_info_event_after_model_call + */ + @Test + void testShouldEmitSessionUsageInfoEventAfterModelCall() throws Exception { + ctx.configureForTest("event_fidelity", "should_emit_session_usage_info_event_after_model_call"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 5+5? Reply with just the number.")).get(60, + TimeUnit.SECONDS); + + List usageInfoEvents = events.stream() + .filter(e -> e instanceof SessionUsageInfoEvent).map(e -> (SessionUsageInfoEvent) e).toList(); + + assertFalse(usageInfoEvents.isEmpty(), "Should have received a session.usage_info event after model call"); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java b/java/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java new file mode 100644 index 000000000..a5eb3a62d --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ExecutorWiringTest.java @@ -0,0 +1,359 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Tests verifying that when an {@link Executor} is provided via + * {@link CopilotClientOptions#setExecutor(Executor)}, all internal + * {@code CompletableFuture.*Async} calls are routed through that executor + * instead of {@code ForkJoinPool.commonPool()}. + * + *

+ * Uses a {@link TrackingExecutor} decorator that delegates to a real executor + * while counting task submissions. After SDK operations complete, the tests + * assert the decorator was invoked. + *

+ */ +public class ExecutorWiringTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * A decorator executor that delegates to a real executor while counting task + * submissions. + */ + static class TrackingExecutor implements Executor { + + private final Executor delegate; + private final AtomicInteger taskCount = new AtomicInteger(0); + + TrackingExecutor(Executor delegate) { + this.delegate = delegate; + } + + @Override + public void execute(Runnable command) { + taskCount.incrementAndGet(); + delegate.execute(command); + } + + int getTaskCount() { + return taskCount.get(); + } + } + + private CopilotClientOptions createOptionsWithExecutor(TrackingExecutor executor) { + CopilotClientOptions options = new CopilotClientOptions().setCliPath(ctx.getCliPath()) + .setCwd(ctx.getWorkDir().toString()).setEnvironment(ctx.getEnvironment()).setExecutor(executor) + .setGitHubToken("fake-token-for-e2e-tests"); + return options; + } + + /** + * Verifies that client start-up routes through the provided executor. + * + *

+ * {@code CopilotClient.startCore()} uses + * {@code CompletableFuture.supplyAsync(...)} to initialize the connection. This + * test asserts that the start-up task goes through the caller-supplied + * executor, not {@code ForkJoinPool.commonPool()}. + *

+ * + * @see Snapshot: tools/invokes_custom_tool + */ + @Test + void testClientStartUsesProvidedExecutor() throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + int beforeStart = trackingExecutor.getTaskCount(); + + try (CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor))) { + client.start().get(30, TimeUnit.SECONDS); + + assertTrue(trackingExecutor.getTaskCount() > beforeStart, + "Expected the tracking executor to have been invoked during client start, " + + "but task count did not increase. CopilotClient.startCore() is not " + + "routing supplyAsync through the provided executor."); + } + } + + /** + * Verifies that tool call dispatch routes through the provided executor. + * + *

+ * When a custom tool is invoked by the LLM, the {@code RpcHandlerDispatcher} + * calls {@code CompletableFuture.runAsync(...)} to dispatch the tool handler. + * This test asserts that dispatch goes through the caller-supplied executor. + *

+ * + * @see Snapshot: tools/invokes_custom_tool + */ + @Test + void testToolCallDispatchUsesProvidedExecutor() throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + + var parameters = new HashMap(); + var properties = new HashMap(); + var inputProp = new HashMap(); + inputProp.put("type", "string"); + inputProp.put("description", "String to encrypt"); + properties.put("input", inputProp); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + // Reset count after client construction to isolate tool-call dispatch + try (CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor))) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + int beforeToolCall = trackingExecutor.getTaskCount(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + + assertTrue(trackingExecutor.getTaskCount() > beforeToolCall, + "Expected the tracking executor to have been invoked for tool call dispatch, " + + "but task count did not increase after sendAndWait. " + + "RpcHandlerDispatcher is not routing runAsync through the provided executor."); + + session.close(); + } + } + + /** + * Verifies that permission request dispatch routes through the provided + * executor. + * + *

+ * When the LLM requests a permission, the {@code RpcHandlerDispatcher} calls + * {@code CompletableFuture.runAsync(...)} to dispatch the permission handler. + * This test asserts that dispatch goes through the caller-supplied executor. + *

+ * + * @see Snapshot: permissions/permission_handler_for_write_operations + */ + @Test + void testPermissionDispatchUsesProvidedExecutor() throws Exception { + ctx.configureForTest("permissions", "permission_handler_for_write_operations"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED))); + + try (CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor))) { + CopilotSession session = client.createSession(config).get(); + + Path testFile = ctx.getWorkDir().resolve("test.txt"); + Files.writeString(testFile, "original content"); + + int beforeSend = trackingExecutor.getTaskCount(); + + session.sendAndWait(new MessageOptions().setPrompt("Edit test.txt and replace 'original' with 'modified'")) + .get(60, TimeUnit.SECONDS); + + assertTrue(trackingExecutor.getTaskCount() > beforeSend, + "Expected the tracking executor to have been invoked for permission dispatch, " + + "but task count did not increase after sendAndWait. " + + "RpcHandlerDispatcher is not routing permission runAsync through the provided executor."); + + session.close(); + } + } + + /** + * Verifies that user input request dispatch routes through the provided + * executor. + * + *

+ * When the LLM asks for user input, the {@code RpcHandlerDispatcher} calls + * {@code CompletableFuture.runAsync(...)} to dispatch the user input handler. + * This test asserts that dispatch goes through the caller-supplied executor. + *

+ * + * @see Snapshot: + * ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool + */ + @Test + void testUserInputDispatchUsesProvidedExecutor() throws Exception { + ctx.configureForTest("ask_user", "should_invoke_user_input_handler_when_model_uses_ask_user_tool"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnUserInputRequest((request, invocation) -> { + String answer = (request.getChoices() != null && !request.getChoices().isEmpty()) + ? request.getChoices().get(0) + : "freeform answer"; + boolean wasFreeform = request.getChoices() == null || request.getChoices().isEmpty(); + return CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(answer).setWasFreeform(wasFreeform)); + }); + + try (CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor))) { + CopilotSession session = client.createSession(config).get(); + + int beforeSend = trackingExecutor.getTaskCount(); + + session.sendAndWait(new MessageOptions().setPrompt( + "Ask me to choose between 'Option A' and 'Option B' using the ask_user tool. Wait for my response before continuing.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(trackingExecutor.getTaskCount() > beforeSend, + "Expected the tracking executor to have been invoked for user input dispatch, " + + "but task count did not increase after sendAndWait. " + + "RpcHandlerDispatcher is not routing userInput runAsync through the provided executor."); + + session.close(); + } + } + + /** + * Verifies that hooks dispatch routes through the provided executor. + * + *

+ * When the LLM triggers a hook, the {@code RpcHandlerDispatcher} calls + * {@code CompletableFuture.runAsync(...)} to dispatch the hooks handler. This + * test asserts that dispatch goes through the caller-supplied executor. + *

+ * + * @see Snapshot: hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool + */ + @Test + void testHooksDispatchUsesProvidedExecutor() throws Exception { + ctx.configureForTest("hooks", "invoke_pre_tool_use_hook_when_model_runs_a_tool"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse( + (input, invocation) -> CompletableFuture.completedFuture(PreToolUseHookOutput.allow()))); + + try (CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor))) { + CopilotSession session = client.createSession(config).get(); + + Path testFile = ctx.getWorkDir().resolve("hello.txt"); + Files.writeString(testFile, "Hello from the test!"); + + int beforeSend = trackingExecutor.getTaskCount(); + + session.sendAndWait( + new MessageOptions().setPrompt("Read the contents of hello.txt and tell me what it says")) + .get(60, TimeUnit.SECONDS); + + assertTrue(trackingExecutor.getTaskCount() > beforeSend, + "Expected the tracking executor to have been invoked for hooks dispatch, " + + "but task count did not increase after sendAndWait. " + + "RpcHandlerDispatcher is not routing hooks runAsync through the provided executor."); + + session.close(); + } + } + + /** + * Verifies that {@code CopilotClient.stop()} routes session closure through the + * provided executor. + * + *

+ * {@code CopilotClient.stop()} uses {@code CompletableFuture.runAsync(...)} to + * close each active session. This test asserts that those closures go through + * the caller-supplied executor. + *

+ * + * @see Snapshot: tools/invokes_custom_tool + */ + @Test + void testClientStopUsesProvidedExecutor() throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool"); + + TrackingExecutor trackingExecutor = new TrackingExecutor(ForkJoinPool.commonPool()); + + var parameters = new HashMap(); + var properties = new HashMap(); + var inputProp = new HashMap(); + inputProp.put("type", "string"); + inputProp.put("description", "String to encrypt"); + properties.put("input", inputProp); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + CopilotClient client = new CopilotClient(createOptionsWithExecutor(trackingExecutor)); + client.createSession(new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + int beforeStop = trackingExecutor.getTaskCount(); + + // stop() should use the provided executor for async session closure + client.stop().get(30, TimeUnit.SECONDS); + + assertTrue(trackingExecutor.getTaskCount() > beforeStop, + "Expected the tracking executor to have been invoked during client stop, " + + "but task count did not increase. CopilotClient.stop() is not " + + "routing session closure runAsync through the provided executor."); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ForwardCompatibilityTest.java b/java/src/test/java/com/github/copilot/sdk/ForwardCompatibilityTest.java new file mode 100644 index 000000000..06e2af5cf --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ForwardCompatibilityTest.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.UnknownSessionEvent; +import com.github.copilot.sdk.generated.UserMessageEvent; + +/** + * Unit tests for forward-compatible handling of unknown session event types. + *

+ * Verifies that the SDK gracefully handles event types introduced by newer CLI + * versions without crashing. + */ +public class ForwardCompatibilityTest { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @Test + void parse_knownEventType_returnsTypedEvent() throws Exception { + String json = """ + { + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-01-01T00:00:00Z", + "type": "user.message", + "data": { "content": "Hello" } + } + """; + SessionEvent result = MAPPER.readValue(json, SessionEvent.class); + + assertInstanceOf(UserMessageEvent.class, result); + assertEquals("user.message", result.getType()); + } + + @Test + void parse_unknownEventType_returnsUnknownSessionEvent() throws Exception { + String json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "type": "future.feature_from_server", + "data": { "key": "value" } + } + """; + SessionEvent result = MAPPER.readValue(json, SessionEvent.class); + + assertInstanceOf(UnknownSessionEvent.class, result); + assertEquals("future.feature_from_server", result.getType()); + } + + @Test + void parse_unknownEventType_preservesOriginalType() throws Exception { + String json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "type": "future.feature_from_server", + "data": {} + } + """; + SessionEvent result = MAPPER.readValue(json, SessionEvent.class); + + assertInstanceOf(UnknownSessionEvent.class, result); + assertEquals("future.feature_from_server", result.getType()); + } + + @Test + void parse_unknownEventType_preservesBaseMetadata() throws Exception { + String json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef", + "type": "future.feature_from_server", + "data": {} + } + """; + SessionEvent result = MAPPER.readValue(json, SessionEvent.class); + + assertNotNull(result); + assertEquals(UUID.fromString("12345678-1234-1234-1234-123456789abc"), result.getId()); + assertEquals(UUID.fromString("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.getParentId()); + } + + @Test + void unknownSessionEvent_getType_returnsUnknown() { + var evt = new UnknownSessionEvent(); + assertEquals("unknown", evt.getType()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/HooksTest.java b/java/src/test/java/com/github/copilot/sdk/HooksTest.java new file mode 100644 index 000000000..1278d082b --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/HooksTest.java @@ -0,0 +1,226 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PostToolUseHookInput; +import com.github.copilot.sdk.json.PreToolUseHookInput; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; + +/** + * Tests for hooks functionality (pre-tool-use and post-tool-use hooks). + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/hooks/. + *

+ * + *

+ * Note: Tests for userPromptSubmitted, sessionStart, and sessionEnd hooks are + * not included as they are not tested in the reference implementation .NET or + * Node.js SDKs and require test harness updates to properly invoke these hooks. + *

+ */ +public class HooksTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that pre-tool-use hook is invoked when model runs a tool. + * + * @see Snapshot: hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool + */ + @Test + void testInvokePreToolUseHookWhenModelRunsATool() throws Exception { + ctx.configureForTest("hooks", "invoke_pre_tool_use_hook_when_model_runs_a_tool"); + + var preToolUseInputs = new ArrayList(); + final String[] sessionIdHolder = new String[1]; + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + })); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + sessionIdHolder[0] = session.getSessionId(); + + // Create a file for the model to read + Path testFile = ctx.getWorkDir().resolve("hello.txt"); + Files.writeString(testFile, "Hello from the test!"); + + session.sendAndWait( + new MessageOptions().setPrompt("Read the contents of hello.txt and tell me what it says")) + .get(60, TimeUnit.SECONDS); + + // Should have received at least one preToolUse hook call + assertFalse(preToolUseInputs.isEmpty(), "Should have received preToolUse hook calls"); + + // Should have received the tool name + assertTrue(preToolUseInputs.stream().anyMatch(i -> i.getToolName() != null && !i.getToolName().isEmpty()), + "Should have received tool name in preToolUse hook"); + } + } + + /** + * Verifies that post-tool-use hook is invoked after model runs a tool. + * + * @see Snapshot: hooks/invoke_post_tool_use_hook_after_model_runs_a_tool + */ + @Test + void testInvokePostToolUseHookAfterModelRunsATool() throws Exception { + ctx.configureForTest("hooks", "invoke_post_tool_use_hook_after_model_runs_a_tool"); + + var postToolUseInputs = new ArrayList(); + final String[] sessionIdHolder = new String[1]; + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPostToolUse((input, invocation) -> { + postToolUseInputs.add(input); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + return CompletableFuture.completedFuture(null); + })); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + sessionIdHolder[0] = session.getSessionId(); + + // Create a file for the model to read + Path testFile = ctx.getWorkDir().resolve("world.txt"); + Files.writeString(testFile, "World from the test!"); + + session.sendAndWait( + new MessageOptions().setPrompt("Read the contents of world.txt and tell me what it says")) + .get(60, TimeUnit.SECONDS); + + // Should have received at least one postToolUse hook call + assertFalse(postToolUseInputs.isEmpty(), "Should have received postToolUse hook calls"); + + // Should have received the tool name and result + assertTrue(postToolUseInputs.stream().anyMatch(i -> i.getToolName() != null && !i.getToolName().isEmpty()), + "Should have received tool name in postToolUse hook"); + assertTrue(postToolUseInputs.stream().anyMatch(i -> i.getToolResult() != null), + "Should have received tool result in postToolUse hook"); + } + } + + /** + * Verifies that both hooks are invoked for a single tool call. + * + * @see Snapshot: hooks/invoke_both_hooks_for_single_tool_call + */ + @Test + void testInvokeBothHooksForSingleToolCall() throws Exception { + ctx.configureForTest("hooks", "invoke_both_hooks_for_single_tool_call"); + + var preToolUseInputs = new ArrayList(); + var postToolUseInputs = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }).setOnPostToolUse((input, invocation) -> { + postToolUseInputs.add(input); + return CompletableFuture.completedFuture(null); + })); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + // Create a file for the model to read + Path testFile = ctx.getWorkDir().resolve("both.txt"); + Files.writeString(testFile, "Testing both hooks!"); + + session.sendAndWait(new MessageOptions().setPrompt("Read the contents of both.txt")).get(60, + TimeUnit.SECONDS); + + // Both hooks should have been called + assertFalse(preToolUseInputs.isEmpty(), "Should have received preToolUse hook calls"); + assertFalse(postToolUseInputs.isEmpty(), "Should have received postToolUse hook calls"); + + // The same tool should appear in both + Set preToolNames = preToolUseInputs.stream().map(PreToolUseHookInput::getToolName) + .filter(n -> n != null && !n.isEmpty()).collect(Collectors.toSet()); + Set postToolNames = postToolUseInputs.stream().map(PostToolUseHookInput::getToolName) + .filter(n -> n != null && !n.isEmpty()).collect(Collectors.toSet()); + + // Check if there's any overlap + boolean hasOverlap = preToolNames.stream().anyMatch(postToolNames::contains); + assertTrue(hasOverlap, "Expected the same tool to appear in both pre and post hooks"); + } + } + + /** + * Verifies that tool execution is denied when pre-tool-use returns deny. + * + * @see Snapshot: hooks/deny_tool_execution_when_pre_tool_use_returns_deny + */ + @Test + void testDenyToolExecutionWhenPreToolUseReturnsDeny() throws Exception { + ctx.configureForTest("hooks", "deny_tool_execution_when_pre_tool_use_returns_deny"); + + var preToolUseInputs = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + preToolUseInputs.add(input); + // Deny all tool calls + return CompletableFuture.completedFuture(PreToolUseHookOutput.deny()); + })); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + // Create a file + Path testFile = ctx.getWorkDir().resolve("protected.txt"); + String originalContent = "Original content that should not be modified"; + Files.writeString(testFile, originalContent); + + var response = session + .sendAndWait( + new MessageOptions().setPrompt("Edit protected.txt and replace 'Original' with 'Modified'")) + .get(60, TimeUnit.SECONDS); + + // The hook should have been called + assertFalse(preToolUseInputs.isEmpty(), "Should have received preToolUse hook calls"); + + // The response should be defined + assertNotNull(response, "Response should not be null"); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java b/java/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java new file mode 100644 index 000000000..7465507f5 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/JsonIncludeNonNullTest.java @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; +import com.github.copilot.sdk.json.TelemetryConfig; +import com.github.copilot.sdk.json.UserInputRequest; + +/** + * Verifies that public DTO classes in the {@code com.github.copilot.sdk.json} + * package are annotated with {@code @JsonInclude(JsonInclude.Include.NON_NULL)} + * so that null-valued fields are omitted during JSON serialization. + */ +class JsonIncludeNonNullTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // --- Annotation presence checks --- + + @Test + void copilotClientOptionsHasNonNullAnnotation() { + assertHasNonNullInclude(CopilotClientOptions.class); + } + + @Test + void sessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(SessionConfig.class); + } + + @Test + void resumeSessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(ResumeSessionConfig.class); + } + + @Test + void infiniteSessionConfigHasNonNullAnnotation() { + assertHasNonNullInclude(InfiniteSessionConfig.class); + } + + @Test + void inputOptionsHasNonNullAnnotation() { + assertHasNonNullInclude(InputOptions.class); + } + + @Test + void modelCapabilitiesOverrideHasNonNullAnnotation() { + assertHasNonNullInclude(ModelCapabilitiesOverride.class); + } + + @Test + void providerConfigHasNonNullAnnotation() { + assertHasNonNullInclude(ProviderConfig.class); + } + + @Test + void telemetryConfigHasNonNullAnnotation() { + assertHasNonNullInclude(TelemetryConfig.class); + } + + @Test + void sessionUiCapabilitiesHasNonNullAnnotation() { + assertHasNonNullInclude(SessionUiCapabilities.class); + } + + @Test + void customAgentConfigHasNonNullAnnotation() { + assertHasNonNullInclude(CustomAgentConfig.class); + } + + @Test + void userInputRequestHasNonNullAnnotation() { + assertHasNonNullInclude(UserInputRequest.class); + } + + // --- Serialization tests: null fields are omitted --- + + @Test + void inputOptionsOmitsNullFieldsInJson() throws JsonProcessingException { + var opts = new InputOptions(); + String json = MAPPER.writeValueAsString(opts); + assertEquals("{}", json, "All-null InputOptions should serialize to empty JSON"); + } + + @Test + void telemetryConfigOmitsNullFieldsInJson() throws JsonProcessingException { + var config = new TelemetryConfig(); + String json = MAPPER.writeValueAsString(config); + assertEquals("{}", json, "All-null TelemetryConfig should serialize to empty JSON"); + } + + @Test + void sessionUiCapabilitiesOmitsNullFieldsInJson() throws JsonProcessingException { + var caps = new SessionUiCapabilities(); + String json = MAPPER.writeValueAsString(caps); + assertEquals("{}", json, "All-null SessionUiCapabilities should serialize to empty JSON"); + } + + @Test + void userInputRequestOmitsNullFieldsInJson() throws JsonProcessingException { + var req = new UserInputRequest(); + String json = MAPPER.writeValueAsString(req); + assertFalse(json.contains("null"), "UserInputRequest with no fields set should not contain 'null' values"); + } + + @Test + void inputOptionsIncludesSetFieldsInJson() throws JsonProcessingException { + var opts = new InputOptions(); + opts.setMinLength(5); + opts.setMaxLength(100); + String json = MAPPER.writeValueAsString(opts); + assertTrue(json.contains("\"minLength\":5"), "Set minLength should appear in JSON"); + assertTrue(json.contains("\"maxLength\":100"), "Set maxLength should appear in JSON"); + assertFalse(json.contains("\"title\""), "Unset title should be omitted from JSON"); + } + + @Test + void telemetryConfigIncludesSetFieldsInJson() throws JsonProcessingException { + var config = new TelemetryConfig(); + config.setOtlpEndpoint("http://localhost:4318"); + String json = MAPPER.writeValueAsString(config); + assertTrue(json.contains("\"otlpEndpoint\":\"http://localhost:4318\""), + "Set otlpEndpoint should appear in JSON"); + assertFalse(json.contains("\"filePath\""), "Unset filePath should be omitted from JSON"); + } + + @Test + void sessionUiCapabilitiesIncludesSetFieldsInJson() throws JsonProcessingException { + var caps = new SessionUiCapabilities(); + caps.setElicitation(true); + String json = MAPPER.writeValueAsString(caps); + assertTrue(json.contains("\"elicitation\":true"), "Set elicitation should appear in JSON"); + } + + private void assertHasNonNullInclude(Class clazz) { + JsonInclude annotation = clazz.getAnnotation(JsonInclude.class); + assertNotNull(annotation, clazz.getSimpleName() + " should be annotated with @JsonInclude"); + assertEquals(JsonInclude.Include.NON_NULL, annotation.value(), + clazz.getSimpleName() + " @JsonInclude should use Include.NON_NULL"); + } + +} diff --git a/java/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java b/java/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java new file mode 100644 index 000000000..4fb43f4b6 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/JsonRpcClientTest.java @@ -0,0 +1,457 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +class JsonRpcClientTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ---- Helpers ---- + + private record SocketPair(JsonRpcClient client, Socket serverSide, + ServerSocket serverSocket) implements AutoCloseable { + + @Override + public void close() throws Exception { + client.close(); + serverSide.close(); + serverSocket.close(); + } + } + + private SocketPair createSocketPair() throws Exception { + var serverSocket = new ServerSocket(0); + var clientSocket = new Socket("localhost", serverSocket.getLocalPort()); + var serverSide = serverSocket.accept(); + var client = JsonRpcClient.fromSocket(clientSocket); + return new SocketPair(client, serverSide, serverSocket); + } + + /** Write a raw JSON-RPC message (with Content-Length header) to a stream. */ + private void writeRpcMessage(OutputStream out, String json) throws IOException { + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String header = "Content-Length: " + content.length + "\r\n\r\n"; + out.write(header.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + } + + /** Read a single JSON-RPC message (Content-Length framed) from a stream. */ + private String readRpcMessage(InputStream in) throws IOException { + var headerLine = new StringBuilder(); + int contentLength = -1; + boolean lastWasCR = false; + boolean inHeaders = true; + + while (inHeaders) { + int b = in.read(); + if (b == -1) + throw new IOException("EOF"); + if (b == '\r') { + lastWasCR = true; + } else if (b == '\n') { + String line = headerLine.toString(); + headerLine.setLength(0); + lastWasCR = false; + if (line.isEmpty()) { + inHeaders = false; + } else if (line.toLowerCase().startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring(15).trim()); + } + } else { + if (lastWasCR) { + headerLine.append('\r'); + lastWasCR = false; + } + headerLine.append((char) b); + } + } + + byte[] buffer = new byte[contentLength]; + int read = 0; + while (read < contentLength) { + int result = in.read(buffer, read, contentLength - read); + if (result == -1) + throw new IOException("EOF"); + read += result; + } + return new String(buffer, StandardCharsets.UTF_8); + } + + // ---- notify() ---- + + @Test + void testNotify() throws Exception { + try (var pair = createSocketPair()) { + pair.client.notify("test.method", Map.of("key", "value")); + + String msg = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(msg); + assertEquals("2.0", node.get("jsonrpc").asText()); + assertEquals("test.method", node.get("method").asText()); + assertNull(node.get("id"), "Notification should not have an id"); + assertEquals("value", node.get("params").get("key").asText()); + } + } + + // ---- isConnected() ---- + + @Test + void testIsConnectedWithSocket() throws Exception { + try (var pair = createSocketPair()) { + assertTrue(pair.client.isConnected()); + } + } + + @Test + void testIsConnectedWithSocketClosed() throws Exception { + var pair = createSocketPair(); + pair.client.close(); + assertFalse(pair.client.isConnected()); + pair.serverSide.close(); + pair.serverSocket.close(); + } + + private static Process startBlockingProcess() throws IOException { + boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + return (isWindows ? new ProcessBuilder("cmd", "/c", "more") : new ProcessBuilder("cat")).start(); + } + + @Test + void testIsConnectedWithProcess() throws Exception { + Process proc = startBlockingProcess(); + try (var client = JsonRpcClient.fromProcess(proc)) { + assertTrue(client.isConnected()); + } + } + + @Test + void testIsConnectedWithProcessDead() throws Exception { + Process proc = startBlockingProcess(); + var client = JsonRpcClient.fromProcess(proc); + proc.destroy(); + proc.waitFor(5, TimeUnit.SECONDS); + assertFalse(client.isConnected()); + client.close(); + } + + // ---- getProcess() ---- + + @Test + void testGetProcessReturnsProcess() throws Exception { + Process proc = startBlockingProcess(); + try (var client = JsonRpcClient.fromProcess(proc)) { + assertSame(proc, client.getProcess()); + } + } + + @Test + void testGetProcessNullForSocket() throws Exception { + try (var pair = createSocketPair()) { + assertNull(pair.client.getProcess()); + } + } + + // ---- invoke() edge cases ---- + + @Test + void testInvokeWithVoidPrimitive() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), void.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"any\":\"thing\"}}"); + + assertNull(future.get(5, TimeUnit.SECONDS)); + } + } + + @Test + void testInvokeWithSendFailure() throws Exception { + var serverSocket = new ServerSocket(0); + var clientSocket = new Socket("localhost", serverSocket.getLocalPort()); + var serverSide = serverSocket.accept(); + var client = JsonRpcClient.fromSocket(clientSocket); + + // Close the client socket so write will fail + clientSocket.close(); + Thread.sleep(100); + + CompletableFuture future = client.invoke("test", Map.of(), JsonNode.class); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IOException.class, ex.getCause()); + client.close(); + serverSide.close(); + serverSocket.close(); + } + + @Test + void testInvokeWithDeserializationError() throws Exception { + try (var pair = createSocketPair()) { + // Integer cannot be deserialized from a JSON object + CompletableFuture future = pair.client.invoke("test", Map.of(), Integer.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"complex\":\"object\"}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + // CompletableFuture unwraps CompletionException, so cause is the + // underlying JsonProcessingException + assertInstanceOf(com.fasterxml.jackson.core.JsonProcessingException.class, ex.getCause()); + } + } + + // ---- handleMessage: response handling ---- + + @Test + void testResponseWithUnknownId() throws Exception { + try (var pair = createSocketPair()) { + // Response for an id that has no pending request - silently ignored + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":99999,\"result\":{\"ok\":true}}"); + + Thread.sleep(200); + // No exception, just silently dropped + } + } + + @Test + void testErrorResponseWithoutMessage() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + // Error with code but no message field + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":-32600}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + var rpcEx = assertInstanceOf(JsonRpcException.class, ex.getCause()); + assertEquals("Unknown error", rpcEx.getMessage()); + assertEquals(-32600, rpcEx.getCode()); + } + } + + @Test + void testErrorResponseWithoutCode() throws Exception { + try (var pair = createSocketPair()) { + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + + String request = readRpcMessage(pair.serverSide.getInputStream()); + long id = MAPPER.readTree(request).get("id").asLong(); + + // Error with message but no code field + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"message\":\"bad request\"}}"); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + var rpcEx = assertInstanceOf(JsonRpcException.class, ex.getCause()); + assertEquals("bad request", rpcEx.getMessage()); + assertEquals(-1, rpcEx.getCode()); + } + } + + // ---- handleMessage: server method calls ---- + + @Test + void testNoHandlerForNotification() throws Exception { + try (var pair = createSocketPair()) { + // Notification (no id) for unregistered method -- silently logged + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"method\":\"unknown.method\",\"params\":{}}"); + + Thread.sleep(200); + } + } + + @Test + void testNoHandlerForRequestSendsErrorResponse() throws Exception { + try (var pair = createSocketPair()) { + // Request (with id) for unregistered method -> -32601 Method not found + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"unknown.method\",\"params\":{}}"); + + String response = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(response); + assertEquals("2.0", node.get("jsonrpc").asText()); + assertTrue(node.has("error")); + assertEquals(-32601, node.get("error").get("code").asInt()); + assertTrue(node.get("error").get("message").asText().contains("Method not found")); + } + } + + @Test + void testHandlerThrowsExceptionWithId() throws Exception { + try (var pair = createSocketPair()) { + pair.client.registerMethodHandler("fail.method", (id, params) -> { + throw new RuntimeException("handler error"); + }); + + // Request with id - handler throws -> -32603 Internal error + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"fail.method\",\"params\":{}}"); + + String response = readRpcMessage(pair.serverSide.getInputStream()); + JsonNode node = MAPPER.readTree(response); + assertTrue(node.has("error")); + assertEquals(-32603, node.get("error").get("code").asInt()); + assertEquals("handler error", node.get("error").get("message").asText()); + } + } + + @Test + void testHandlerThrowsExceptionWithoutId() throws Exception { + try (var pair = createSocketPair()) { + pair.client.registerMethodHandler("fail.notify", (id, params) -> { + throw new RuntimeException("notify error"); + }); + + // Notification (no id) - handler throws -> just logged, no error response + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"method\":\"fail.notify\",\"params\":{}}"); + + Thread.sleep(200); + // Should not crash + } + } + + @Test + void testMethodCallWithNullId() throws Exception { + try (var pair = createSocketPair()) { + var received = new AtomicReference(); + pair.client.registerMethodHandler("test.null.id", (id, params) -> { + received.set(id); + }); + + // Explicit null id - should be treated as notification + writeRpcMessage(pair.serverSide.getOutputStream(), + "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"test.null.id\",\"params\":{}}"); + + Thread.sleep(200); + assertNull(received.get()); + } + } + + // ---- handleMessage: edge cases ---- + + @Test + void testInvalidJson() throws Exception { + try (var pair = createSocketPair()) { + writeRpcMessage(pair.serverSide.getOutputStream(), "not valid json {{{"); + + Thread.sleep(200); + // Should not crash, error is logged + } + } + + @Test + void testMessageWithNeitherResponseNorMethod() throws Exception { + try (var pair = createSocketPair()) { + // JSON object with id but no result/error/method - silently ignored + writeRpcMessage(pair.serverSide.getOutputStream(), "{\"jsonrpc\":\"2.0\",\"id\":1}"); + + Thread.sleep(200); + } + } + + // ---- reader: header parsing edge cases ---- + + @Test + void testReaderWithUnknownHeader() throws Exception { + try (var pair = createSocketPair()) { + var received = new CompletableFuture(); + pair.client.registerMethodHandler("test.header", (id, params) -> { + received.complete(params); + }); + + // Send a message with an extra header before Content-Length + var out = pair.serverSide.getOutputStream(); + String json = "{\"jsonrpc\":\"2.0\",\"method\":\"test.header\",\"params\":{\"ok\":true}}"; + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String msg = "X-Custom-Header: value\r\nContent-Length: " + content.length + "\r\n\r\n"; + out.write(msg.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + + JsonNode params = received.get(5, TimeUnit.SECONDS); + assertTrue(params.get("ok").asBoolean()); + } + } + + @Test + void testReaderWithMissingContentLength() throws Exception { + try (var pair = createSocketPair()) { + var received = new CompletableFuture(); + pair.client.registerMethodHandler("test.after", (id, params) -> { + received.complete(params); + }); + + var out = pair.serverSide.getOutputStream(); + + // First: send a message with no Content-Length header (just blank line) - + // should skip + out.write("X-Only-Header: no-length\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + out.flush(); + + // Then: send a proper message that should be received + String json = "{\"jsonrpc\":\"2.0\",\"method\":\"test.after\",\"params\":{\"ok\":true}}"; + byte[] content = json.getBytes(StandardCharsets.UTF_8); + String proper = "Content-Length: " + content.length + "\r\n\r\n"; + out.write(proper.getBytes(StandardCharsets.UTF_8)); + out.write(content); + out.flush(); + + JsonNode params = received.get(5, TimeUnit.SECONDS); + assertTrue(params.get("ok").asBoolean()); + } + } + + // ---- close() ---- + + @Test + void testCloseWithPendingRequests() throws Exception { + var pair = createSocketPair(); + CompletableFuture future = pair.client.invoke("test", Map.of(), JsonNode.class); + // Read the outgoing request to avoid blocking + readRpcMessage(pair.serverSide.getInputStream()); + + // Close without responding - should cancel pending request + pair.client.close(); + + var ex = assertThrows(ExecutionException.class, () -> future.get(5, TimeUnit.SECONDS)); + assertInstanceOf(IOException.class, ex.getCause()); + + pair.serverSide.close(); + pair.serverSocket.close(); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java b/java/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java new file mode 100644 index 000000000..1500f2794 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/LifecycleEventManagerTest.java @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.SessionLifecycleEvent; + +/** + * Unit tests for {@link LifecycleEventManager} covering subscribe, unsubscribe, + * dispatch, typed handlers, wildcard handlers, and error handling paths + * identified as gaps by JaCoCo. + */ +class LifecycleEventManagerTest { + + private LifecycleEventManager manager; + + @BeforeEach + void setup() { + manager = new LifecycleEventManager(); + } + + private static SessionLifecycleEvent event(String type) { + var e = new SessionLifecycleEvent(); + e.setType(type); + return e; + } + + // ===== wildcard subscribe / dispatch ===== + + @Test + void wildcardHandlerReceivesAllEvents() { + var received = new ArrayList(); + manager.subscribe(received::add); + + manager.dispatch(event("created")); + manager.dispatch(event("deleted")); + + assertEquals(2, received.size()); + assertEquals("created", received.get(0).getType()); + assertEquals("deleted", received.get(1).getType()); + } + + @Test + void wildcardUnsubscribeStopsDelivery() throws Exception { + var received = new ArrayList(); + AutoCloseable sub = manager.subscribe(received::add); + + manager.dispatch(event("created")); + assertEquals(1, received.size()); + + sub.close(); + + manager.dispatch(event("deleted")); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + + // ===== typed subscribe / dispatch ===== + + @Test + void typedHandlerReceivesOnlyMatchingEvents() { + var received = new ArrayList(); + manager.subscribe("created", received::add); + + manager.dispatch(event("created")); + manager.dispatch(event("deleted")); + + assertEquals(1, received.size()); + assertEquals("created", received.get(0).getType()); + } + + @Test + void typedUnsubscribeStopsDelivery() throws Exception { + var received = new ArrayList(); + AutoCloseable sub = manager.subscribe("created", received::add); + + manager.dispatch(event("created")); + assertEquals(1, received.size()); + + sub.close(); + + manager.dispatch(event("created")); + assertEquals(1, received.size(), "Should not receive events after unsubscribe"); + } + + // ===== both typed + wildcard ===== + + @Test + void bothTypedAndWildcardReceiveEvent() { + var typedReceived = new ArrayList(); + var wildcardReceived = new ArrayList(); + + manager.subscribe("created", typedReceived::add); + manager.subscribe(wildcardReceived::add); + + manager.dispatch(event("created")); + + assertEquals(1, typedReceived.size()); + assertEquals(1, wildcardReceived.size()); + } + + // ===== dispatch with no handlers ===== + + @Test + void dispatchWithNoHandlersDoesNotThrow() { + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + } + + @Test + void dispatchWithNoTypedMatchDoesNotThrow() { + var received = new ArrayList(); + manager.subscribe("deleted", received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertTrue(received.isEmpty()); + } + + // ===== error handling ===== + + @Test + void typedHandlerExceptionDoesNotPreventOtherHandlers() { + var received = new ArrayList(); + + // First handler throws + manager.subscribe("created", e -> { + throw new RuntimeException("typed handler error"); + }); + // Second handler should still receive the event + manager.subscribe("created", received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, received.size()); + } + + @Test + void wildcardHandlerExceptionDoesNotPreventOtherHandlers() { + var received = new ArrayList(); + + manager.subscribe(e -> { + throw new RuntimeException("wildcard handler error"); + }); + manager.subscribe(received::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, received.size()); + } + + @Test + void typedAndWildcardErrorsDoNotAffectEachOther() { + var wildcardReceived = new ArrayList(); + + // Typed handler throws + manager.subscribe("created", e -> { + throw new RuntimeException("typed error"); + }); + // Wildcard still receives + manager.subscribe(wildcardReceived::add); + + assertDoesNotThrow(() -> manager.dispatch(event("created"))); + assertEquals(1, wildcardReceived.size()); + } + + // ===== multiple handlers ===== + + @Test + void multipleWildcardHandlersAllReceive() { + var list1 = new ArrayList(); + var list2 = new ArrayList(); + + manager.subscribe(list1::add); + manager.subscribe(list2::add); + + manager.dispatch(event("updated")); + + assertEquals(1, list1.size()); + assertEquals(1, list2.size()); + } + + @Test + void multipleTypedHandlersAllReceive() { + var list1 = new ArrayList(); + var list2 = new ArrayList(); + + manager.subscribe("updated", list1::add); + manager.subscribe("updated", list2::add); + + manager.dispatch(event("updated")); + + assertEquals(1, list1.size()); + assertEquals(1, list2.size()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java new file mode 100644 index 000000000..a7d81646b --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/McpAndAgentsTest.java @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.DefaultAgentConfig; +import com.github.copilot.sdk.json.McpServerConfig; +import com.github.copilot.sdk.json.McpStdioServerConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; + +/** + * Tests for MCP Servers and Custom Agents functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/mcp_and_agents/. + *

+ */ +public class McpAndAgentsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + // Helper method to create an MCP stdio server configuration + private McpStdioServerConfig createLocalMcpServer(String command, List args) { + return new McpStdioServerConfig().setCommand(command).setArgs(args).setTools(List.of("*")); + } + + // ============ MCP Server Tests ============ + + /** + * Verifies that MCP server configuration is accepted on session create. + * + * @see Snapshot: + * mcp_and_agents/should_accept_mcp_server_configuration_on_session_create + */ + @Test + void testShouldAcceptMcpServerConfigurationOnSessionCreate() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); + + var mcpServers = new HashMap(); + mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + assertNotNull(session.getSessionId()); + + // Simple interaction to verify session works + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that MCP server configuration is accepted on session resume. + * + * @see Snapshot: + * mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume + */ + @Test + void testShouldAcceptMcpServerConfigurationOnSessionResume() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with MCP servers + var mcpServers = new HashMap(); + mcpServers.put("test-server", createLocalMcpServer("echo", List.of("hello"))); + + CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() + .setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent response = session2.sendAndWait(new MessageOptions().setPrompt("What is 3+3?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("6"), + "Response should contain 6: " + response.getData().content()); + + session2.close(); + } + } + + /** + * Verifies that multiple MCP servers can be configured. + * + * @see Snapshot: + * mcp_and_agents/should_accept_mcp_server_configuration_on_session_create + */ + @Test + void testShouldHandleMultipleMcpServers() throws Exception { + // Use same snapshot as single MCP server test since it doesn't depend on server + // count + ctx.configureForTest("mcp_and_agents", "should_accept_mcp_server_configuration_on_session_create"); + + var mcpServers = new HashMap(); + mcpServers.put("server1", createLocalMcpServer("echo", List.of("server1"))); + mcpServers.put("server2", createLocalMcpServer("echo", List.of("server2"))); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setMcpServers(mcpServers).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + // ============ Custom Agent Tests ============ + + /** + * Verifies that custom agent configuration is accepted on session create. + * + * @see Snapshot: + * mcp_and_agents/should_accept_custom_agent_configuration_on_session_create + */ + @Test + void testShouldAcceptCustomAgentConfigurationOnSessionCreate() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of(new CustomAgentConfig().setName("test-agent") + .setDisplayName("Test Agent").setDescription("A test agent for SDK testing") + .setPrompt("You are a helpful test agent.").setInfer(true)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + + // Simple interaction to verify session works + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 5+5?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("10"), + "Response should contain 10: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that custom agent configuration is accepted on session resume. + * + * @see Snapshot: + * mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume + */ + @Test + void testShouldAcceptCustomAgentConfigurationOnSessionResume() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_custom_agent_configuration_on_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with custom agents + List customAgents = List + .of(new CustomAgentConfig().setName("resume-agent").setDisplayName("Resume Agent") + .setDescription("An agent added on resume").setPrompt("You are a resume test agent.")); + + CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() + .setCustomAgents(customAgents).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent response = session2.sendAndWait(new MessageOptions().setPrompt("What is 6+6?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("12"), + "Response should contain 12: " + response.getData().content()); + + session2.close(); + } + } + + /** + * Verifies that custom agents can be configured with tools. + * + * @see Snapshot: + * mcp_and_agents/should_accept_custom_agent_configuration_on_session_create + */ + @Test + void testShouldAcceptCustomAgentWithToolsConfiguration() throws Exception { + // Use same snapshot as create test since this just verifies configuration + // acceptance + ctx.configureForTest("mcp_and_agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of(new CustomAgentConfig().setName("tool-agent") + .setDisplayName("Tool Agent").setDescription("An agent with specific tools") + .setPrompt("You are an agent with specific tools.").setTools(List.of("bash", "edit")).setInfer(true)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + /** + * Verifies that custom agents can be configured with MCP servers. + * + * @see Snapshot: + * mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents + */ + @Test + void testShouldAcceptCustomAgentWithMcpServers() throws Exception { + // Use combined snapshot since this uses both MCP servers and custom agents + ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); + + var agentMcpServers = new HashMap(); + agentMcpServers.put("agent-server", createLocalMcpServer("echo", List.of("agent-mcp"))); + + List customAgents = List.of(new CustomAgentConfig().setName("mcp-agent") + .setDisplayName("MCP Agent").setDescription("An agent with its own MCP servers") + .setPrompt("You are an agent with MCP servers.").setMcpServers(agentMcpServers)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + /** + * Verifies that multiple custom agents can be configured. + * + * @see Snapshot: + * mcp_and_agents/should_accept_custom_agent_configuration_on_session_create + */ + @Test + void testShouldAcceptMultipleCustomAgents() throws Exception { + // Use same snapshot as create test + ctx.configureForTest("mcp_and_agents", "should_accept_custom_agent_configuration_on_session_create"); + + List customAgents = List.of( + new CustomAgentConfig().setName("agent1").setDisplayName("Agent One").setDescription("First agent") + .setPrompt("You are agent one."), + new CustomAgentConfig().setName("agent2").setDisplayName("Agent Two").setDescription("Second agent") + .setPrompt("You are agent two.").setInfer(false)); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setCustomAgents(customAgents) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + session.close(); + } + } + + // ============ Combined Configuration Tests ============ + + /** + * Verifies that both MCP servers and custom agents can be configured. + * + * @see Snapshot: + * mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents + */ + @Test + void testShouldAcceptBothMcpServersAndCustomAgents() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_both_mcp_servers_and_custom_agents"); + + var mcpServers = new HashMap(); + mcpServers.put("shared-server", createLocalMcpServer("echo", List.of("shared"))); + + List customAgents = List.of(new CustomAgentConfig().setName("combined-agent") + .setDisplayName("Combined Agent").setDescription("An agent using shared MCP servers") + .setPrompt("You are a combined test agent.")); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setMcpServers(mcpServers) + .setCustomAgents(customAgents).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 7+7?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("14"), + "Response should contain 14: " + response.getData().content()); + + session.close(); + } + } + + // ============ DefaultAgent Tests ============ + + /** + * Verifies that sessions can be created with defaultAgent configuration and + * excludedTools hides tools from the default agent. + * + * @see Snapshot: mcp_and_agents/should_hide_excluded_tools_from_default_agent + */ + @Test + void testShouldHideExcludedToolsFromDefaultAgent() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_hide_excluded_tools_from_default_agent"); + + try (CopilotClient client = ctx.createClient()) { + // Register a secret_tool and exclude it from the default agent β€” the LLM + // should report it has no access to the tool. + Map parameters = new HashMap<>(); + parameters.put("type", "object"); + parameters.put("properties", Map.of("input", Map.of("type", "string"))); + parameters.put("required", List.of("input")); + + ToolDefinition secretTool = ToolDefinition.create("secret_tool", + "A secret tool hidden from the default agent", parameters, + invocation -> CompletableFuture.completedFuture("SECRET")); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setTools(List.of(secretTool)) + .setDefaultAgent(new DefaultAgentConfig().setExcludedTools(List.of("secret_tool"))); + + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("Do you have access to a tool called secret_tool? Answer yes or no.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().toLowerCase().contains("no"), + "Response should indicate that secret_tool is not accessible: " + response.getData().content()); + session.close(); + } + } + + /** + * Verifies that defaultAgent configuration is accepted on session resume. + * + * @see Snapshot: + * mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume + */ + @Test + void testShouldAcceptDefaultAgentConfigurationOnSessionResume() throws Exception { + ctx.configureForTest("mcp_and_agents", "should_accept_defaultagent_configuration_on_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + assertNotNull(session.getSessionId()); + String sessionId = session.getSessionId(); + // Do not call session.close() here β€” that invokes session.destroy on the + // server, + // which removes the session and causes the subsequent resumeSession to fail + // with "Session not found". The session handle is simply abandoned and the + // server-side session remains alive for the resume call below. + + CopilotSession resumedSession = client.resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setDefaultAgent(new DefaultAgentConfig().setExcludedTools(List.of("view")))) + .get(); + + assertNotNull(resumedSession.getSessionId()); + + AssistantMessageEvent response = resumedSession.sendAndWait(new MessageOptions().setPrompt("What is 3+3?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + resumedSession.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/MessageAttachmentTest.java b/java/src/test/java/com/github/copilot/sdk/MessageAttachmentTest.java new file mode 100644 index 000000000..3150cecc2 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/MessageAttachmentTest.java @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.sdk.json.Attachment; +import com.github.copilot.sdk.json.BlobAttachment; +import com.github.copilot.sdk.json.MessageAttachment; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.SendMessageRequest; + +/** + * Tests for the {@link MessageAttachment} sealed interface and type-safe + * attachment handling. + */ +class MessageAttachmentTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // ========================================================================= + // Sealed interface hierarchy + // ========================================================================= + + @Test + void attachmentImplementsMessageAttachment() { + Attachment attachment = new Attachment("file", "/path/to/file.java", "Source"); + assertInstanceOf(MessageAttachment.class, attachment); + assertEquals("file", attachment.getType()); + } + + @Test + void blobAttachmentImplementsMessageAttachment() { + BlobAttachment blob = new BlobAttachment().setData("aGVsbG8=").setMimeType("image/png") + .setDisplayName("test.png"); + assertInstanceOf(MessageAttachment.class, blob); + assertEquals("blob", blob.getType()); + } + + // ========================================================================= + // MessageOptions type safety + // ========================================================================= + + @Test + void setAttachmentsAcceptsListOfAttachment() { + MessageOptions options = new MessageOptions(); + List list = List.of(new Attachment("file", "/a.java", "A")); + options.setAttachments(list); + + assertEquals(1, options.getAttachments().size()); + assertInstanceOf(Attachment.class, options.getAttachments().get(0)); + } + + @Test + void setAttachmentsAcceptsListOfBlobAttachment() { + MessageOptions options = new MessageOptions(); + List list = List.of(new BlobAttachment().setData("ZGF0YQ==").setMimeType("image/jpeg")); + options.setAttachments(list); + + assertEquals(1, options.getAttachments().size()); + assertInstanceOf(BlobAttachment.class, options.getAttachments().get(0)); + } + + @Test + void setAttachmentsAcceptsMixedList() { + MessageOptions options = new MessageOptions(); + List mixed = List.of(new Attachment("file", "/a.java", "A"), + new BlobAttachment().setData("ZGF0YQ==").setMimeType("image/png")); + options.setAttachments(mixed); + + assertEquals(2, options.getAttachments().size()); + assertInstanceOf(Attachment.class, options.getAttachments().get(0)); + assertInstanceOf(BlobAttachment.class, options.getAttachments().get(1)); + } + + @Test + void setAttachmentsHandlesNull() { + MessageOptions options = new MessageOptions(); + options.setAttachments(null); + assertNull(options.getAttachments()); + } + + @Test + void getAttachmentsReturnsUnmodifiableList() { + MessageOptions options = new MessageOptions(); + options.setAttachments(List.of(new Attachment("file", "/a.java", "A"))); + assertThrows(UnsupportedOperationException.class, + () -> options.getAttachments().add(new Attachment("file", "/b.java", "B"))); + } + + // ========================================================================= + // SendMessageRequest type safety + // ========================================================================= + + @Test + void sendMessageRequestAcceptsMessageAttachmentList() { + SendMessageRequest request = new SendMessageRequest(); + List list = List.of(new Attachment("file", "/a.java", "A"), + new BlobAttachment().setData("ZGF0YQ==").setMimeType("image/png")); + request.setAttachments(list); + + assertEquals(2, request.getAttachments().size()); + } + + // ========================================================================= + // Jackson serialization + // ========================================================================= + + @Test + void serializeAttachmentIncludesType() throws Exception { + Attachment attachment = new Attachment("file", "/path/to/file.java", "Source"); + String json = MAPPER.writeValueAsString(attachment); + assertTrue(json.contains("\"type\":\"file\"")); + assertTrue(json.contains("\"path\":\"/path/to/file.java\"")); + } + + @Test + void serializeBlobAttachmentIncludesType() throws Exception { + BlobAttachment blob = new BlobAttachment().setData("aGVsbG8=").setMimeType("image/png") + .setDisplayName("test.png"); + String json = MAPPER.writeValueAsString(blob); + assertTrue(json.contains("\"type\":\"blob\"")); + assertTrue(json.contains("\"data\":\"aGVsbG8=\"")); + assertTrue(json.contains("\"mimeType\":\"image/png\"")); + } + + @Test + void serializeMessageOptionsWithMixedAttachments() throws Exception { + MessageOptions options = new MessageOptions().setPrompt("Describe") + .setAttachments(List.of(new Attachment("file", "/a.java", "A"), + new BlobAttachment().setData("ZGF0YQ==").setMimeType("image/png").setDisplayName("img.png"))); + + String json = MAPPER.writeValueAsString(options); + assertTrue(json.contains("\"type\":\"file\"")); + assertTrue(json.contains("\"type\":\"blob\"")); + } + + @Test + void cloneMessageOptionsPreservesAttachments() { + MessageOptions original = new MessageOptions().setPrompt("test") + .setAttachments(List.of(new Attachment("file", "/a.java", "A"))); + + MessageOptions cloned = original.clone(); + + assertEquals(1, cloned.getAttachments().size()); + assertInstanceOf(Attachment.class, cloned.getAttachments().get(0)); + // Verify clone is independent + assertNotSame(original.getAttachments(), cloned.getAttachments()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/MetadataApiTest.java b/java/src/test/java/com/github/copilot/sdk/MetadataApiTest.java new file mode 100644 index 000000000..3a9120a52 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/MetadataApiTest.java @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.ToolExecutionProgressEvent; +import com.github.copilot.sdk.json.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the new metadata APIs (getStatus, getAuthStatus, listModels) and + * the ToolExecutionProgressEvent. + */ +public class MetadataApiTest { + + private static String cliPath; + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @BeforeAll + static void setup() { + cliPath = TestUtil.findCliPath(); + } + + // ===== ToolExecutionProgressEvent Tests ===== + + @Test + void testToolExecutionProgressEventParsing() throws Exception { + String json = """ + { + "type": "tool.execution_progress", + "id": "550e8400-e29b-41d4-a716-446655440000", + "timestamp": "2026-01-22T10:00:00Z", + "data": { + "toolCallId": "call-123", + "progressMessage": "Processing file 1 of 10..." + } + } + """; + + var event = MAPPER.treeToValue(MAPPER.readTree(json), SessionEvent.class); + + assertNotNull(event); + assertInstanceOf(ToolExecutionProgressEvent.class, event); + + ToolExecutionProgressEvent progressEvent = (ToolExecutionProgressEvent) event; + assertEquals("tool.execution_progress", progressEvent.getType()); + assertNotNull(progressEvent.getData()); + assertEquals("call-123", progressEvent.getData().toolCallId()); + assertEquals("Processing file 1 of 10...", progressEvent.getData().progressMessage()); + } + + @Test + void testToolExecutionProgressEventType() { + assertEquals("tool.execution_progress", new ToolExecutionProgressEvent().getType()); + } + + // ===== Response Type Deserialization Tests ===== + + @Test + void testGetStatusResponseDeserialization() throws Exception { + String json = """ + { + "version": "1.2.3", + "protocolVersion": 2 + } + """; + + GetStatusResponse response = MAPPER.readValue(json, GetStatusResponse.class); + + assertEquals("1.2.3", response.getVersion()); + assertEquals(2, response.getProtocolVersion()); + } + + @Test + void testGetAuthStatusResponseDeserialization() throws Exception { + String json = """ + { + "isAuthenticated": true, + "authType": "user", + "host": "github.com", + "login": "testuser", + "statusMessage": "Authenticated successfully" + } + """; + + GetAuthStatusResponse response = MAPPER.readValue(json, GetAuthStatusResponse.class); + + assertTrue(response.isAuthenticated()); + assertEquals("user", response.getAuthType()); + assertEquals("github.com", response.getHost()); + assertEquals("testuser", response.getLogin()); + assertEquals("Authenticated successfully", response.getStatusMessage()); + } + + @Test + void testGetAuthStatusResponseNotAuthenticated() throws Exception { + String json = """ + { + "isAuthenticated": false, + "statusMessage": "Not authenticated" + } + """; + + GetAuthStatusResponse response = MAPPER.readValue(json, GetAuthStatusResponse.class); + + assertFalse(response.isAuthenticated()); + assertNull(response.getAuthType()); + assertNull(response.getHost()); + assertNull(response.getLogin()); + assertEquals("Not authenticated", response.getStatusMessage()); + } + + @Test + void testModelInfoDeserialization() throws Exception { + String json = """ + { + "id": "gpt-4", + "name": "GPT-4", + "capabilities": { + "supports": { + "vision": true + }, + "limits": { + "max_prompt_tokens": 8192, + "max_context_window_tokens": 128000, + "vision": { + "supported_media_types": ["image/png", "image/jpeg"], + "max_prompt_images": 10, + "max_prompt_image_size": 20971520 + } + } + }, + "policy": { + "state": "active", + "terms": "https://example.com/terms" + }, + "billing": { + "multiplier": 1.5 + } + } + """; + + ModelInfo model = MAPPER.readValue(json, ModelInfo.class); + + assertEquals("gpt-4", model.getId()); + assertEquals("GPT-4", model.getName()); + + // Capabilities + assertNotNull(model.getCapabilities()); + assertTrue(model.getCapabilities().getSupports().isVision()); + assertEquals(8192, model.getCapabilities().getLimits().getMaxPromptTokens()); + assertEquals(128000, model.getCapabilities().getLimits().getMaxContextWindowTokens()); + + // Vision limits + ModelVisionLimits visionLimits = model.getCapabilities().getLimits().getVision(); + assertNotNull(visionLimits); + assertEquals(List.of("image/png", "image/jpeg"), visionLimits.getSupportedMediaTypes()); + assertEquals(10, visionLimits.getMaxPromptImages()); + assertEquals(20971520, visionLimits.getMaxPromptImageSize()); + + // Policy + assertNotNull(model.getPolicy()); + assertEquals("active", model.getPolicy().getState()); + assertEquals("https://example.com/terms", model.getPolicy().getTerms()); + + // Billing + assertNotNull(model.getBilling()); + assertEquals(1.5, model.getBilling().getMultiplier()); + } + + @Test + void testGetModelsResponseDeserialization() throws Exception { + String json = """ + { + "models": [ + { + "id": "gpt-4", + "name": "GPT-4", + "capabilities": { + "supports": { "vision": false }, + "limits": { "max_context_window_tokens": 8192 } + } + }, + { + "id": "claude-3", + "name": "Claude 3", + "capabilities": { + "supports": { "vision": true }, + "limits": { "max_context_window_tokens": 200000 } + } + } + ] + } + """; + + GetModelsResponse response = MAPPER.readValue(json, GetModelsResponse.class); + + assertNotNull(response.getModels()); + assertEquals(2, response.getModels().size()); + assertEquals("gpt-4", response.getModels().get(0).getId()); + assertEquals("claude-3", response.getModels().get(1).getId()); + } + + // ===== Integration Tests (require CLI) ===== + + @Test + void testGetStatus() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + GetStatusResponse status = client.getStatus().get(); + + assertNotNull(status); + assertNotNull(status.getVersion()); + assertFalse(status.getVersion().isEmpty()); + assertEquals(SdkProtocolVersion.get(), status.getProtocolVersion()); + } + } + + @Test + void testGetAuthStatus() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + GetAuthStatusResponse authStatus = client.getAuthStatus().get(); + + assertNotNull(authStatus); + // The response should have a status message regardless of auth state + // We can't guarantee the user is authenticated in tests + } + } + + @Test + void testListModels() throws Exception { + assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); + + try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) { + client.start().get(); + + // Note: listModels may require authentication + // This test verifies the method exists and can be called + try { + List models = client.listModels().get(); + assertNotNull(models); + // If we got models, verify they have expected fields + for (ModelInfo model : models) { + assertNotNull(model.getId()); + assertNotNull(model.getName()); + } + } catch (Exception e) { + // May fail if not authenticated, which is acceptable in tests + System.out.println("listModels failed (may require auth): " + e.getMessage()); + } + } + } + + // ===== Protocol Version Test ===== + + @Test + void testProtocolVersionIsThree() { + assertEquals(3, SdkProtocolVersion.get()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java b/java/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java new file mode 100644 index 000000000..965d431e0 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ModeHandlersTest.java @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.ExitPlanModeAction; +import com.github.copilot.sdk.generated.ExitPlanModeCompletedEvent; +import com.github.copilot.sdk.generated.ExitPlanModeRequestedEvent; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.AutoModeSwitchResponse; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.ExitPlanModeRequest; +import com.github.copilot.sdk.json.ExitPlanModeResult; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for exit-plan-mode and auto-mode-switch handler APIs. + * + *

+ * Ported from {@code ModeHandlersE2ETests.cs} in the reference implementation + * dotnet SDK. + *

+ */ +public class ModeHandlersTest { + + private static final String TOKEN = "mode-handler-token"; + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + private CopilotClient createAuthenticatedClient() { + Map env = new HashMap<>(ctx.getEnvironment()); + env.put("COPILOT_DEBUG_GITHUB_API_URL", ctx.getProxyUrl()); + + return ctx.createClient(new CopilotClientOptions().setEnvironment(env)); + } + + private void configureAuthenticatedUser(String testName) throws Exception { + ctx.configureForTest("mode_handlers", testName); + ctx.setCopilotUserByToken(TOKEN, "mode-handler-user", "individual_pro", ctx.getProxyUrl(), + "https://localhost:1/telemetry", "mode-handler-tracking-id"); + } + + @Test + void shouldInvokeExitPlanModeHandlerWhenModelUsesTool() throws Exception { + final String summary = "Greeting file implementation plan"; + configureAuthenticatedUser("should_invoke_exit_plan_mode_handler_when_model_uses_tool"); + + var handlerCalled = new CompletableFuture(); + + try (var client = createAuthenticatedClient()) { + var session = client.createSession(new SessionConfig().setGitHubToken(TOKEN) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnExitPlanMode((request, invocation) -> { + handlerCalled.complete(request); + return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true) + .setSelectedAction("interactive").setFeedback("Approved by the Java E2E test")); + })).get(30, TimeUnit.SECONDS); + + var requestedEvent = new CompletableFuture(); + var completedEvent = new CompletableFuture(); + + session.on(event -> { + if (event instanceof ExitPlanModeRequestedEvent requested + && summary.equals(requested.getData().summary())) { + requestedEvent.complete(requested); + } else if (event instanceof ExitPlanModeCompletedEvent completed + && Boolean.TRUE.equals(completed.getData().approved()) + && ExitPlanModeAction.INTERACTIVE == completed.getData().selectedAction()) { + completedEvent.complete(completed); + } + }); + + var response = session.sendAndWait(new MessageOptions().setPrompt( + "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode.") + .setMode("plan")).get(120, TimeUnit.SECONDS); + + var request = handlerCalled.get(10, TimeUnit.SECONDS); + assertEquals(summary, request.getSummary()); + assertNotNull(request.getActions()); + assertTrue(request.getActions().contains("interactive")); + assertNotNull(request.getPlanContent()); + + var reqEvent = requestedEvent.get(10, TimeUnit.SECONDS); + assertEquals(request.getSummary(), reqEvent.getData().summary()); + + var compEvent = completedEvent.get(10, TimeUnit.SECONDS); + assertTrue(compEvent.getData().approved()); + assertEquals(ExitPlanModeAction.INTERACTIVE, compEvent.getData().selectedAction()); + + assertNotNull(response); + + session.close(); + } + } + + @Test + void shouldInvokeAutoModeSwitchHandlerWhenRateLimited() throws Exception { + configureAuthenticatedUser("should_invoke_auto_mode_switch_handler_when_rate_limited"); + + var handlerCalled = new CompletableFuture(); + + try (var client = createAuthenticatedClient()) { + var session = client.createSession( + new SessionConfig().setGitHubToken(TOKEN).setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnAutoModeSwitch((request, invocation) -> { + handlerCalled.complete(request); + return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES); + })) + .get(30, TimeUnit.SECONDS); + + var messageId = session + .send(new MessageOptions() + .setPrompt("Explain that auto mode recovered from a rate limit in one short sentence.")) + .get(30, TimeUnit.SECONDS); + + assertNotNull(messageId); + assertFalse(messageId.isEmpty()); + + var request = handlerCalled.get(30, TimeUnit.SECONDS); + assertEquals("user_weekly_rate_limited", request.getErrorCode()); + assertEquals(1.0, request.getRetryAfterSeconds()); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ModelInfoTest.java b/java/src/test/java/com/github/copilot/sdk/ModelInfoTest.java new file mode 100644 index 000000000..f36d0c4bd --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ModelInfoTest.java @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.ModelInfo; +import com.github.copilot.sdk.json.ModelSupports; +import com.github.copilot.sdk.json.SessionMetadata; + +/** + * Unit tests for {@link ModelInfo}, {@link ModelSupports}, and + * {@link SessionMetadata} getters and setters. + */ +class ModelInfoTest { + + @Test + void modelSupportsReasoningEffortGetterSetter() { + var supports = new ModelSupports(); + assertFalse(supports.isReasoningEffort()); + + supports.setReasoningEffort(true); + assertTrue(supports.isReasoningEffort()); + } + + @Test + void modelSupportsFluentChaining() { + var supports = new ModelSupports().setVision(true).setReasoningEffort(true); + assertTrue(supports.isVision()); + assertTrue(supports.isReasoningEffort()); + } + + @Test + void modelInfoSupportedReasoningEffortsGetterSetter() { + var model = new ModelInfo(); + assertNull(model.getSupportedReasoningEfforts()); + + model.setSupportedReasoningEfforts(List.of("low", "medium", "high")); + assertEquals(List.of("low", "medium", "high"), model.getSupportedReasoningEfforts()); + } + + @Test + void modelInfoDefaultReasoningEffortGetterSetter() { + var model = new ModelInfo(); + assertNull(model.getDefaultReasoningEffort()); + + model.setDefaultReasoningEffort("medium"); + assertEquals("medium", model.getDefaultReasoningEffort()); + } + + @Test + void sessionMetadataGettersAndSetters() { + var meta = new SessionMetadata(); + assertNull(meta.getStartTime()); + assertNull(meta.getModifiedTime()); + assertNull(meta.getSummary()); + assertFalse(meta.isRemote()); + + meta.setRemote(true); + assertTrue(meta.isRemote()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java b/java/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java new file mode 100644 index 000000000..36be13734 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ModuleDescriptorTest.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.module.ModuleDescriptor; +import org.junit.jupiter.api.Test; + +class ModuleDescriptorTest { + + @Test + void sdkHasExplicitModuleDescriptor() { + Module module = CopilotClient.class.getModule(); + assertTrue(module.isNamed()); + assertEquals("com.github.copilot.sdk.java", module.getName()); + + ModuleDescriptor descriptor = module.getDescriptor(); + assertTrue(descriptor.exports().stream().anyMatch(export -> export.source().equals("com.github.copilot.sdk"))); + assertTrue(descriptor.exports().stream() + .anyMatch(export -> export.source().equals("com.github.copilot.sdk.json"))); + assertTrue(descriptor.requires().stream() + .anyMatch(require -> require.name().equals("com.fasterxml.jackson.databind"))); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java b/java/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java new file mode 100644 index 000000000..2a9770e2e --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/OptionalApiAndJacksonTest.java @@ -0,0 +1,635 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.InfiniteSessionConfig; +import com.github.copilot.sdk.json.InputOptions; +import com.github.copilot.sdk.json.ModelCapabilitiesOverride; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionUiCapabilities; +import com.github.copilot.sdk.json.TelemetryConfig; +import com.github.copilot.sdk.json.UserInputRequest; +import org.junit.jupiter.api.Test; + +/** + * Validates that every {@code clearXxx()} method resets its field to absent, + * that Optional-returning getters report the correct state, and that Jackson + * omits cleared fields from serialized output. + */ +class OptionalApiAndJacksonTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ── CopilotClientOptions ────────────────────────────────────────── + + @Test + void copilotClientOptions_clearSessionIdleTimeoutSeconds() { + var opts = new CopilotClientOptions(); + opts.setSessionIdleTimeoutSeconds(120); + assertFalse(opts.getSessionIdleTimeoutSeconds().isEmpty()); + + opts.clearSessionIdleTimeoutSeconds(); + assertTrue(opts.getSessionIdleTimeoutSeconds().isEmpty()); + } + + @Test + void copilotClientOptions_clearUseLoggedInUser() { + var opts = new CopilotClientOptions(); + opts.setUseLoggedInUser(true); + assertTrue(opts.getUseLoggedInUser().isPresent()); + + opts.clearUseLoggedInUser(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); + } + + // ── SessionConfig ───────────────────────────────────────────────── + + @Test + void sessionConfig_clearEnableSessionTelemetry() { + var cfg = new SessionConfig(); + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().isPresent()); + + cfg.clearEnableSessionTelemetry(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void sessionConfig_clearEnableConfigDiscovery() { + var cfg = new SessionConfig(); + cfg.setEnableConfigDiscovery(false); + assertTrue(cfg.getEnableConfigDiscovery().isPresent()); + + cfg.clearEnableConfigDiscovery(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + } + + @Test + void sessionConfig_clearIncludeSubAgentStreamingEvents() { + var cfg = new SessionConfig(); + cfg.setIncludeSubAgentStreamingEvents(true); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isPresent()); + + cfg.clearIncludeSubAgentStreamingEvents(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + } + + // ── ResumeSessionConfig ─────────────────────────────────────────── + + @Test + void resumeSessionConfig_clearEnableSessionTelemetry() { + var cfg = new ResumeSessionConfig(); + cfg.setEnableSessionTelemetry(false); + assertTrue(cfg.getEnableSessionTelemetry().isPresent()); + + cfg.clearEnableSessionTelemetry(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + } + + @Test + void resumeSessionConfig_clearEnableConfigDiscovery() { + var cfg = new ResumeSessionConfig(); + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().isPresent()); + + cfg.clearEnableConfigDiscovery(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + } + + @Test + void resumeSessionConfig_clearIncludeSubAgentStreamingEvents() { + var cfg = new ResumeSessionConfig(); + cfg.setIncludeSubAgentStreamingEvents(false); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isPresent()); + + cfg.clearIncludeSubAgentStreamingEvents(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + } + + // ── InfiniteSessionConfig ───────────────────────────────────────── + + @Test + void infiniteSessionConfig_clearEnabled() { + var cfg = new InfiniteSessionConfig(); + cfg.setEnabled(true); + assertTrue(cfg.getEnabled().isPresent()); + + cfg.clearEnabled(); + assertTrue(cfg.getEnabled().isEmpty()); + } + + @Test + void infiniteSessionConfig_clearBackgroundCompactionThreshold() { + var cfg = new InfiniteSessionConfig(); + cfg.setBackgroundCompactionThreshold(0.75); + assertFalse(cfg.getBackgroundCompactionThreshold().isEmpty()); + + cfg.clearBackgroundCompactionThreshold(); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + } + + @Test + void infiniteSessionConfig_clearBufferExhaustionThreshold() { + var cfg = new InfiniteSessionConfig(); + cfg.setBufferExhaustionThreshold(0.9); + assertFalse(cfg.getBufferExhaustionThreshold().isEmpty()); + + cfg.clearBufferExhaustionThreshold(); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + } + + // ── InputOptions ────────────────────────────────────────────────── + + @Test + void inputOptions_clearMinLength() { + var opts = new InputOptions(); + opts.setMinLength(5); + assertFalse(opts.getMinLength().isEmpty()); + + opts.clearMinLength(); + assertTrue(opts.getMinLength().isEmpty()); + } + + @Test + void inputOptions_clearMaxLength() { + var opts = new InputOptions(); + opts.setMaxLength(100); + assertFalse(opts.getMaxLength().isEmpty()); + + opts.clearMaxLength(); + assertTrue(opts.getMaxLength().isEmpty()); + } + + // ── ModelCapabilitiesOverride.Supports ───────────────────────────── + + @Test + void supports_clearVision() { + var s = new ModelCapabilitiesOverride.Supports(); + s.setVision(true); + assertTrue(s.getVision().isPresent()); + + s.clearVision(); + assertTrue(s.getVision().isEmpty()); + } + + @Test + void supports_clearReasoningEffort() { + var s = new ModelCapabilitiesOverride.Supports(); + s.setReasoningEffort(false); + assertTrue(s.getReasoningEffort().isPresent()); + + s.clearReasoningEffort(); + assertTrue(s.getReasoningEffort().isEmpty()); + } + + // ── ModelCapabilitiesOverride.Limits ─────────────────────────────── + + @Test + void limits_clearMaxPromptTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxPromptTokens(4096); + assertFalse(l.getMaxPromptTokens().isEmpty()); + + l.clearMaxPromptTokens(); + assertTrue(l.getMaxPromptTokens().isEmpty()); + } + + @Test + void limits_clearMaxOutputTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxOutputTokens(1024); + assertFalse(l.getMaxOutputTokens().isEmpty()); + + l.clearMaxOutputTokens(); + assertTrue(l.getMaxOutputTokens().isEmpty()); + } + + @Test + void limits_clearMaxContextWindowTokens() { + var l = new ModelCapabilitiesOverride.Limits(); + l.setMaxContextWindowTokens(16384); + assertFalse(l.getMaxContextWindowTokens().isEmpty()); + + l.clearMaxContextWindowTokens(); + assertTrue(l.getMaxContextWindowTokens().isEmpty()); + } + + // ── ProviderConfig ──────────────────────────────────────────────── + + @Test + void providerConfig_clearMaxPromptTokens() { + var cfg = new ProviderConfig(); + cfg.setMaxPromptTokens(2048); + assertFalse(cfg.getMaxPromptTokens().isEmpty()); + + cfg.clearMaxPromptTokens(); + assertTrue(cfg.getMaxPromptTokens().isEmpty()); + } + + @Test + void providerConfig_clearMaxOutputTokens() { + var cfg = new ProviderConfig(); + cfg.setMaxOutputTokens(512); + assertFalse(cfg.getMaxOutputTokens().isEmpty()); + + cfg.clearMaxOutputTokens(); + assertTrue(cfg.getMaxOutputTokens().isEmpty()); + } + + // ── TelemetryConfig ─────────────────────────────────────────────── + + @Test + void telemetryConfig_clearCaptureContent() { + var cfg = new TelemetryConfig(); + cfg.setCaptureContent(true); + assertTrue(cfg.getCaptureContent().isPresent()); + + cfg.clearCaptureContent(); + assertTrue(cfg.getCaptureContent().isEmpty()); + } + + // ── SessionUiCapabilities ───────────────────────────────────────── + + @Test + void sessionUiCapabilities_clearElicitation() { + var caps = new SessionUiCapabilities(); + caps.setElicitation(true); + assertTrue(caps.getElicitation().isPresent()); + + caps.clearElicitation(); + assertTrue(caps.getElicitation().isEmpty()); + } + + // ── CustomAgentConfig ───────────────────────────────────────────── + + @Test + void customAgentConfig_clearInfer() { + var cfg = new CustomAgentConfig(); + cfg.setInfer(true); + assertTrue(cfg.getInfer().isPresent()); + + cfg.clearInfer(); + assertTrue(cfg.getInfer().isEmpty()); + } + + // ── UserInputRequest ────────────────────────────────────────────── + + @Test + void userInputRequest_clearAllowFreeform() { + var req = new UserInputRequest(); + req.setAllowFreeform(false); + assertTrue(req.getAllowFreeform().isPresent()); + + req.clearAllowFreeform(); + assertTrue(req.getAllowFreeform().isEmpty()); + } + + // ── Value retrieval through Optional getters ──────────────────────── + + @Test + void copilotClientOptions_sessionIdleTimeoutSecondsValue() { + var opts = new CopilotClientOptions(); + assertTrue(opts.getSessionIdleTimeoutSeconds().isEmpty()); + + opts.setSessionIdleTimeoutSeconds(300); + assertEquals(300, opts.getSessionIdleTimeoutSeconds().getAsInt()); + + opts.setSessionIdleTimeoutSeconds(0); + assertTrue(opts.getSessionIdleTimeoutSeconds().isPresent()); + assertEquals(0, opts.getSessionIdleTimeoutSeconds().getAsInt()); + } + + @Test + void copilotClientOptions_useLoggedInUserValue() { + var opts = new CopilotClientOptions(); + assertTrue(opts.getUseLoggedInUser().isEmpty()); + + opts.setUseLoggedInUser(true); + assertEquals(Boolean.TRUE, opts.getUseLoggedInUser().get()); + + opts.setUseLoggedInUser(false); + assertEquals(Boolean.FALSE, opts.getUseLoggedInUser().get()); + } + + @Test + void sessionConfig_enableSessionTelemetryValue() { + var cfg = new SessionConfig(); + assertFalse(cfg.getEnableSessionTelemetry().orElse(false)); + + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().orElse(false)); + + cfg.setEnableSessionTelemetry(false); + assertFalse(cfg.getEnableSessionTelemetry().orElse(true)); + } + + @Test + void sessionConfig_enableConfigDiscoveryValue() { + var cfg = new SessionConfig(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().get()); + + cfg.setEnableConfigDiscovery(false); + assertFalse(cfg.getEnableConfigDiscovery().get()); + } + + @Test + void sessionConfig_includeSubAgentStreamingEventsValue() { + var cfg = new SessionConfig(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + + cfg.setIncludeSubAgentStreamingEvents(true); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().get()); + } + + @Test + void resumeSessionConfig_enableSessionTelemetryValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getEnableSessionTelemetry().isEmpty()); + + cfg.setEnableSessionTelemetry(true); + assertTrue(cfg.getEnableSessionTelemetry().get()); + + cfg.setEnableSessionTelemetry(false); + assertFalse(cfg.getEnableSessionTelemetry().get()); + } + + @Test + void resumeSessionConfig_enableConfigDiscoveryValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getEnableConfigDiscovery().isEmpty()); + + cfg.setEnableConfigDiscovery(true); + assertTrue(cfg.getEnableConfigDiscovery().get()); + } + + @Test + void resumeSessionConfig_includeSubAgentStreamingEventsValue() { + var cfg = new ResumeSessionConfig(); + assertTrue(cfg.getIncludeSubAgentStreamingEvents().isEmpty()); + + cfg.setIncludeSubAgentStreamingEvents(false); + assertFalse(cfg.getIncludeSubAgentStreamingEvents().get()); + } + + @Test + void infiniteSessionConfig_thresholdValues() { + var cfg = new InfiniteSessionConfig(); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + + cfg.setBackgroundCompactionThreshold(0.6); + cfg.setBufferExhaustionThreshold(0.85); + assertEquals(0.6, cfg.getBackgroundCompactionThreshold().getAsDouble(), 0.001); + assertEquals(0.85, cfg.getBufferExhaustionThreshold().getAsDouble(), 0.001); + } + + @Test + void infiniteSessionConfig_enabledValue() { + var cfg = new InfiniteSessionConfig(); + assertTrue(cfg.getEnabled().isEmpty()); + + cfg.setEnabled(true); + assertTrue(cfg.getEnabled().get()); + + cfg.setEnabled(false); + assertFalse(cfg.getEnabled().get()); + } + + @Test + void inputOptions_minAndMaxLengthValues() { + var opts = new InputOptions(); + assertTrue(opts.getMinLength().isEmpty()); + assertTrue(opts.getMaxLength().isEmpty()); + + opts.setMinLength(1); + opts.setMaxLength(255); + assertEquals(1, opts.getMinLength().getAsInt()); + assertEquals(255, opts.getMaxLength().getAsInt()); + } + + @Test + void supports_visionAndReasoningEffortValues() { + var s = new ModelCapabilitiesOverride.Supports(); + assertTrue(s.getVision().isEmpty()); + assertTrue(s.getReasoningEffort().isEmpty()); + + s.setVision(true); + s.setReasoningEffort(false); + assertTrue(s.getVision().get()); + assertFalse(s.getReasoningEffort().get()); + } + + @Test + void limits_tokenValues() { + var l = new ModelCapabilitiesOverride.Limits(); + assertTrue(l.getMaxPromptTokens().isEmpty()); + assertTrue(l.getMaxOutputTokens().isEmpty()); + assertTrue(l.getMaxContextWindowTokens().isEmpty()); + + l.setMaxPromptTokens(4096); + l.setMaxOutputTokens(1024); + l.setMaxContextWindowTokens(16384); + assertEquals(4096, l.getMaxPromptTokens().getAsInt()); + assertEquals(1024, l.getMaxOutputTokens().getAsInt()); + assertEquals(16384, l.getMaxContextWindowTokens().getAsInt()); + } + + @Test + void providerConfig_tokenValues() { + var cfg = new ProviderConfig(); + assertTrue(cfg.getMaxPromptTokens().isEmpty()); + assertTrue(cfg.getMaxOutputTokens().isEmpty()); + + cfg.setMaxPromptTokens(8192); + cfg.setMaxOutputTokens(2048); + assertEquals(8192, cfg.getMaxPromptTokens().getAsInt()); + assertEquals(2048, cfg.getMaxOutputTokens().getAsInt()); + } + + @Test + void telemetryConfig_captureContentValue() { + var cfg = new TelemetryConfig(); + assertTrue(cfg.getCaptureContent().isEmpty()); + + cfg.setCaptureContent(true); + assertTrue(cfg.getCaptureContent().get()); + + cfg.setCaptureContent(false); + assertFalse(cfg.getCaptureContent().get()); + } + + @Test + void sessionUiCapabilities_elicitationValue() { + var caps = new SessionUiCapabilities(); + assertTrue(caps.getElicitation().isEmpty()); + assertFalse(caps.getElicitation().orElse(false)); + + caps.setElicitation(true); + assertTrue(caps.getElicitation().orElse(false)); + } + + @Test + void customAgentConfig_inferValue() { + var cfg = new CustomAgentConfig(); + assertTrue(cfg.getInfer().isEmpty()); + + cfg.setInfer(true); + assertTrue(cfg.getInfer().get()); + + cfg.setInfer(false); + assertFalse(cfg.getInfer().get()); + } + + @Test + void userInputRequest_allowFreeformValue() { + var req = new UserInputRequest(); + assertTrue(req.getAllowFreeform().isEmpty()); + + req.setAllowFreeform(true); + assertTrue(req.getAllowFreeform().get()); + + req.setAllowFreeform(false); + assertFalse(req.getAllowFreeform().get()); + } + + // ── JSON deserialization into Optional-returning classes ─────────── + + @Test + void jackson_deserializeSupportsWithFields() throws Exception { + String json = "{\"vision\":true,\"reasoningEffort\":false}"; + var supports = MAPPER.readValue(json, ModelCapabilitiesOverride.Supports.class); + assertTrue(supports.getVision().get()); + assertFalse(supports.getReasoningEffort().get()); + } + + @Test + void jackson_deserializeSupportsEmpty() throws Exception { + String json = "{}"; + var supports = MAPPER.readValue(json, ModelCapabilitiesOverride.Supports.class); + assertTrue(supports.getVision().isEmpty()); + assertTrue(supports.getReasoningEffort().isEmpty()); + } + + @Test + void jackson_deserializeLimitsWithFields() throws Exception { + String json = "{\"max_prompt_tokens\":4096,\"max_output_tokens\":1024,\"max_context_window_tokens\":16384}"; + var limits = MAPPER.readValue(json, ModelCapabilitiesOverride.Limits.class); + assertEquals(4096, limits.getMaxPromptTokens().getAsInt()); + assertEquals(1024, limits.getMaxOutputTokens().getAsInt()); + assertEquals(16384, limits.getMaxContextWindowTokens().getAsInt()); + } + + @Test + void jackson_deserializeLimitsEmpty() throws Exception { + String json = "{}"; + var limits = MAPPER.readValue(json, ModelCapabilitiesOverride.Limits.class); + assertTrue(limits.getMaxPromptTokens().isEmpty()); + assertTrue(limits.getMaxOutputTokens().isEmpty()); + assertTrue(limits.getMaxContextWindowTokens().isEmpty()); + } + + @Test + void jackson_deserializeInfiniteSessionConfigWithFields() throws Exception { + String json = "{\"enabled\":true,\"backgroundCompactionThreshold\":0.7,\"bufferExhaustionThreshold\":0.9}"; + var cfg = MAPPER.readValue(json, InfiniteSessionConfig.class); + assertTrue(cfg.getEnabled().get()); + assertEquals(0.7, cfg.getBackgroundCompactionThreshold().getAsDouble(), 0.001); + assertEquals(0.9, cfg.getBufferExhaustionThreshold().getAsDouble(), 0.001); + } + + @Test + void jackson_deserializeInfiniteSessionConfigEmpty() throws Exception { + String json = "{}"; + var cfg = MAPPER.readValue(json, InfiniteSessionConfig.class); + assertTrue(cfg.getEnabled().isEmpty()); + assertTrue(cfg.getBackgroundCompactionThreshold().isEmpty()); + assertTrue(cfg.getBufferExhaustionThreshold().isEmpty()); + } + + // ── Jackson serialization roundtrip ─────────────────────────────── + // + // Classes whose fields carry @JsonProperty (InfiniteSessionConfig, + // ModelCapabilitiesOverride inner classes) are serialized via field + // access: Jackson writes the field when set and omits it when cleared. + // + // Classes without @JsonProperty on fields (SessionConfig, + // CopilotClientOptions, TelemetryConfig, ProviderConfig) are not + // directly serialized β€” their values are copied to wire DTOs by + // SessionRequestBuilder. The @JsonIgnore on their Optional-returning + // getters prevents Jackson from attempting to serialize Optional + // wrappers if the class is ever processed. + + @Test + void jackson_infiniteSessionConfigClearedFieldsOmitted() throws Exception { + var cfg = new InfiniteSessionConfig(); + cfg.setEnabled(true); + cfg.setBackgroundCompactionThreshold(0.75); + cfg.setBufferExhaustionThreshold(0.9); + + String withFields = MAPPER.writeValueAsString(cfg); + assertTrue(withFields.contains("enabled")); + assertTrue(withFields.contains("backgroundCompactionThreshold")); + assertTrue(withFields.contains("bufferExhaustionThreshold")); + + cfg.clearEnabled(); + cfg.clearBackgroundCompactionThreshold(); + cfg.clearBufferExhaustionThreshold(); + + String cleared = MAPPER.writeValueAsString(cfg); + assertFalse(cleared.contains("enabled")); + assertFalse(cleared.contains("backgroundCompactionThreshold")); + assertFalse(cleared.contains("bufferExhaustionThreshold")); + } + + @Test + void jackson_modelCapabilitiesOverrideSupportsClearedFieldsOmitted() throws Exception { + var supports = new ModelCapabilitiesOverride.Supports(); + supports.setVision(true); + supports.setReasoningEffort(false); + + String withFields = MAPPER.writeValueAsString(supports); + assertTrue(withFields.contains("vision")); + assertTrue(withFields.contains("reasoningEffort")); + + supports.clearVision(); + supports.clearReasoningEffort(); + + String cleared = MAPPER.writeValueAsString(supports); + assertFalse(cleared.contains("vision")); + assertFalse(cleared.contains("reasoningEffort")); + } + + @Test + void jackson_modelCapabilitiesOverrideLimitsClearedFieldsOmitted() throws Exception { + var limits = new ModelCapabilitiesOverride.Limits(); + limits.setMaxPromptTokens(2048); + limits.setMaxOutputTokens(512); + limits.setMaxContextWindowTokens(16384); + + String withFields = MAPPER.writeValueAsString(limits); + assertTrue(withFields.contains("max_prompt_tokens")); + assertTrue(withFields.contains("max_output_tokens")); + assertTrue(withFields.contains("max_context_window_tokens")); + + limits.clearMaxPromptTokens(); + limits.clearMaxOutputTokens(); + limits.clearMaxContextWindowTokens(); + + String cleared = MAPPER.writeValueAsString(limits); + assertFalse(cleared.contains("max_prompt_tokens")); + assertFalse(cleared.contains("max_output_tokens")); + assertFalse(cleared.contains("max_context_window_tokens")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/PerSessionAuthTest.java b/java/src/test/java/com/github/copilot/sdk/PerSessionAuthTest.java new file mode 100644 index 000000000..dd5de81b8 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/PerSessionAuthTest.java @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.rpc.SessionAuthGetStatusResult; +import com.github.copilot.sdk.json.CopilotClientOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for per-session GitHub authentication. + * + *

+ * These tests verify that a per-session GitHub token is resolved into a full + * identity by the CLI runtime and that sessions with different tokens are + * isolated from each other. + *

+ */ +public class PerSessionAuthTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Creates a CopilotClient with the GitHub API URL redirected to the proxy so + * that per-session auth token resolution (fetchCopilotUser) is intercepted. + */ + private CopilotClient createAuthTestClient() { + Map env = new HashMap<>(ctx.getEnvironment()); + env.put("COPILOT_DEBUG_GITHUB_API_URL", ctx.getProxyUrl()); + return ctx.createClient(new CopilotClientOptions().setEnvironment(env)); + } + + private void setupCopilotUsers() throws Exception { + // Initialize proxy state before registering tokens β€” the proxy requires its + // internal state to be initialized (via /config) before it can handle the + // /copilot_internal/user endpoint used for per-session auth resolution. + ctx.initializeProxy(); + ctx.setCopilotUserByToken("token-alice", "alice", "individual_pro", ctx.getProxyUrl(), + "https://localhost:1/telemetry", "alice-tracking-id"); + ctx.setCopilotUserByToken("token-bob", "bob", "business", ctx.getProxyUrl(), "https://localhost:1/telemetry", + "bob-tracking-id"); + } + + @Test + void shouldAuthenticateWithGitHubToken() throws Exception { + setupCopilotUsers(); + + try (CopilotClient client = createAuthTestClient()) { + CopilotSession session = client.createSession(new SessionConfig().setGitHubToken("token-alice") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + try { + SessionAuthGetStatusResult authStatus = session.getRpc().auth.getStatus().get(); + + assertTrue(authStatus.isAuthenticated(), "Expected session to be authenticated"); + assertEquals("alice", authStatus.login()); + } finally { + session.close(); + } + } + } + + @Test + void shouldIsolateAuthBetweenSessions() throws Exception { + setupCopilotUsers(); + + try (CopilotClient client = createAuthTestClient()) { + CopilotSession sessionA = client.createSession(new SessionConfig().setGitHubToken("token-alice") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + CopilotSession sessionB = client.createSession(new SessionConfig().setGitHubToken("token-bob") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + try { + SessionAuthGetStatusResult statusA = sessionA.getRpc().auth.getStatus().get(); + SessionAuthGetStatusResult statusB = sessionB.getRpc().auth.getStatus().get(); + + assertTrue(statusA.isAuthenticated(), "Expected session A to be authenticated"); + assertEquals("alice", statusA.login()); + + assertTrue(statusB.isAuthenticated(), "Expected session B to be authenticated"); + assertEquals("bob", statusB.login()); + } finally { + sessionA.close(); + sessionB.close(); + } + } + } + + @Test + void shouldBeUnauthenticatedWithoutToken() throws Exception { + try (CopilotClient client = createAuthTestClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + try { + SessionAuthGetStatusResult authStatus = session.getRpc().auth.getStatus().get(); + + // Without a per-session token, there is no per-session identity. + // In CI the process-level fake token may still authenticate globally, + // so we check login rather than isAuthenticated. + assertNull(authStatus.login(), "Expected no login without per-session token"); + } finally { + session.close(); + } + } + } + + @Test + void shouldFailWithInvalidToken() throws Exception { + setupCopilotUsers(); + + try (CopilotClient client = createAuthTestClient()) { + Exception ex = assertThrows(Exception.class, () -> { + CopilotSession session = client.createSession(new SessionConfig().setGitHubToken("invalid-token") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + session.close(); + }); + + assertNotNull(ex); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java b/java/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java new file mode 100644 index 000000000..ab81966dc --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/PermissionRequestResultKindTest.java @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; + +/** + * Unit tests for {@link PermissionRequestResultKind}. + *

+ * Covers well-known kind values, equality, hash code, serialization, and + * backward-compatible {@link PermissionRequestResult} integration. + */ +public class PermissionRequestResultKindTest { + + @Test + void wellKnownKinds_haveExpectedValues() { + assertEquals("approve-once", PermissionRequestResultKind.APPROVED.getValue()); + assertEquals("reject", PermissionRequestResultKind.REJECTED.getValue()); + assertEquals("user-not-available", PermissionRequestResultKind.USER_NOT_AVAILABLE.getValue()); + assertEquals("no-result", PermissionRequestResultKind.NO_RESULT.getValue()); + + // Deprecated aliases still resolve + assertEquals(PermissionRequestResultKind.REJECTED, PermissionRequestResultKind.DENIED_INTERACTIVELY_BY_USER); + assertEquals(PermissionRequestResultKind.USER_NOT_AVAILABLE, + PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER); + assertEquals(PermissionRequestResultKind.USER_NOT_AVAILABLE, PermissionRequestResultKind.DENIED_BY_RULES); + } + + @Test + void equals_sameValue_returnsTrue() { + var a = new PermissionRequestResultKind("approve-once"); + assertEquals(PermissionRequestResultKind.APPROVED, a); + assertEquals(a, PermissionRequestResultKind.APPROVED); + } + + @Test + void equals_differentValue_returnsFalse() { + assertNotEquals(PermissionRequestResultKind.APPROVED, PermissionRequestResultKind.REJECTED); + } + + @Test + void equals_isCaseInsensitive() { + var upper = new PermissionRequestResultKind("APPROVE-ONCE"); + assertEquals(PermissionRequestResultKind.APPROVED, upper); + } + + @Test + void hashCode_isCaseInsensitive() { + var upper = new PermissionRequestResultKind("APPROVE-ONCE"); + assertEquals(PermissionRequestResultKind.APPROVED.hashCode(), upper.hashCode()); + } + + @Test + void toString_returnsValue() { + assertEquals("approve-once", PermissionRequestResultKind.APPROVED.toString()); + assertEquals("reject", PermissionRequestResultKind.REJECTED.toString()); + } + + @Test + void customValue_isPreserved() { + var custom = new PermissionRequestResultKind("custom-kind"); + assertEquals("custom-kind", custom.getValue()); + assertEquals("custom-kind", custom.toString()); + } + + @Test + void constructor_nullValue_treatedAsEmpty() { + var kind = new PermissionRequestResultKind(null); + assertEquals("", kind.getValue()); + assertEquals("", kind.toString()); + } + + @Test + void equals_nonKindObject_returnsFalse() { + assertNotEquals(PermissionRequestResultKind.APPROVED, "approve-once"); + } + + @Test + void jsonSerialize_writesStringValue() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + var result = new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); + String json = mapper.writeValueAsString(result); + assertTrue(json.contains("\"kind\":\"approve-once\""), "Expected kind to be serialized as string: " + json); + } + + @Test + void jsonDeserialize_readsStringValue() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + String json = "{\"kind\":\"reject\"}"; + var result = mapper.readValue(json, PermissionRequestResult.class); + assertEquals("reject", result.getKind()); + } + + @Test + void permissionRequestResult_setKindWithKindType() { + var result = new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); + assertEquals("approve-once", result.getKind()); + } + + @Test + void permissionRequestResult_setKindWithString_backwardCompatible() { + var result = new PermissionRequestResult().setKind("approve-once"); + assertEquals("approve-once", result.getKind()); + } + + @Test + void jsonRoundTrip_allWellKnownKinds() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + PermissionRequestResultKind[] kinds = {PermissionRequestResultKind.APPROVED, + PermissionRequestResultKind.REJECTED, PermissionRequestResultKind.USER_NOT_AVAILABLE, + PermissionRequestResultKind.NO_RESULT,}; + for (PermissionRequestResultKind kind : kinds) { + var result = new PermissionRequestResult().setKind(kind); + String json = mapper.writeValueAsString(result); + var deserialized = mapper.readValue(json, PermissionRequestResult.class); + assertEquals(kind.getValue(), deserialized.getKind()); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java new file mode 100644 index 000000000..041d8181c --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/PermissionsTest.java @@ -0,0 +1,477 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.MessageOptions; + +/** + * Tests for permission callback functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/permissions/. + *

+ */ +public class PermissionsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that permission handler is invoked for write operations. + * + * @see Snapshot: permissions/permission_handler_for_write_operations + */ + @Test + void testPermissionHandlerForWriteOperations(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "permission_handler_for_write_operations"); + + var permissionRequests = new ArrayList(); + + final String[] sessionIdHolder = new String[1]; + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + assertEquals(sessionIdHolder[0], invocation.getSessionId()); + // Approve the permission + return CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + sessionIdHolder[0] = session.getSessionId(); + + // Write a test file + Path testFile = ctx.getWorkDir().resolve("test.txt"); + Files.writeString(testFile, "original content"); + + session.sendAndWait(new MessageOptions().setPrompt("Edit test.txt and replace 'original' with 'modified'")) + .get(60, TimeUnit.SECONDS); + + // Should have received at least one permission request + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); + + // Should include write permission request + boolean hasWriteRequest = permissionRequests.stream().anyMatch(req -> "write".equals(req.getKind())); + assertTrue(hasWriteRequest, "Should have received a write permission request"); + + session.close(); + } + } + + /** + * Verifies that permissions can be denied. + * + * @see Snapshot: permissions/deny_permission + */ + @Test + void testDenyPermission(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "deny_permission"); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + // Deny all permissions + return CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.REJECTED)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + String originalContent = "protected content"; + Path testFile = ctx.getWorkDir().resolve("protected.txt"); + Files.writeString(testFile, originalContent); + + session.sendAndWait( + new MessageOptions().setPrompt("Edit protected.txt and replace 'protected' with 'hacked'.")) + .get(60, TimeUnit.SECONDS); + + // Verify the file was NOT modified + String content = Files.readString(testFile); + assertEquals(originalContent, content, "File should not have been modified"); + + session.close(); + } + } + + /** + * Verifies that sessions work with the approve-all permission handler. + * + * @see Snapshot: permissions/should_work_with_approve_all_permission_handler + */ + @Test + void testShouldWorkWithApproveAllPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_work_with_approve_all_permission_handler"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, + TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("4"), + "Response should contain 4: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that async permission handlers work correctly. + * + * @see Snapshot: permissions/async_permission_handler + */ + @Test + void testAsyncPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "async_permission_handler"); + + var permissionRequests = new ArrayList(); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + + // Simulate async permission check with delay + return CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(10); // Small delay to simulate async check + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED); + }); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")).get(60, + TimeUnit.SECONDS); + + // Should have received permission requests + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); + + session.close(); + } + } + + /** + * Verifies that permission handlers work when resuming a session. + * + * @see Snapshot: permissions/resume_session_with_permission_handler + */ + @Test + void testResumeSessionWithPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "resume_session_with_permission_handler"); + + var permissionRequests = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + // Create session with approve-all handler for initial exchange + CopilotSession session1 = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + // Resume with permission handler + var resumeConfig = new ResumeSessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + return CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + }); + + CopilotSession session2 = client.resumeSession(sessionId, resumeConfig).get(); + + assertEquals(sessionId, session2.getSessionId()); + + session2.sendAndWait(new MessageOptions().setPrompt("Run 'echo resumed' for me")).get(60, TimeUnit.SECONDS); + + // Should have permission requests from resumed session + assertFalse(permissionRequests.isEmpty(), "Should have received permission requests from resumed session"); + + session2.close(); + } + } + + /** + * Verifies that tool call IDs are included in permission requests. + * + * @see Snapshot: permissions/tool_call_id_in_permission_requests + */ + @Test + void testToolCallIdInPermissionRequests(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "tool_call_id_in_permission_requests"); + + final boolean[] receivedToolCallId = {false}; + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + if (request.getToolCallId() != null) { + receivedToolCallId[0] = true; + assertFalse(request.getToolCallId().isEmpty(), "Tool call ID should not be empty"); + } + return CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'")).get(60, TimeUnit.SECONDS); + + assertTrue(receivedToolCallId[0], "Should have received toolCallId in permission request"); + + session.close(); + } + } + + /** + * Verifies that permission handler errors are handled gracefully. + *

+ * When the handler throws an exception, the SDK should deny the permission and + * the assistant should indicate it couldn't complete the task. + *

+ * + * @see Snapshot: permissions/should_handle_permission_handler_errors_gracefully + */ + @Test + void testShouldHandlePermissionHandlerErrorsGracefully(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_handle_permission_handler_errors_gracefully"); + + var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { + // Throw an error in the handler + throw new RuntimeException("Handler error"); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) + .get(60, TimeUnit.SECONDS); + + // Should handle the error and deny permission + assertNotNull(response); + String content = response.getData().content().toLowerCase(); + assertTrue(content.contains("fail") || content.contains("cannot") || content.contains("unable") + || content.contains("permission"), "Response should indicate failure: " + content); + + session.close(); + } + } + + /** + * Verifies that tool operations are denied when the handler explicitly denies. + * + * @see Snapshot: + * permissions/should_deny_tool_operations_when_handler_explicitly_denies + */ + @Test + void testShouldDenyToolOperationsWhenHandlerExplicitlyDenies(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_deny_tool_operations_when_handler_explicitly_denies"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest((request, invocation) -> CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.USER_NOT_AVAILABLE)))) + .get(); + + final boolean[] permissionDenied = {false}; + session.on(ToolExecutionCompleteEvent.class, evt -> { + if (!evt.getData().success() && evt.getData().error() != null && evt.getData().error().message() != null + && evt.getData().error().message().contains("Permission denied")) { + permissionDenied[0] = true; + } + }); + + session.sendAndWait(new MessageOptions().setPrompt("Run 'node --version'")).get(60, TimeUnit.SECONDS); + + assertTrue(permissionDenied[0], "Expected a tool.execution_complete event with Permission denied result"); + + session.close(); + } + } + + /** + * Verifies that tool operations are denied when the handler explicitly denies + * after resuming a session. + * + * @see Snapshot: + * permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume + */ + @Test + void testShouldDenyToolOperationsWhenHandlerExplicitlyDeniesAfterResume(TestInfo testInfo) throws Exception { + ctx.configureForTest("permissions", "should_deny_tool_operations_when_handler_explicitly_denies_after_resume"); + + try (CopilotClient client = ctx.createClient()) { + var config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + CopilotSession session1 = client.createSession(config).get(); + String sessionId = session1.getSessionId(); + session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + CopilotSession session2 = client.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest((request, invocation) -> CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.USER_NOT_AVAILABLE)))) + .get(); + + final boolean[] permissionDenied = {false}; + session2.on(ToolExecutionCompleteEvent.class, evt -> { + if (!evt.getData().success() && evt.getData().error() != null && evt.getData().error().message() != null + && evt.getData().error().message().contains("Permission denied")) { + permissionDenied[0] = true; + } + }); + + session2.sendAndWait(new MessageOptions().setPrompt("Run 'node --version'")).get(60, TimeUnit.SECONDS); + + assertTrue(permissionDenied[0], "Expected a tool.execution_complete event with Permission denied result"); + + session2.close(); + } + } + + /** + * Verifies that a permission handler returning {@code noResult} is handled + * correctly β€” the handler is called, and the session can be aborted afterward. + * + * @see Snapshot: permissions/should_deny_permission_with_noresult_kind + */ + @Test + void testShouldDenyPermissionWithNoResultKind() throws Exception { + ctx.configureForTest("permissions", "should_deny_permission_with_noresult_kind"); + + var permissionCalled = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + permissionCalled.complete(true); + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.NO_RESULT)); + })).get(); + + session.send(new MessageOptions().setPrompt("Run 'node --version'")); + + assertTrue(permissionCalled.get(30, TimeUnit.SECONDS), + "Expected the no-result permission handler to be called."); + + session.abort().get(10, TimeUnit.SECONDS); + session.close(); + } + } + + /** + * Verifies that the runtime short-circuits the permission handler when + * {@code session.permissions.setApproveAll(true)} has been called. + * + * @see Snapshot: + * permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled + */ + @Test + void testShouldShortCircuitPermissionHandlerWhenSetApproveAllEnabled() throws Exception { + ctx.configureForTest("permissions", "should_short_circuit_permission_handler_when_set_approve_all_enabled"); + + var handlerCallCount = new int[]{0}; + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerCallCount[0]++; + return CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Set approve-all so the runtime short-circuits + var setResult = session.getRpc().permissions + .setApproveAll(new com.github.copilot.sdk.generated.rpc.SessionPermissionsSetApproveAllParams( + session.getSessionId(), true)) + .get(10, TimeUnit.SECONDS); + assertTrue(setResult.success(), "setApproveAll should succeed"); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")) + .get(60, TimeUnit.SECONDS); + assertNotNull(response); + + // Handler should not have been called since runtime approves all + assertEquals(0, handlerCallCount[0], + "Permission handler should not be called when setApproveAll is enabled"); + + session.close(); + } + } + + /** + * Verifies that the SDK correctly waits for a slow permission handler before + * completing tool execution. + * + * @see Snapshot: permissions/should_wait_for_slow_permission_handler + */ + @Test + void testShouldWaitForSlowPermissionHandler() throws Exception { + ctx.configureForTest("permissions", "should_wait_for_slow_permission_handler"); + + var handlerEntered = new CompletableFuture(); + var releaseHandler = new CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest((request, invocation) -> { + handlerEntered.complete(null); + return releaseHandler.thenApply( + v -> new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED)); + })).get(); + + // Capture the sendAndWait future before awaiting it so we can interact with the + // handler + CompletableFuture responseFuture = session + .sendAndWait(new MessageOptions().setPrompt("Run 'echo slow_handler_test'")); + + // Wait for permission handler to be entered + handlerEntered.get(30, TimeUnit.SECONDS); + + // Release the handler + releaseHandler.complete(null); + + // Session should complete successfully + AssistantMessageEvent message = responseFuture.get(60, TimeUnit.SECONDS); + assertNotNull(message); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java b/java/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java new file mode 100644 index 000000000..6bbb3ae28 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ProviderConfigTest.java @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.sdk.json.AzureOptions; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for {@link ProviderConfig} and {@link AzureOptions} BYOK (Bring Your + * Own Key) configuration. + * + *

+ * Covers fluent setters, JSON serialization, null-field omission, and + * integration with {@link SessionConfig} and {@link ResumeSessionConfig}. + *

+ */ +public class ProviderConfigTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ========================================================================= + // Fluent setters and getters + // ========================================================================= + + @Test + void testDefaultsAreNull() { + var provider = new ProviderConfig(); + + assertNull(provider.getType()); + assertNull(provider.getWireApi()); + assertNull(provider.getBaseUrl()); + assertNull(provider.getApiKey()); + assertNull(provider.getBearerToken()); + assertNull(provider.getAzure()); + } + + @Test + void testFluentSettersReturnSameInstance() { + var provider = new ProviderConfig(); + + ProviderConfig result = provider.setType("openai").setWireApi("completions") + .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test-key").setBearerToken("bearer-token") + .setAzure(new AzureOptions()); + + // All chained calls should return the same instance + assertEquals(provider, result); + } + + @Test + void testGettersReturnSetValues() { + var azure = new AzureOptions().setApiVersion("2024-02-01"); + var provider = new ProviderConfig().setType("azure-openai").setWireApi("chat") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-key").setBearerToken("my-token") + .setAzure(azure); + + assertEquals("azure-openai", provider.getType()); + assertEquals("chat", provider.getWireApi()); + assertEquals("https://my-resource.openai.azure.com", provider.getBaseUrl()); + assertEquals("my-key", provider.getApiKey()); + assertEquals("my-token", provider.getBearerToken()); + assertNotNull(provider.getAzure()); + assertEquals("2024-02-01", provider.getAzure().getApiVersion()); + } + + // ========================================================================= + // AzureOptions + // ========================================================================= + + @Test + void testAzureOptionsDefaultsAreNull() { + var azure = new AzureOptions(); + assertNull(azure.getApiVersion()); + } + + @Test + void testAzureOptionsFluentSetter() { + var azure = new AzureOptions(); + AzureOptions result = azure.setApiVersion("2023-12-01-preview"); + + assertEquals(azure, result); + assertEquals("2023-12-01-preview", azure.getApiVersion()); + } + + // ========================================================================= + // JSON serialization β€” OpenAI BYOK + // ========================================================================= + + @Test + void testSerializeOpenAiProvider() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://api.openai.com/v1") + .setApiKey("sk-test-key"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("openai", json.get("type").asText()); + assertEquals("https://api.openai.com/v1", json.get("baseUrl").asText()); + assertEquals("sk-test-key", json.get("apiKey").asText()); + // Null fields must be omitted (NON_NULL) + assertTrue(json.path("wireApi").isMissingNode()); + assertTrue(json.path("bearerToken").isMissingNode()); + assertTrue(json.path("azure").isMissingNode()); + } + + @Test + void testDeserializeOpenAiProvider() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-test-key" + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://api.openai.com/v1", provider.getBaseUrl()); + assertEquals("sk-test-key", provider.getApiKey()); + assertNull(provider.getWireApi()); + assertNull(provider.getBearerToken()); + assertNull(provider.getAzure()); + } + + // ========================================================================= + // JSON serialization β€” Azure OpenAI BYOK + // ========================================================================= + + @Test + void testSerializeAzureOpenAiProvider() throws Exception { + var provider = new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("azure-api-key").setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("azure-openai", json.get("type").asText()); + assertEquals("https://my-resource.openai.azure.com", json.get("baseUrl").asText()); + assertEquals("azure-api-key", json.get("apiKey").asText()); + assertNotNull(json.get("azure")); + assertEquals("2024-02-01", json.get("azure").get("apiVersion").asText()); + } + + @Test + void testDeserializeAzureOpenAiProvider() throws Exception { + String json = """ + { + "type": "azure-openai", + "baseUrl": "https://my-resource.openai.azure.com", + "apiKey": "azure-key", + "azure": { + "apiVersion": "2024-02-01" + } + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("azure-openai", provider.getType()); + assertEquals("https://my-resource.openai.azure.com", provider.getBaseUrl()); + assertEquals("azure-key", provider.getApiKey()); + assertNotNull(provider.getAzure()); + assertEquals("2024-02-01", provider.getAzure().getApiVersion()); + } + + // ========================================================================= + // JSON serialization β€” Bearer token authentication + // ========================================================================= + + @Test + void testSerializeBearerTokenProvider() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom-provider.example.com/v1") + .setBearerToken("eyJhbGciOiJSUzI1NiIs..."); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("openai", json.get("type").asText()); + assertEquals("https://custom-provider.example.com/v1", json.get("baseUrl").asText()); + assertEquals("eyJhbGciOiJSUzI1NiIs...", json.get("bearerToken").asText()); + assertTrue(json.path("apiKey").isMissingNode()); + } + + @Test + void testDeserializeBearerTokenProvider() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://custom-provider.example.com/v1", + "bearerToken": "my-bearer-token" + } + """; + + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://custom-provider.example.com/v1", provider.getBaseUrl()); + assertEquals("my-bearer-token", provider.getBearerToken()); + assertNull(provider.getApiKey()); + } + + // ========================================================================= + // JSON serialization β€” custom wire API + // ========================================================================= + + @Test + void testSerializeCustomWireApi() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom.example.com").setApiKey("key") + .setWireApi("responses"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("responses", json.get("wireApi").asText()); + } + + // ========================================================================= + // JSON serialization β€” all fields populated + // ========================================================================= + + @Test + void testSerializeAllFields() throws Exception { + var provider = new ProviderConfig().setType("azure-openai").setWireApi("completions") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-api-key") + .setBearerToken("my-bearer-token").setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("azure-openai", json.get("type").asText()); + assertEquals("completions", json.get("wireApi").asText()); + assertEquals("https://my-resource.openai.azure.com", json.get("baseUrl").asText()); + assertEquals("my-api-key", json.get("apiKey").asText()); + assertEquals("my-bearer-token", json.get("bearerToken").asText()); + assertEquals("2024-02-01", json.get("azure").get("apiVersion").asText()); + assertEquals(6, json.size(), "Expected exactly 6 JSON fields"); + } + + @Test + void testSerializeEmptyProviderOmitsAllFields() throws Exception { + var provider = new ProviderConfig(); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals(0, json.size(), "Empty ProviderConfig should serialize to {}"); + } + + @Test + void testSerializeEmptyAzureOptionsOmitsAllFields() throws Exception { + var azure = new AzureOptions(); + + JsonNode json = MAPPER.valueToTree(azure); + + assertEquals(0, json.size(), "Empty AzureOptions should serialize to {}"); + } + + // ========================================================================= + // JSON round-trip + // ========================================================================= + + @Test + void testRoundTripProviderConfig() throws Exception { + var original = new ProviderConfig().setType("azure-openai").setWireApi("completions") + .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-key").setBearerToken("my-token") + .setAzure(new AzureOptions().setApiVersion("2024-02-01")); + + String json = MAPPER.writeValueAsString(original); + ProviderConfig deserialized = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals(original.getType(), deserialized.getType()); + assertEquals(original.getWireApi(), deserialized.getWireApi()); + assertEquals(original.getBaseUrl(), deserialized.getBaseUrl()); + assertEquals(original.getApiKey(), deserialized.getApiKey()); + assertEquals(original.getBearerToken(), deserialized.getBearerToken()); + assertNotNull(deserialized.getAzure()); + assertEquals(original.getAzure().getApiVersion(), deserialized.getAzure().getApiVersion()); + } + + @Test + void testForwardCompatibilityIgnoresUnknownFields() throws Exception { + String json = """ + { + "type": "openai", + "baseUrl": "https://api.openai.com/v1", + "apiKey": "sk-key", + "unknownFutureField": "some-value", + "anotherNewField": 42 + } + """; + + // Should not throw - ObjectMapper is configured with + // FAIL_ON_UNKNOWN_PROPERTIES = false + ProviderConfig provider = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", provider.getType()); + assertEquals("https://api.openai.com/v1", provider.getBaseUrl()); + assertEquals("sk-key", provider.getApiKey()); + } + + // ========================================================================= + // Integration with SessionConfig + // ========================================================================= + + @Test + void testSessionConfigWithOpenAiProvider() throws Exception { + var config = new SessionConfig().setModel("gpt-4").setProvider(new ProviderConfig().setType("openai") + .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test-key")); + + JsonNode json = MAPPER.valueToTree(config); + + assertNotNull(json.get("provider")); + assertEquals("openai", json.get("provider").get("type").asText()); + assertEquals("https://api.openai.com/v1", json.get("provider").get("baseUrl").asText()); + assertEquals("sk-test-key", json.get("provider").get("apiKey").asText()); + assertEquals("gpt-4", json.get("model").asText()); + } + + @Test + void testSessionConfigWithAzureProvider() throws Exception { + var config = new SessionConfig().setModel("gpt-4").setProvider( + new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("azure-key").setAzure(new AzureOptions().setApiVersion("2024-02-01"))); + + JsonNode json = MAPPER.valueToTree(config); + + JsonNode providerNode = json.get("provider"); + assertNotNull(providerNode); + assertEquals("azure-openai", providerNode.get("type").asText()); + assertEquals("2024-02-01", providerNode.get("azure").get("apiVersion").asText()); + } + + @Test + void testSessionConfigWithoutProviderOmitsField() throws Exception { + var config = new SessionConfig().setModel("gpt-4"); + + JsonNode json = MAPPER.valueToTree(config); + + assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); + } + + // ========================================================================= + // Integration with ResumeSessionConfig + // ========================================================================= + + @Test + void testResumeSessionConfigWithProvider() throws Exception { + var config = new ResumeSessionConfig().setStreaming(true).setProvider(new ProviderConfig().setType("openai") + .setBaseUrl("https://api.openai.com/v1").setBearerToken("my-bearer-token")); + + assertNotNull(config.getProvider()); + assertEquals("openai", config.getProvider().getType()); + assertEquals("https://api.openai.com/v1", config.getProvider().getBaseUrl()); + assertEquals("my-bearer-token", config.getProvider().getBearerToken()); + } + + @Test + void testResumeSessionConfigProviderSerialization() throws Exception { + var config = new ResumeSessionConfig().setProvider( + new ProviderConfig().setType("azure-openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("key").setAzure(new AzureOptions().setApiVersion("2024-02-01"))); + + JsonNode json = MAPPER.valueToTree(config); + + JsonNode providerNode = json.get("provider"); + assertNotNull(providerNode); + assertEquals("azure-openai", providerNode.get("type").asText()); + assertEquals("https://my-resource.openai.azure.com", providerNode.get("baseUrl").asText()); + assertEquals("key", providerNode.get("apiKey").asText()); + assertEquals("2024-02-01", providerNode.get("azure").get("apiVersion").asText()); + } + + @Test + void testResumeSessionConfigWithoutProviderOmitsField() throws Exception { + var config = new ResumeSessionConfig().setStreaming(true); + + JsonNode json = MAPPER.valueToTree(config); + + assertTrue(json.path("provider").isMissingNode(), "provider field should be omitted when null"); + } + + // ========================================================================= + // Provider model and token limit overrides + // ========================================================================= + + @Test + void testProviderModelIdAndWireModelSerialization() throws Exception { + var provider = new ProviderConfig().setBaseUrl("https://example.com/provider") + .setHeaders(java.util.Map.of("Authorization", "Bearer provider-token")).setModelId("gpt-4o") + .setWireModel("my-finetune-v3").setMaxPromptTokens(100_000).setMaxOutputTokens(4096); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("https://example.com/provider", json.get("baseUrl").asText()); + assertEquals("Bearer provider-token", json.get("headers").get("Authorization").asText()); + assertEquals("gpt-4o", json.get("modelId").asText()); + assertEquals("my-finetune-v3", json.get("wireModel").asText()); + assertEquals(100_000, json.get("maxPromptTokens").asInt()); + assertEquals(4096, json.get("maxOutputTokens").asInt()); + + // Round-trip + ProviderConfig deserialized = MAPPER.readValue(MAPPER.writeValueAsString(provider), ProviderConfig.class); + assertEquals("gpt-4o", deserialized.getModelId()); + assertEquals("my-finetune-v3", deserialized.getWireModel()); + assertEquals(100_000, deserialized.getMaxPromptTokens().getAsInt()); + assertEquals(4096, deserialized.getMaxOutputTokens().getAsInt()); + } + + @Test + void testProviderModelFieldsDefaultToNull() { + var provider = new ProviderConfig(); + assertNull(provider.getModelId()); + assertNull(provider.getWireModel()); + assertTrue(provider.getMaxPromptTokens().isEmpty()); + assertTrue(provider.getMaxOutputTokens().isEmpty()); + } + + @Test + void testProviderModelFieldsOmittedWhenNull() throws Exception { + var provider = new ProviderConfig().setType("openai"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertTrue(json.path("modelId").isMissingNode()); + assertTrue(json.path("wireModel").isMissingNode()); + assertTrue(json.path("maxPromptTokens").isMissingNode()); + assertTrue(json.path("maxOutputTokens").isMissingNode()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java b/java/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java new file mode 100644 index 000000000..6e093db6c --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/RemoteSessionTest.java @@ -0,0 +1,399 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for the {@code remoteSession} feature across all session config types. + *

+ * Validates the complete lifecycle of the remote session mode: + *

    + *
  • Getter/setter and fluent chaining on {@link SessionConfig} and + * {@link ResumeSessionConfig}
  • + *
  • Propagation through {@link SessionRequestBuilder} into + * {@link CreateSessionRequest} and {@link ResumeSessionRequest}
  • + *
  • JSON wire-format serialization: correct key, correct value, omission when + * unset
  • + *
  • Defensive copy via {@code copy()} preserves the value
  • + *
  • All three supported mode values ("off", "export", "on") are transmitted + * correctly
  • + *
+ */ +class RemoteSessionTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + // ========================================================================= + // SessionConfig getter/setter/copy + // ========================================================================= + + @Test + void sessionConfig_remoteSessionDefaultsToNull() { + var cfg = new SessionConfig(); + assertNull(cfg.getRemoteSession(), "remoteSession should be null when not set"); + } + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void sessionConfig_setRemoteSessionReturnsSelf(String mode) { + var cfg = new SessionConfig(); + SessionConfig result = cfg.setRemoteSession(mode); + assertSame(cfg, result, "setRemoteSession should return the same instance for chaining"); + assertEquals(mode, cfg.getRemoteSession()); + } + + @Test + void sessionConfig_copyPreservesRemoteSession() { + var original = new SessionConfig().setRemoteSession("export"); + var copy = original.clone(); + assertEquals("export", copy.getRemoteSession()); + } + + @Test + void sessionConfig_copyPreservesNullRemoteSession() { + var original = new SessionConfig(); + var copy = original.clone(); + assertNull(copy.getRemoteSession()); + } + + @Test + void sessionConfig_setRemoteSessionToNullClearsValue() { + var cfg = new SessionConfig().setRemoteSession("on"); + cfg.setRemoteSession(null); + assertNull(cfg.getRemoteSession()); + } + + // ========================================================================= + // ResumeSessionConfig getter/setter/copy + // ========================================================================= + + @Test + void resumeSessionConfig_remoteSessionDefaultsToNull() { + var cfg = new ResumeSessionConfig(); + assertNull(cfg.getRemoteSession(), "remoteSession should be null when not set"); + } + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeSessionConfig_setRemoteSessionReturnsSelf(String mode) { + var cfg = new ResumeSessionConfig(); + ResumeSessionConfig result = cfg.setRemoteSession(mode); + assertSame(cfg, result, "setRemoteSession should return the same instance for chaining"); + assertEquals(mode, cfg.getRemoteSession()); + } + + @Test + void resumeSessionConfig_copyPreservesRemoteSession() { + var original = new ResumeSessionConfig().setRemoteSession("on"); + var copy = original.clone(); + assertEquals("on", copy.getRemoteSession()); + } + + // ========================================================================= + // SessionRequestBuilder – CreateSessionRequest wiring + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void buildCreateRequest_propagatesRemoteSession(String mode) { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals(mode, request.getRemoteSession()); + } + + @Test + void buildCreateRequest_nullConfig_remoteSessionIsNull() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null); + assertNull(request.getRemoteSession()); + } + + @Test + void buildCreateRequest_unsetRemoteSession_isNull() { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // SessionRequestBuilder – ResumeSessionRequest wiring + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void buildResumeRequest_propagatesRemoteSession(String mode) { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertEquals(mode, request.getRemoteSession()); + } + + @Test + void buildResumeRequest_nullConfig_remoteSessionIsNull() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); + assertNull(request.getRemoteSession()); + } + + @Test + void buildResumeRequest_unsetRemoteSession_isNull() { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // JSON wire-format: CreateSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void createRequest_serializesRemoteSessionCorrectly(String mode) throws Exception { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertTrue(tree.has("remoteSession"), "Serialized JSON should contain 'remoteSession' field for mode: " + mode); + assertEquals(mode, tree.get("remoteSession").asText()); + } + + @Test + void createRequest_omitsRemoteSessionWhenNull() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertFalse(tree.has("remoteSession"), "Serialized JSON should omit 'remoteSession' when not set"); + } + + // ========================================================================= + // JSON wire-format: ResumeSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeRequest_serializesRemoteSessionCorrectly(String mode) throws Exception { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertTrue(tree.has("remoteSession"), "Serialized JSON should contain 'remoteSession' field for mode: " + mode); + assertEquals(mode, tree.get("remoteSession").asText()); + } + + @Test + void resumeRequest_omitsRemoteSessionWhenNull() throws Exception { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertFalse(tree.has("remoteSession"), "Serialized JSON should omit 'remoteSession' when not set"); + } + + // ========================================================================= + // JSON round-trip: CreateSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void createRequest_roundTripsRemoteSession(String mode) throws Exception { + var config = new SessionConfig().setRemoteSession(mode); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + CreateSessionRequest deserialized = MAPPER.readValue(json, CreateSessionRequest.class); + assertEquals(mode, deserialized.getRemoteSession()); + } + + @Test + void createRequest_roundTripsNullRemoteSession() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + String json = MAPPER.writeValueAsString(request); + CreateSessionRequest deserialized = MAPPER.readValue(json, CreateSessionRequest.class); + assertNull(deserialized.getRemoteSession()); + } + + // ========================================================================= + // JSON round-trip: ResumeSessionRequest + // ========================================================================= + + @ParameterizedTest + @ValueSource(strings = {"off", "export", "on"}) + void resumeRequest_roundTripsRemoteSession(String mode) throws Exception { + var config = new ResumeSessionConfig().setRemoteSession(mode); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + + String json = MAPPER.writeValueAsString(request); + ResumeSessionRequest deserialized = MAPPER.readValue(json, ResumeSessionRequest.class); + assertEquals(mode, deserialized.getRemoteSession()); + } + + // ========================================================================= + // Fluent chaining: remoteSession composes with other config options + // ========================================================================= + + @Test + void sessionConfig_remoteSessionComposesWithOtherFields() { + var config = new SessionConfig().setModel("gpt-4o").setRemoteSession("export").setReasoningEffort("high"); + + assertEquals("gpt-4o", config.getModel()); + assertEquals("export", config.getRemoteSession()); + assertEquals("high", config.getReasoningEffort()); + } + + @Test + void resumeSessionConfig_remoteSessionComposesWithOtherFields() { + var config = new ResumeSessionConfig().setModel("gpt-4o").setRemoteSession("on").setReasoningEffort("medium"); + + assertEquals("gpt-4o", config.getModel()); + assertEquals("on", config.getRemoteSession()); + assertEquals("medium", config.getReasoningEffort()); + } + + @Test + void createRequest_remoteSessionDoesNotAffectOtherFields() throws Exception { + var config = new SessionConfig().setModel("gpt-4o").setRemoteSession("export").setReasoningEffort("high") + .setGitHubToken("ghp_test"); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertEquals("export", tree.get("remoteSession").asText()); + assertEquals("gpt-4o", tree.get("model").asText()); + assertEquals("high", tree.get("reasoningEffort").asText()); + assertEquals("ghp_test", tree.get("gitHubToken").asText()); + } + + @Test + void resumeRequest_remoteSessionDoesNotAffectOtherFields() throws Exception { + var config = new ResumeSessionConfig().setModel("gpt-4o").setRemoteSession("on").setReasoningEffort("medium") + .setGitHubToken("ghp_test"); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + String json = MAPPER.writeValueAsString(request); + JsonNode tree = MAPPER.readTree(json); + + assertEquals("on", tree.get("remoteSession").asText()); + assertEquals("gpt-4o", tree.get("model").asText()); + assertEquals("medium", tree.get("reasoningEffort").asText()); + assertEquals("ghp_test", tree.get("gitHubToken").asText()); + } + + // ========================================================================= + // Deserialization from raw JSON (simulates CLI response ingestion) + // ========================================================================= + + @Test + void createRequest_deserializesRemoteSessionFromRawJson() throws Exception { + String json = """ + { + "sessionId": "test-session", + "remoteSession": "export", + "model": "gpt-4o" + } + """; + CreateSessionRequest request = MAPPER.readValue(json, CreateSessionRequest.class); + assertEquals("export", request.getRemoteSession()); + assertEquals("test-session", request.getSessionId()); + } + + @Test + void resumeRequest_deserializesRemoteSessionFromRawJson() throws Exception { + String json = """ + { + "sessionId": "resume-session", + "remoteSession": "on", + "model": "gpt-4o" + } + """; + ResumeSessionRequest request = MAPPER.readValue(json, ResumeSessionRequest.class); + assertEquals("on", request.getRemoteSession()); + assertEquals("resume-session", request.getSessionId()); + } + + @Test + void createRequest_deserializesWithMissingRemoteSession() throws Exception { + String json = """ + { + "sessionId": "test-session", + "model": "gpt-4o" + } + """; + CreateSessionRequest request = MAPPER.readValue(json, CreateSessionRequest.class); + assertNull(request.getRemoteSession()); + } + + // ========================================================================= + // Handoff event with remoteSessionId (remote session lifecycle) + // ========================================================================= + + @Test + void handoffEvent_withRemoteSourceType_containsRemoteSessionId() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "handoffTime": "2025-06-01T12:00:00Z", + "sourceType": "remote", + "remoteSessionId": "remote-sess-42", + "summary": "Session exported for remote execution", + "repository": { + "owner": "test-org", + "name": "test-repo", + "branch": "feature-branch" + } + } + } + """; + + var event = (com.github.copilot.sdk.generated.SessionHandoffEvent) MAPPER.readValue(json, + com.github.copilot.sdk.generated.SessionEvent.class); + assertNotNull(event); + var data = event.getData(); + assertEquals("remote-sess-42", data.remoteSessionId()); + assertEquals(com.github.copilot.sdk.generated.HandoffSourceType.REMOTE, data.sourceType()); + assertEquals("Session exported for remote execution", data.summary()); + assertEquals("test-org", data.repository().owner()); + assertEquals("test-repo", data.repository().name()); + assertEquals("feature-branch", data.repository().branch()); + } + + @Test + void handoffEvent_withoutRemoteSessionId_fieldIsNull() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "targetAgent": "local-agent" + } + } + """; + + var event = (com.github.copilot.sdk.generated.SessionHandoffEvent) MAPPER.readValue(json, + com.github.copilot.sdk.generated.SessionEvent.class); + assertNotNull(event); + assertNull(event.getData().remoteSessionId()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java b/java/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java new file mode 100644 index 000000000..7453a7b26 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/RpcHandlerDispatcherTest.java @@ -0,0 +1,593 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.PreToolUseHookOutput; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.SessionLifecycleEvent; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Unit tests for {@link RpcHandlerDispatcher} focusing on coverage gaps + * identified by JaCoCo: unknown sessions, missing fields, error paths, and edge + * cases for each handler method. + */ +class RpcHandlerDispatcherTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + private static final int SOCKET_TIMEOUT_MS = 5000; + + private Socket clientSideSocket; + private Socket serverSideSocket; + private JsonRpcClient rpc; + private Map sessions; + private CopyOnWriteArrayList lifecycleEvents; + private RpcHandlerDispatcher dispatcher; + private InputStream responseStream; + private Map> handlers; + + @BeforeEach + void setup() throws Exception { + // Create a socket pair for the JsonRpcClient + try (ServerSocket ss = new ServerSocket(0)) { + clientSideSocket = new Socket("localhost", ss.getLocalPort()); + serverSideSocket = ss.accept(); + } + serverSideSocket.setSoTimeout(SOCKET_TIMEOUT_MS); + + rpc = JsonRpcClient.fromSocket(clientSideSocket); + responseStream = serverSideSocket.getInputStream(); + + sessions = new ConcurrentHashMap<>(); + lifecycleEvents = new CopyOnWriteArrayList<>(); + dispatcher = new RpcHandlerDispatcher(sessions, lifecycleEvents::add, null); + dispatcher.registerHandlers(rpc); + + // Extract the registered handlers via reflection so we can invoke them directly + Field f = JsonRpcClient.class.getDeclaredField("notificationHandlers"); + f.setAccessible(true); + @SuppressWarnings("unchecked") + Map> h = (Map>) f.get(rpc); + handlers = h; + } + + @AfterEach + void teardown() throws Exception { + if (rpc != null) { + rpc.close(); + } + if (serverSideSocket != null) { + serverSideSocket.close(); + } + if (clientSideSocket != null) { + clientSideSocket.close(); + } + } + + /** Invoke a registered RPC handler directly. */ + private void invokeHandler(String method, String requestId, JsonNode params) { + handlers.get(method).accept(requestId, params); + } + + /** Read a single JSON-RPC response message from the server-side socket. */ + private JsonNode readResponse() throws Exception { + StringBuilder header = new StringBuilder(); + while (!header.toString().endsWith("\r\n\r\n")) { + int b = responseStream.read(); + if (b == -1) { + throw new java.io.IOException("Unexpected end of stream"); + } + header.append((char) b); + } + String headerStr = header.toString().trim(); + int idx = headerStr.indexOf(':'); + int contentLength = Integer.parseInt(headerStr.substring(idx + 1).trim()); + byte[] body = responseStream.readNBytes(contentLength); + return MAPPER.readTree(body); + } + + /** Create and register a CopilotSession in the sessions map. */ + private CopilotSession createSession(String sessionId) { + CopilotSession session = new CopilotSession(sessionId, rpc); + sessions.put(sessionId, session); + return session; + } + + // ===== session.event tests ===== + + @Test + void sessionEventWithNullEventNode() throws Exception { + CopilotSession session = createSession("s1"); + var dispatched = new CopyOnWriteArrayList<>(); + session.on(dispatched::add); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + // "event" field is absent β†’ eventNode is null + + invokeHandler("session.event", null, params); + + // Give a moment for async processing (though this handler is synchronous) + Thread.sleep(50); + assertTrue(dispatched.isEmpty(), "No events should be dispatched when eventNode is null"); + } + + @Test + void sessionEventWithUnknownSession() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "unknown"); + ObjectNode event = params.putObject("event"); + event.put("type", "assistantMessage"); + event.putObject("data").put("content", "hello"); + + // Should not throw β€” silently skips when session is not found + assertDoesNotThrow(() -> invokeHandler("session.event", null, params)); + } + + // ===== session.lifecycle tests ===== + + @Test + void lifecycleEventWithMissingTypeAndSessionId() { + ObjectNode params = MAPPER.createObjectNode(); + // No "type" or "sessionId" fields β€” defaults to "" + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("", lifecycleEvents.get(0).getType()); + assertEquals("", lifecycleEvents.get(0).getSessionId()); + } + + @Test + void lifecycleEventWithoutMetadata() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("type", "started"); + params.put("sessionId", "s1"); + // No "metadata" field at all + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("started", lifecycleEvents.get(0).getType()); + assertNull(lifecycleEvents.get(0).getMetadata()); + } + + @Test + void lifecycleEventWithNullMetadata() { + ObjectNode params = MAPPER.createObjectNode(); + params.put("type", "ended"); + params.put("sessionId", "s2"); + params.putNull("metadata"); + + invokeHandler("session.lifecycle", null, params); + + assertEquals(1, lifecycleEvents.size()); + assertEquals("ended", lifecycleEvents.get(0).getType()); + assertNull(lifecycleEvents.get(0).getMetadata()); + } + + // ===== tool.call tests ===== + + @Test + void toolCallWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("toolCallId", "tc1"); + params.put("toolName", "my_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "1", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("nonexistent")); + } + + @Test + void toolCallWithUnknownTool() throws Exception { + createSession("s1"); + // Don't register any tools + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "nonexistent_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "2", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("failure", result.get("resultType").asText()); + assertTrue(result.get("error").asText().contains("nonexistent_tool")); + } + + @Test + void toolCallReturnsToolResultObjectDirectly() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("my_tool", "A test tool", Map.of("type", "object"), + invocation -> CompletableFuture.completedFuture(ToolResultObject.success("direct result"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "my_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "3", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("success", result.get("resultType").asText()); + assertEquals("direct result", result.get("textResultForLlm").asText()); + } + + @Test + void toolCallWithNonStringResult() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("map_tool", "Returns a map", Map.of("type", "object"), + invocation -> CompletableFuture.completedFuture(Map.of("key", "value"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "map_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "4", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("success", result.get("resultType").asText()); + // The map should be serialized to JSON string + assertNotNull(result.get("textResultForLlm").asText()); + } + + @Test + void toolCallHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + var tool = ToolDefinition.create("fail_tool", "Fails", Map.of("type", "object"), + invocation -> CompletableFuture.failedFuture(new RuntimeException("tool error"))); + session.registerTools(List.of(tool)); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("toolCallId", "tc1"); + params.put("toolName", "fail_tool"); + params.putObject("arguments"); + + invokeHandler("tool.call", "5", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("failure", result.get("resultType").asText()); + } + + // ===== permission.request tests ===== + + @Test + void permissionRequestWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "10", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("user-not-available", result.get("kind").asText()); + } + + @Test + void permissionRequestWithHandler() throws Exception { + CopilotSession session = createSession("s1"); + session.registerPermissionHandler((request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind("allow"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "11", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("allow", result.get("kind").asText()); + } + + @Test + void permissionRequestHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerPermissionHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("permission error"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "12", params); + + JsonNode response = readResponse(); + // CopilotSession catches the exception and returns a denied result + JsonNode result = response.get("result").get("result"); + assertEquals("user-not-available", result.get("kind").asText()); + } + + @Test + void permissionRequestV2RejectsNoResult() throws Exception { + CopilotSession session = createSession("s1"); + session.registerPermissionHandler((request, invocation) -> CompletableFuture + .completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.NO_RESULT))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.putObject("permissionRequest"); + + invokeHandler("permission.request", "13", params); + + // V2 protocol does not support NO_RESULT β€” the handler should fall through + // to the exception path and respond with denied. + JsonNode response = readResponse(); + JsonNode result = response.get("result").get("result"); + assertEquals("user-not-available", result.get("kind").asText()); + } + + // ===== userInput.request tests ===== + + @Test + void userInputRequestWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "20", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void userInputRequestWithNullChoicesAndFreeform() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler((request, invocation) -> CompletableFuture + .completedFuture(new UserInputResponse().setAnswer("my answer").setWasFreeform(true))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What is your name?"); + // No "choices" or "allowFreeform" fields + + invokeHandler("userInput.request", "21", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result"); + assertEquals("my answer", result.get("answer").asText()); + assertTrue(result.get("wasFreeform").asBoolean()); + } + + @Test + void userInputRequestWithNullAnswer() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler((request, invocation) -> CompletableFuture + .completedFuture(new UserInputResponse().setAnswer(null).setWasFreeform(false))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "Choose something"); + + invokeHandler("userInput.request", "22", params); + + JsonNode response = readResponse(); + JsonNode result = response.get("result"); + // Null answer should be replaced with empty string + assertEquals("", result.get("answer").asText()); + assertFalse(result.get("wasFreeform").asBoolean()); + } + + @Test + void userInputRequestWithNoHandler() throws Exception { + // Session exists but no user input handler registered + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "23", params); + + JsonNode response = readResponse(); + // No handler β†’ CopilotSession returns failedFuture β†’ dispatcher's + // .exceptionally() fires + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("User input handler error")); + } + + @Test + void userInputRequestHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerUserInputHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("handler failed"))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("question", "What?"); + + invokeHandler("userInput.request", "24", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + } + + // ===== hooks.invoke tests ===== + + @Test + void hooksInvokeWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "30", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void hooksInvokeWithNullOutput() throws Exception { + CopilotSession session = createSession("s1"); + // Register empty hooks β€” no specific handler for preToolUse β†’ returns null + session.registerHooks(new SessionHooks()); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "31", params); + + JsonNode response = readResponse(); + JsonNode output = response.get("result").get("output"); + assertTrue(output == null || output.isNull(), "Output should be null when no hook handler is set"); + } + + @Test + void hooksInvokeWithNonNullOutput() throws Exception { + CopilotSession session = createSession("s1"); + session.registerHooks(new SessionHooks().setOnPreToolUse( + (input, invocation) -> CompletableFuture.completedFuture(PreToolUseHookOutput.allow()))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + ObjectNode input = params.putObject("input"); + input.put("toolName", "some_tool"); + input.put("toolCallId", "tc1"); + + invokeHandler("hooks.invoke", "32", params); + + JsonNode response = readResponse(); + JsonNode output = response.get("result").get("output"); + assertNotNull(output); + assertEquals("allow", output.get("permissionDecision").asText()); + } + + @Test + void hooksInvokeHandlerFails() throws Exception { + CopilotSession session = createSession("s1"); + session.registerHooks(new SessionHooks().setOnPreToolUse( + (input, invocation) -> CompletableFuture.failedFuture(new RuntimeException("hook error")))); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + ObjectNode input = params.putObject("input"); + input.put("toolName", "some_tool"); + input.put("toolCallId", "tc1"); + + invokeHandler("hooks.invoke", "33", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32603, response.get("error").get("code").asInt()); + assertTrue(response.get("error").get("message").asText().contains("Hooks handler error")); + } + + @Test + void hooksInvokeWithNoHooksRegistered() throws Exception { + // Session exists but no hooks registered at all β†’ returns null output + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + params.put("hookType", "preToolUse"); + params.putObject("input"); + + invokeHandler("hooks.invoke", "34", params); + + JsonNode response = readResponse(); + JsonNode output = response.get("result").get("output"); + assertTrue(output == null || output.isNull(), "Output should be null when no hooks registered"); + } + + // ===== systemMessage.transform tests ===== + + @Test + void systemMessageTransformWithUnknownSession() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "nonexistent"); + params.putObject("sections"); + + invokeHandler("systemMessage.transform", "40", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void systemMessageTransformWithNullSessionId() throws Exception { + ObjectNode params = MAPPER.createObjectNode(); + // sessionId omitted β†’ null β†’ session lookup returns null β†’ error + params.putObject("sections"); + + invokeHandler("systemMessage.transform", "41", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("error")); + assertEquals(-32602, response.get("error").get("code").asInt()); + } + + @Test + void systemMessageTransformWithKnownSessionNoCallbacks() throws Exception { + // Session without transform callbacks returns the sections unchanged + createSession("s1"); + + ObjectNode params = MAPPER.createObjectNode(); + params.put("sessionId", "s1"); + ObjectNode sections = params.putObject("sections"); + ObjectNode sectionData = sections.putObject("identity"); + sectionData.put("content", "Original content"); + + invokeHandler("systemMessage.transform", "42", params); + + JsonNode response = readResponse(); + assertNotNull(response.get("result")); + JsonNode resultSections = response.get("result").get("sections"); + assertNotNull(resultSections); + assertEquals("Original content", resultSections.get("identity").get("content").asText()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/RpcWrappersTest.java b/java/src/test/java/com/github/copilot/sdk/RpcWrappersTest.java new file mode 100644 index 000000000..e2356d985 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/RpcWrappersTest.java @@ -0,0 +1,471 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.generated.rpc.McpConfigAddParams; +import com.github.copilot.sdk.generated.rpc.McpDiscoverParams; +import com.github.copilot.sdk.generated.rpc.RpcCaller; +import com.github.copilot.sdk.generated.rpc.ServerRpc; +import com.github.copilot.sdk.generated.rpc.SessionAgentSelectParams; +import com.github.copilot.sdk.generated.rpc.SessionModelSwitchToParams; +import com.github.copilot.sdk.generated.rpc.SessionRpc; + +/** + * Unit tests for the generated RPC wrapper classes ({@link ServerRpc} and + * {@link SessionRpc}). Uses a simple in-memory {@link RpcCaller} stub to verify + * that: + *
    + *
  • The correct RPC method name is passed for each API call.
  • + *
  • {@link SessionRpc} automatically injects {@code sessionId} into every + * call.
  • + *
  • Session methods with extra params merge those params with the session + * ID.
  • + *
+ */ +class RpcWrappersTest { + + /** + * A simple stub {@link RpcCaller} that records every call made to it and + * returns a pre-configured result (or null). + */ + private static final class StubCaller implements RpcCaller { + + static record Call(String method, Object params) { + } + + final List calls = new ArrayList<>(); + Object nextResult = null; + + @Override + @SuppressWarnings("unchecked") + public CompletableFuture invoke(String method, Object params, Class resultType) { + calls.add(new Call(method, params)); + return CompletableFuture.completedFuture((T) nextResult); + } + } + + // ── ServerRpc tests ─────────────────────────────────────────────────────── + + @Test + void serverRpc_instantiates_with_all_namespace_fields() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + assertNotNull(server.models); + assertNotNull(server.tools); + assertNotNull(server.account); + assertNotNull(server.mcp); + assertNotNull(server.mcp.config); // nested sub-namespace + assertNotNull(server.sessionFs); + assertNotNull(server.sessions); + } + + @Test + void serverRpc_models_list_invokes_correct_rpc_method() { + var stub = new StubCaller(); + stub.nextResult = null; // no result needed for method dispatch test + + var server = new ServerRpc(stub); + server.models.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("models.list", stub.calls.get(0).method()); + } + + @Test + void serverRpc_ping_passes_params_directly() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new com.github.copilot.sdk.generated.rpc.PingParams(null); + server.ping(params); + + assertEquals(1, stub.calls.size()); + assertEquals("ping", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_mcp_config_list_invokes_correct_rpc_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + server.mcp.config.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("mcp.config.list", stub.calls.get(0).method()); + } + + @Test + void serverRpc_mcp_config_add_passes_params() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new McpConfigAddParams("myServer", null); + server.mcp.config.add(params); + + assertEquals(1, stub.calls.size()); + assertEquals("mcp.config.add", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_mcp_discover_passes_params() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new McpDiscoverParams("/workspace"); + server.mcp.discover(params); + + assertEquals(1, stub.calls.size()); + assertEquals("mcp.discover", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + // ── SessionRpc tests ────────────────────────────────────────────────────── + + @Test + void sessionRpc_instantiates_with_all_namespace_fields() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-001"); + + assertNotNull(session.model); + assertNotNull(session.mode); + assertNotNull(session.plan); + assertNotNull(session.workspaces); + assertNotNull(session.fleet); + assertNotNull(session.agent); + assertNotNull(session.skills); + assertNotNull(session.mcp); + assertNotNull(session.plugins); + assertNotNull(session.extensions); + assertNotNull(session.tools); + assertNotNull(session.commands); + assertNotNull(session.ui); + assertNotNull(session.permissions); + assertNotNull(session.shell); + assertNotNull(session.history); + assertNotNull(session.usage); + } + + @Test + void sessionRpc_model_getCurrent_injects_sessionId_automatically() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-abc"); + + session.model.getCurrent(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.model.getCurrent", stub.calls.get(0).method()); + + // Params should be a Map containing sessionId + var params = stub.calls.get(0).params(); + assertInstanceOf(Map.class, params); + assertEquals("sess-abc", ((Map) params).get("sessionId")); + } + + @Test + void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-xyz"); + + // switchTo takes extra params beyond sessionId + var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null); + session.model.switchTo(switchParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.model.switchTo", stub.calls.get(0).method()); + + // Params should be a JsonNode containing both sessionId and modelId + var params = stub.calls.get(0).params(); + assertInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class, params); + var node = (com.fasterxml.jackson.databind.node.ObjectNode) params; + assertEquals("sess-xyz", node.get("sessionId").asText()); + assertEquals("gpt-5", node.get("modelId").asText()); + } + + @Test + void sessionRpc_agent_list_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-999"); + + session.agent.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.agent.list", stub.calls.get(0).method()); + + var params = stub.calls.get(0).params(); + assertInstanceOf(Map.class, params); + assertEquals("sess-999", ((Map) params).get("sessionId")); + } + + @Test + void sessionRpc_agent_select_merges_sessionId_with_extra_params() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-select"); + + var selectParams = new SessionAgentSelectParams(null, "my-agent"); + session.agent.select(selectParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.agent.select", stub.calls.get(0).method()); + + var params = stub.calls.get(0).params(); + assertInstanceOf(com.fasterxml.jackson.databind.node.ObjectNode.class, params); + var node = (com.fasterxml.jackson.databind.node.ObjectNode) params; + assertEquals("sess-select", node.get("sessionId").asText()); + assertEquals("my-agent", node.get("name").asText()); + } + + @Test + void sessionRpc_different_sessions_have_different_sessionIds() { + var stub = new StubCaller(); + var session1 = new SessionRpc(stub, "sess-1"); + var session2 = new SessionRpc(stub, "sess-2"); + + session1.model.getCurrent(); + session2.model.getCurrent(); + + assertEquals(2, stub.calls.size()); + var params1 = (Map) stub.calls.get(0).params(); + var params2 = (Map) stub.calls.get(1).params(); + assertEquals("sess-1", params1.get("sessionId")); + assertEquals("sess-2", params2.get("sessionId")); + } + + @Test + void rpcCaller_is_implementable_as_anonymous_class_or_method_reference() { + // Verify RpcCaller can be used as an anonymous class + AtomicReference capturedMethod = new AtomicReference<>(); + RpcCaller caller = new RpcCaller() { + @Override + public CompletableFuture invoke(String method, Object params, Class resultType) { + capturedMethod.set(method); + return CompletableFuture.completedFuture(null); + } + }; + + var server = new ServerRpc(caller); + server.models.list(); + + assertEquals("models.list", capturedMethod.get()); + } + + @Test + void serverRpc_account_getQuota_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + server.account.getQuota(); + + assertEquals(1, stub.calls.size()); + assertEquals("account.getQuota", stub.calls.get(0).method()); + } + + // ── CopilotSession.getRpc() wiring tests ────────────────────────────────── + // These tests use a socket-pair backed JsonRpcClient (same pattern as + // RpcHandlerDispatcherTest) to construct a real CopilotSession and verify + // that getRpc() returns a correctly wired SessionRpc. + + @Test + void copilotSession_getRpc_returns_non_null_session_rpc() throws Exception { + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var session = new CopilotSession("sess-unit", rpc); + + assertNotNull(session.getRpc()); + } + } + + @Test + void copilotSession_getRpc_sessionId_matches_session() throws Exception { + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var stub = sockets.stubServer(); + var session = new CopilotSession("sess-test-id", rpc); + + // Call any no-arg session method via getRpc() to verify sessionId injection + session.getRpc().agent.list(); + + // Drain the sent message from the stub server + var sent = stub.readOneMessage(); + assertEquals("session.agent.list", sent.get("method").asText()); + assertEquals("sess-test-id", sent.get("params").get("sessionId").asText()); + } + } + + @Test + void copilotSession_getRpc_updates_when_sessionId_changes() throws Exception { + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var stub = sockets.stubServer(); + var session = new CopilotSession("old-id", rpc); + + // Simulate server returning a different sessionId (v2 CLI behaviour) + session.setActiveSessionId("new-id"); + + session.getRpc().agent.list(); + + var sent = stub.readOneMessage(); + assertEquals("new-id", sent.get("params").get("sessionId").asText(), + "getRpc() should reflect the updated sessionId"); + } + } + + @Test + void copilotSession_getRpc_all_namespace_fields_present() throws Exception { + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var session = new CopilotSession("sess-ns", rpc); + + var sessionRpc = session.getRpc(); + assertNotNull(sessionRpc.model); + assertNotNull(sessionRpc.agent); + assertNotNull(sessionRpc.skills); + assertNotNull(sessionRpc.tools); + assertNotNull(sessionRpc.permissions); + assertNotNull(sessionRpc.commands); + assertNotNull(sessionRpc.ui); + } + } + + @Test + void copilotSession_getRpc_is_lazy_and_cached() throws Exception { + // Verify lazy init: getRpc() returns the same instance on repeated calls + // (caches rather than allocating a new SessionRpc per call). + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var session = new CopilotSession("sess-cache", rpc); + + var first = session.getRpc(); + var second = session.getRpc(); + assertNotNull(first); + assertSame(first, second, "getRpc() must return the cached instance when sessionId has not changed"); + } + } + + @Test + void copilotSession_getRpc_returns_new_instance_after_sessionId_change() throws Exception { + // Verify that after setActiveSessionId() the old cached instance is discarded + // and the next getRpc() call produces a fresh SessionRpc with the new ID. + try (var sockets = new SocketPair()) { + var rpc = sockets.client(); + var stub = sockets.stubServer(); + var session = new CopilotSession("old-id", rpc); + + var before = session.getRpc(); + session.setActiveSessionId("new-id"); + var after = session.getRpc(); + + assertNotNull(before); + assertNotNull(after); + assertNotSame(before, after, "getRpc() must return a new instance after sessionId changes"); + + // Confirm the new instance uses the new sessionId + after.agent.list(); + var sent = stub.readOneMessage(); + assertEquals("new-id", sent.get("params").get("sessionId").asText()); + } + } + + @Test + void copilotClient_getRpc_throws_before_start() { + // CopilotClient.getRpc() should throw before start() is called. + var client = new CopilotClient(); + assertThrows(IllegalStateException.class, client::getRpc, + "getRpc() must throw IllegalStateException if called before start()"); + } + + /** + * Helper that creates a loopback socket pair. The client side is used by + * {@link JsonRpcClient}; the server side can be read to inspect outbound + * messages. + */ + private static final class SocketPair implements AutoCloseable { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final java.net.Socket clientSocket; + private final java.net.Socket serverSocket; + private final JsonRpcClient rpcClient; + + SocketPair() throws Exception { + try (var ss = new java.net.ServerSocket(0)) { + clientSocket = new java.net.Socket("localhost", ss.getLocalPort()); + serverSocket = ss.accept(); + } + serverSocket.setSoTimeout(3000); + rpcClient = JsonRpcClient.fromSocket(clientSocket); + } + + JsonRpcClient client() { + return rpcClient; + } + + StubServer stubServer() { + return new StubServer(serverSocket); + } + + @Override + public void close() throws Exception { + rpcClient.close(); + clientSocket.close(); + serverSocket.close(); + } + } + + /** + * Reads raw JSON-RPC messages written to the server side of the socket. + */ + private static final class StubServer { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final java.io.InputStream in; + + StubServer(java.net.Socket socket) { + try { + this.in = socket.getInputStream(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Reads one JSON-RPC message (Content-Length framed) from the stream. + */ + com.fasterxml.jackson.databind.JsonNode readOneMessage() throws Exception { + // Read Content-Length header + var header = new StringBuilder(); + int b; + while ((b = in.read()) != -1) { + if (b == '\n' && header.toString().endsWith("\r")) { + break; + } + header.append((char) b); + } + // Skip blank line + in.read(); // '\r' + in.read(); // '\n' + + String hdr = header.toString().trim(); + int colon = hdr.indexOf(':'); + int len = Integer.parseInt(hdr.substring(colon + 1).trim()); + byte[] body = in.readNBytes(len); + return MAPPER.readTree(body); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SchedulerShutdownRaceTest.java b/java/src/test/java/com/github/copilot/sdk/SchedulerShutdownRaceTest.java new file mode 100644 index 000000000..e60e4aa34 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SchedulerShutdownRaceTest.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; + +/** + * Regression coverage for the race between {@code sendAndWait()} and + * {@code close()}. + *

+ * If {@code close()} shuts down the timeout scheduler after + * {@code ensureNotTerminated()} passes but before + * {@code timeoutScheduler.schedule()} executes, the schedule call throws + * {@link RejectedExecutionException}. This test asserts that + * {@code sendAndWait()} handles this race by returning a future that completes + * exceptionally (rather than propagating the exception to the caller or leaving + * the returned future incomplete). + */ +public class SchedulerShutdownRaceTest { + + @SuppressWarnings("unchecked") + @Test + void sendAndWaitShouldReturnFailedFutureWhenSchedulerIsShutDown() throws Exception { + // Build a session via reflection (package-private constructor) + var ctor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + ctor.setAccessible(true); + + // Mock JsonRpcClient so send() returns a pending future instead of NPE + var mockRpc = mock(JsonRpcClient.class); + when(mockRpc.invoke(any(), any(), any())).thenReturn(new CompletableFuture<>()); + + var session = ctor.newInstance("race-test", mockRpc, null); + + // Shut down the scheduler without setting isTerminated, + // simulating the race window between ensureNotTerminated() and schedule() + var schedulerField = CopilotSession.class.getDeclaredField("timeoutScheduler"); + schedulerField.setAccessible(true); + var scheduler = (ScheduledExecutorService) schedulerField.get(session); + scheduler.shutdownNow(); + + // sendAndWait must return a failed future rather than throwing directly. + CompletableFuture result = session.sendAndWait(new MessageOptions().setPrompt("test"), 5000); + + assertNotNull(result, "sendAndWait should return a future, not throw"); + var ex = assertThrows(ExecutionException.class, () -> result.get(1, TimeUnit.SECONDS)); + assertInstanceOf(RejectedExecutionException.class, ex.getCause()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/java/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java new file mode 100644 index 000000000..4c2691d86 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ProviderConfig; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for session configuration features. + */ +public class SessionConfigE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnCreate() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_create"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-create-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-create-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldApplyInstructionDirectoriesOnResume() throws Exception { + ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_resume"); + + // Set up instruction directory with a custom instruction file + Path projectDir = ctx.getWorkDir().resolve("instruction-resume-project"); + Path instructionDir = ctx.getWorkDir().resolve("extra-resume-instructions"); + Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions"); + String sentinel = "JAVA_RESUME_INSTRUCTION_DIRECTORIES_SENTINEL"; + Files.createDirectories(projectDir); + Files.createDirectories(instructionFilesDir); + Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + "."); + + try (CopilotClient client = ctx.createClient()) { + // Create a session first + CopilotSession session1 = client.createSession(new SessionConfig() + .setWorkingDirectory(projectDir.toString()).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + // Resume with instructionDirectories + CopilotSession session2 = client.resumeSession(session1.getSessionId(), + new ResumeSessionConfig().setWorkingDirectory(projectDir.toString()) + .setInstructionDirectories(List.of(instructionDir.toString())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session2.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + String systemMessage = getSystemMessage(exchanges.get(0)); + assertNotNull(systemMessage, "System message should not be null"); + assertTrue(systemMessage.contains(sentinel), + "System message should contain the instruction sentinel: " + sentinel); + } + } + + @Test + void testShouldForwardProviderWireModel() throws Exception { + ctx.configureForTest("session_config", "should_forward_provider_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setWireModel("test-wire-model") + .setMaxOutputTokens(1024)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("test-wire-model", request.get("model")); + } + } + + @Test + void testShouldUseProviderModelIdAsWireModel() throws Exception { + ctx.configureForTest("session_config", "should_use_provider_model_id_as_wire_model"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) + .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); + + List> exchanges = ctx.getExchanges(); + assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); + @SuppressWarnings("unchecked") + Map request = (Map) exchanges.get(0).get("request"); + assertEquals("claude-sonnet-4.5", request.get("model")); + } + } + + @SuppressWarnings("unchecked") + private static String getSystemMessage(Map exchange) { + // The exchange structure is: { request: { messages: [...] }, response: ..., + // requestHeaders: ... } + Object requestObj = exchange.get("request"); + if (!(requestObj instanceof Map request)) { + return null; + } + Object messagesObj = request.get("messages"); + if (messagesObj instanceof List messages) { + for (Object msg : messages) { + if (msg instanceof Map msgMap) { + if ("system".equals(msgMap.get("role"))) { + Object content = msgMap.get("content"); + return content != null ? content.toString() : null; + } + } + } + } + return null; + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java b/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java new file mode 100644 index 000000000..08edfa5fa --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionEventDeserializationTest.java @@ -0,0 +1,2562 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.sdk.generated.*; + +/** + * Tests for session event deserialization. + *

+ * These are unit tests that verify JSON deserialization works correctly for all + * event types supported by the SDK. + *

+ */ +public class SessionEventDeserializationTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + /** + * Helper to parse a JSON string directly to a {@link SessionEvent}. + */ + private static SessionEvent parseJson(String json) throws Exception { + return MAPPER.readValue(json, SessionEvent.class); + } + + // ========================================================================= + // Session Events + // ========================================================================= + + @Test + void testParseSessionStartEvent() throws Exception { + String json = """ + { + "type": "session.start", + "data": { + "sessionId": "sess-123", + "model": "gpt-4" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionStartEvent.class, event); + assertEquals("session.start", event.getType()); + + var startEvent = (SessionStartEvent) event; + assertEquals("sess-123", startEvent.getData().sessionId()); + } + + @Test + void testParseSessionResumeEvent() throws Exception { + String json = """ + { + "type": "session.resume", + "data": { + "sessionId": "sess-456" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionResumeEvent.class, event); + assertEquals("session.resume", event.getType()); + } + + @Test + void testParseSessionErrorEvent() throws Exception { + String json = """ + { + "type": "session.error", + "data": { + "errorType": "RateLimitError", + "message": "Rate limit exceeded", + "stack": "Error: Rate limit exceeded\\n at processRequest" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionErrorEvent.class, event); + assertEquals("session.error", event.getType()); + + var errorEvent = (SessionErrorEvent) event; + assertEquals("RateLimitError", errorEvent.getData().errorType()); + assertEquals("Rate limit exceeded", errorEvent.getData().message()); + assertNotNull(errorEvent.getData().stack()); + } + + @Test + void testParseSessionIdleEvent() throws Exception { + String json = """ + { + "type": "session.idle", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionIdleEvent.class, event); + assertEquals("session.idle", event.getType()); + } + + @Test + void testParseSessionInfoEvent() throws Exception { + String json = """ + { + "type": "session.info", + "data": { + "infoType": "status", + "message": "Processing request" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionInfoEvent.class, event); + assertEquals("session.info", event.getType()); + + var infoEvent = (SessionInfoEvent) event; + assertEquals("status", infoEvent.getData().infoType()); + assertEquals("Processing request", infoEvent.getData().message()); + } + + @Test + void testParseSessionModelChangeEvent() throws Exception { + String json = """ + { + "type": "session.model_change", + "data": { + "previousModel": "gpt-4", + "newModel": "gpt-4-turbo" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionModelChangeEvent.class, event); + assertEquals("session.model_change", event.getType()); + } + + @Test + void testParseSessionModeChangedEvent() throws Exception { + String json = """ + { + "type": "session.mode_changed", + "data": { + "previousMode": "interactive", + "newMode": "plan" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionModeChangedEvent.class, event); + assertEquals("session.mode_changed", event.getType()); + } + + @Test + void testParseSessionPlanChangedEvent() throws Exception { + String json = """ + { + "type": "session.plan_changed", + "data": { + "operation": "update" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionPlanChangedEvent.class, event); + assertEquals("session.plan_changed", event.getType()); + } + + @Test + void testParseSessionWorkspaceFileChangedEvent() throws Exception { + String json = """ + { + "type": "session.workspace_file_changed", + "data": { + "path": "plan.md", + "operation": "create" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionWorkspaceFileChangedEvent.class, event); + assertEquals("session.workspace_file_changed", event.getType()); + } + + @Test + void testParseSessionHandoffEvent() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "targetAgent": "code-review-agent" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionHandoffEvent.class, event); + assertEquals("session.handoff", event.getType()); + } + + @Test + void testParseSessionTruncationEvent() throws Exception { + String json = """ + { + "type": "session.truncation", + "data": { + "reason": "context_limit" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionTruncationEvent.class, event); + assertEquals("session.truncation", event.getType()); + } + + @Test + void testParseSessionSnapshotRewindEvent() throws Exception { + String json = """ + { + "type": "session.snapshot_rewind", + "data": { + "snapshotId": "snap-123" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionSnapshotRewindEvent.class, event); + assertEquals("session.snapshot_rewind", event.getType()); + } + + @Test + void testParseSessionUsageInfoEvent() throws Exception { + String json = """ + { + "type": "session.usage_info", + "data": { + "tokenCount": 1500 + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionUsageInfoEvent.class, event); + assertEquals("session.usage_info", event.getType()); + } + + @Test + void testParseSessionCompactionStartEvent() throws Exception { + String json = """ + { + "type": "session.compaction_start", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionCompactionStartEvent.class, event); + assertEquals("session.compaction_start", event.getType()); + } + + @Test + void testParseSessionCompactionCompleteEvent() throws Exception { + String json = """ + { + "type": "session.compaction_complete", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionCompactionCompleteEvent.class, event); + assertEquals("session.compaction_complete", event.getType()); + } + + // ========================================================================= + // User Events + // ========================================================================= + + @Test + void testParseUserMessageEvent() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "messageId": "msg-123", + "content": "Hello, Copilot!" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(UserMessageEvent.class, event); + assertEquals("user.message", event.getType()); + } + + @Test + void testParsePendingMessagesModifiedEvent() throws Exception { + String json = """ + { + "type": "pending_messages.modified", + "data": { + "count": 3 + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(PendingMessagesModifiedEvent.class, event); + assertEquals("pending_messages.modified", event.getType()); + } + + // ========================================================================= + // Assistant Events + // ========================================================================= + + @Test + void testParseAssistantTurnStartEvent() throws Exception { + String json = """ + { + "type": "assistant.turn_start", + "data": { + "turnId": "turn-123" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantTurnStartEvent.class, event); + assertEquals("assistant.turn_start", event.getType()); + + var turnEvent = (AssistantTurnStartEvent) event; + assertEquals("turn-123", turnEvent.getData().turnId()); + } + + @Test + void testParseAssistantIntentEvent() throws Exception { + String json = """ + { + "type": "assistant.intent", + "data": { + "intent": "code_generation" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantIntentEvent.class, event); + assertEquals("assistant.intent", event.getType()); + } + + @Test + void testParseAssistantReasoningEvent() throws Exception { + String json = """ + { + "type": "assistant.reasoning", + "data": { + "reasoningId": "reason-123", + "content": "Analyzing the code structure..." + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantReasoningEvent.class, event); + assertEquals("assistant.reasoning", event.getType()); + + var reasoningEvent = (AssistantReasoningEvent) event; + assertEquals("reason-123", reasoningEvent.getData().reasoningId()); + assertEquals("Analyzing the code structure...", reasoningEvent.getData().content()); + } + + @Test + void testParseAssistantReasoningDeltaEvent() throws Exception { + String json = """ + { + "type": "assistant.reasoning_delta", + "data": { + "reasoningId": "reason-123", + "delta": "Considering options..." + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantReasoningDeltaEvent.class, event); + assertEquals("assistant.reasoning_delta", event.getType()); + } + + @Test + void testParseAssistantMessageEvent() throws Exception { + String json = """ + { + "type": "assistant.message", + "data": { + "messageId": "msg-456", + "content": "Here is the code you requested." + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantMessageEvent.class, event); + assertEquals("assistant.message", event.getType()); + + var msgEvent = (AssistantMessageEvent) event; + assertEquals("Here is the code you requested.", msgEvent.getData().content()); + } + + @Test + void testParseAssistantMessageDeltaEvent() throws Exception { + String json = """ + { + "type": "assistant.message_delta", + "data": { + "messageId": "msg-456", + "delta": "Here is" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantMessageDeltaEvent.class, event); + assertEquals("assistant.message_delta", event.getType()); + } + + @Test + void testParseAssistantTurnEndEvent() throws Exception { + String json = """ + { + "type": "assistant.turn_end", + "data": { + "turnId": "turn-123" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantTurnEndEvent.class, event); + assertEquals("assistant.turn_end", event.getType()); + } + + @Test + void testParseAssistantUsageEvent() throws Exception { + String json = """ + { + "type": "assistant.usage", + "data": { + "promptTokens": 100, + "completionTokens": 50, + "totalTokens": 150 + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AssistantUsageEvent.class, event); + assertEquals("assistant.usage", event.getType()); + } + + // ========================================================================= + // Tool Events + // ========================================================================= + + @Test + void testParseToolUserRequestedEvent() throws Exception { + String json = """ + { + "type": "tool.user_requested", + "data": { + "toolName": "read_file", + "userRequest": "Please read the config file" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ToolUserRequestedEvent.class, event); + assertEquals("tool.user_requested", event.getType()); + } + + @Test + void testParseToolExecutionStartEvent() throws Exception { + String json = """ + { + "type": "tool.execution_start", + "data": { + "toolCallId": "call-123", + "toolName": "read_file" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ToolExecutionStartEvent.class, event); + assertEquals("tool.execution_start", event.getType()); + } + + @Test + void testParseToolExecutionPartialResultEvent() throws Exception { + String json = """ + { + "type": "tool.execution_partial_result", + "data": { + "toolCallId": "call-123", + "partialResult": "Reading file..." + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ToolExecutionPartialResultEvent.class, event); + assertEquals("tool.execution_partial_result", event.getType()); + } + + @Test + void testParseToolExecutionProgressEvent() throws Exception { + String json = """ + { + "type": "tool.execution_progress", + "data": { + "toolCallId": "call-123", + "progress": 50 + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ToolExecutionProgressEvent.class, event); + assertEquals("tool.execution_progress", event.getType()); + } + + @Test + void testParseToolExecutionCompleteEvent() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "call-123", + "success": true, + "result": { + "type": "text", + "content": "File contents here" + } + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ToolExecutionCompleteEvent.class, event); + assertEquals("tool.execution_complete", event.getType()); + + var completeEvent = (ToolExecutionCompleteEvent) event; + assertTrue(completeEvent.getData().success()); + } + + // ========================================================================= + // Subagent Events + // ========================================================================= + + @Test + void testParseSubagentStartedEvent() throws Exception { + String json = """ + { + "type": "subagent.started", + "data": { + "toolCallId": "call-789", + "agentName": "code-review", + "agentDisplayName": "Code Review Agent", + "agentDescription": "Reviews code for best practices" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentStartedEvent.class, event); + assertEquals("subagent.started", event.getType()); + + var startedEvent = (SubagentStartedEvent) event; + assertEquals("code-review", startedEvent.getData().agentName()); + assertEquals("Code Review Agent", startedEvent.getData().agentDisplayName()); + } + + @Test + void testParseSubagentCompletedEvent() throws Exception { + String json = """ + { + "type": "subagent.completed", + "data": { + "toolCallId": "call-789", + "result": "Review completed successfully" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentCompletedEvent.class, event); + assertEquals("subagent.completed", event.getType()); + } + + @Test + void testParseSubagentFailedEvent() throws Exception { + String json = """ + { + "type": "subagent.failed", + "data": { + "toolCallId": "call-789", + "error": "Agent timeout" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentFailedEvent.class, event); + assertEquals("subagent.failed", event.getType()); + } + + @Test + void testParseSubagentSelectedEvent() throws Exception { + String json = """ + { + "type": "subagent.selected", + "data": { + "agentName": "documentation-agent" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentSelectedEvent.class, event); + assertEquals("subagent.selected", event.getType()); + } + + // ========================================================================= + // Hook Events + // ========================================================================= + + @Test + void testParseHookStartEvent() throws Exception { + String json = """ + { + "type": "hook.start", + "data": { + "hookInvocationId": "hook-123", + "hookType": "preToolUse", + "input": {"toolName": "read_file"} + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(HookStartEvent.class, event); + assertEquals("hook.start", event.getType()); + + var hookEvent = (HookStartEvent) event; + assertEquals("hook-123", hookEvent.getData().hookInvocationId()); + assertEquals("preToolUse", hookEvent.getData().hookType()); + } + + @Test + void testParseHookEndEvent() throws Exception { + String json = """ + { + "type": "hook.end", + "data": { + "hookInvocationId": "hook-123", + "success": true + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(HookEndEvent.class, event); + assertEquals("hook.end", event.getType()); + } + + // ========================================================================= + // Other Events + // ========================================================================= + + @Test + void testParseAbortEvent() throws Exception { + String json = """ + { + "type": "abort", + "data": { + "reason": "user_initiated" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(AbortEvent.class, event); + assertEquals("abort", event.getType()); + } + + @Test + void testParseSystemMessageEvent() throws Exception { + String json = """ + { + "type": "system.message", + "data": { + "content": "System is ready" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SystemMessageEvent.class, event); + assertEquals("system.message", event.getType()); + } + + @Test + void testParseSessionShutdownEvent() throws Exception { + String json = """ + { + "type": "session.shutdown", + "data": { + "shutdownType": "routine", + "totalPremiumRequests": 5, + "totalApiDurationMs": 1234.5, + "sessionStartTime": 1612345678000, + "codeChanges": { + "linesAdded": 10, + "linesRemoved": 3, + "filesModified": ["file1.java", "file2.java"] + }, + "modelMetrics": {}, + "currentModel": "gpt-4" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionShutdownEvent.class, event); + assertEquals("session.shutdown", event.getType()); + + var shutdownEvent = (SessionShutdownEvent) event; + assertEquals(ShutdownType.ROUTINE, shutdownEvent.getData().shutdownType()); + assertEquals(5.0, shutdownEvent.getData().totalPremiumRequests()); + assertEquals("gpt-4", shutdownEvent.getData().currentModel()); + assertNotNull(shutdownEvent.getData().codeChanges()); + assertEquals(10.0, shutdownEvent.getData().codeChanges().linesAdded()); + } + + @Test + void testParseSkillInvokedEvent() throws Exception { + String json = """ + { + "type": "skill.invoked", + "data": { + "name": "code-review", + "path": "/path/to/skill", + "content": "Skill instructions here", + "allowedTools": ["view", "edit", "grep"] + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SkillInvokedEvent.class, event); + assertEquals("skill.invoked", event.getType()); + + var skillEvent = (SkillInvokedEvent) event; + assertEquals("code-review", skillEvent.getData().name()); + assertEquals("/path/to/skill", skillEvent.getData().path()); + assertEquals("Skill instructions here", skillEvent.getData().content()); + assertNotNull(skillEvent.getData().allowedTools()); + assertEquals(3, skillEvent.getData().allowedTools().size()); + } + + // ========================================================================= + // Edge Cases + // ========================================================================= + + @Test + void testParseUnknownEventType() throws Exception { + // Unknown types log at FINE level, no need to suppress + String json = """ + { + "type": "unknown.event.type", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event, "Unknown event types should return an UnknownSessionEvent"); + assertInstanceOf(com.github.copilot.sdk.generated.UnknownSessionEvent.class, event, + "Unknown event types should return UnknownSessionEvent for forward compatibility"); + assertEquals("unknown.event.type", event.getType(), + "UnknownSessionEvent should preserve the original type from JSON"); + } + + @Test + void testParseMissingTypeField() throws Exception { + String json = """ + { + "data": { + "content": "Hello" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event, "Events without type field should return UnknownSessionEvent"); + assertInstanceOf(com.github.copilot.sdk.generated.UnknownSessionEvent.class, event); + } + + @Test + void testParseEventWithUnknownFields() throws Exception { + // Should not fail when there are extra unknown fields + String json = """ + { + "type": "session.idle", + "data": { + "unknownField": "value", + "anotherUnknown": 123 + }, + "extraTopLevel": true + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event, "Events with unknown fields should still parse"); + assertInstanceOf(SessionIdleEvent.class, event); + } + + @Test + void testParseEmptyJson() throws Exception { + String json = "{}"; + + SessionEvent event = parseJson(json); + assertNotNull(event, "Empty JSON should return UnknownSessionEvent"); + assertInstanceOf(com.github.copilot.sdk.generated.UnknownSessionEvent.class, event); + } + + // ========================================================================= + // All event types in one test + // ========================================================================= + + @Test + void testParseAllEventTypes() throws Exception { + String[] types = {"session.start", "session.resume", "session.error", "session.idle", "session.info", + "session.model_change", "session.mode_changed", "session.plan_changed", + "session.workspace_file_changed", "session.handoff", "session.truncation", "session.snapshot_rewind", + "session.usage_info", "session.compaction_start", "session.compaction_complete", "user.message", + "pending_messages.modified", "assistant.turn_start", "assistant.intent", "assistant.reasoning", + "assistant.reasoning_delta", "assistant.message", "assistant.message_delta", "assistant.turn_end", + "assistant.usage", "abort", "tool.user_requested", "tool.execution_start", + "tool.execution_partial_result", "tool.execution_progress", "tool.execution_complete", + "subagent.started", "subagent.completed", "subagent.failed", "subagent.selected", "hook.start", + "hook.end", "system.message", "session.shutdown", "skill.invoked"}; + + for (String type : types) { + String json = """ + { + "type": "%s", + "data": {} + } + """.formatted(type); + SessionEvent event = parseJson(json); + assertNotNull(event, "Event type '%s' should parse".formatted(type)); + assertEquals(type, event.getType(), "Parsed type should match for '%s'".formatted(type)); + } + } + + // ========================================================================= + // SessionEvent base fields + // ========================================================================= + + @Test + void testParseBaseFieldsId() throws Exception { + String uuid = "550e8400-e29b-41d4-a716-446655440000"; + String json = """ + { + "type": "session.idle", + "id": "%s", + "data": {} + } + """.formatted(uuid); + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(uuid), event.getId()); + } + + @Test + void testParseBaseFieldsParentId() throws Exception { + String parentUuid = "660e8400-e29b-41d4-a716-446655440001"; + String json = """ + { + "type": "session.idle", + "parentId": "%s", + "data": {} + } + """.formatted(parentUuid); + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(parentUuid), event.getParentId()); + } + + @Test + void testParseBaseFieldsEphemeral() throws Exception { + String json = """ + { + "type": "session.idle", + "ephemeral": true, + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertTrue(event.getEphemeral()); + } + + @Test + void testParseBaseFieldsTimestamp() throws Exception { + String json = """ + { + "type": "session.idle", + "timestamp": "2025-01-15T10:30:00Z", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertNotNull(event.getTimestamp()); + } + + @Test + void testParseBaseFieldsAllTogether() throws Exception { + String uuid = "550e8400-e29b-41d4-a716-446655440000"; + String parentUuid = "660e8400-e29b-41d4-a716-446655440001"; + String json = """ + { + "type": "assistant.message", + "id": "%s", + "parentId": "%s", + "ephemeral": false, + "timestamp": "2025-06-15T12:00:00+02:00", + "data": { + "content": "Hello" + } + } + """.formatted(uuid, parentUuid); + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString(uuid), event.getId()); + assertEquals(UUID.fromString(parentUuid), event.getParentId()); + assertFalse(event.getEphemeral()); + assertNotNull(event.getTimestamp()); + assertInstanceOf(AssistantMessageEvent.class, event); + assertEquals("Hello", ((AssistantMessageEvent) event).getData().content()); + } + + @Test + void testParseBaseFieldsNullWhenAbsent() throws Exception { + String json = """ + { + "type": "session.idle", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertNull(event.getId()); + assertNull(event.getParentId()); + assertNull(event.getEphemeral()); + assertNull(event.getTimestamp()); + } + + // ========================================================================= + // Rich data field assertions + // ========================================================================= + + @Test + void testSessionStartEventAllFields() throws Exception { + String json = """ + { + "type": "session.start", + "data": { + "sessionId": "sess-full", + "version": 2.0, + "producer": "copilot-cli", + "copilotVersion": "1.2.3", + "startTime": "2025-03-01T08:00:00Z", + "selectedModel": "gpt-4-turbo" + } + } + """; + + var event = (SessionStartEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("sess-full", data.sessionId()); + assertEquals(2.0, data.version()); + assertEquals("copilot-cli", data.producer()); + assertEquals("1.2.3", data.copilotVersion()); + assertNotNull(data.startTime()); + assertEquals("gpt-4-turbo", data.selectedModel()); + } + + @Test + void testSessionResumeEventAllFields() throws Exception { + String json = """ + { + "type": "session.resume", + "data": { + "resumeTime": "2025-04-10T09:30:00Z", + "eventCount": 42 + } + } + """; + + var event = (SessionResumeEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertNotNull(data.resumeTime()); + assertEquals(42.0, data.eventCount()); + } + + @Test + void testSessionErrorEventAllFields() throws Exception { + String json = """ + { + "type": "session.error", + "data": { + "errorType": "InternalError", + "message": "Something went wrong", + "stack": "at line 42", + "statusCode": 500, + "providerCallId": "prov-err-1" + } + } + """; + + var event = (SessionErrorEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("InternalError", data.errorType()); + assertEquals("Something went wrong", data.message()); + assertEquals("at line 42", data.stack()); + assertEquals(500, data.statusCode()); + assertEquals("prov-err-1", data.providerCallId()); + } + + @Test + void testSessionModelChangeEventAllFields() throws Exception { + String json = """ + { + "type": "session.model_change", + "data": { + "previousModel": "gpt-4", + "newModel": "gpt-4o" + } + } + """; + + var event = (SessionModelChangeEvent) parseJson(json); + assertNotNull(event); + assertEquals("gpt-4", event.getData().previousModel()); + assertEquals("gpt-4o", event.getData().newModel()); + } + + @Test + void testSessionHandoffEventAllFields() throws Exception { + String json = """ + { + "type": "session.handoff", + "data": { + "handoffTime": "2025-05-01T10:00:00Z", + "sourceType": "remote", + "repository": { + "owner": "my-org", + "name": "my-repo", + "branch": "main" + }, + "context": "additional context", + "summary": "handoff summary", + "remoteSessionId": "remote-sess-1" + } + } + """; + + var event = (SessionHandoffEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertNotNull(data.handoffTime()); + assertEquals(HandoffSourceType.REMOTE, data.sourceType()); + assertEquals("additional context", data.context()); + assertEquals("handoff summary", data.summary()); + assertEquals("remote-sess-1", data.remoteSessionId()); + assertNotNull(data.repository()); + assertEquals("my-org", data.repository().owner()); + assertEquals("my-repo", data.repository().name()); + assertEquals("main", data.repository().branch()); + } + + @Test + void testSessionTruncationEventAllFields() throws Exception { + String json = """ + { + "type": "session.truncation", + "data": { + "tokenLimit": 128000, + "preTruncationTokensInMessages": 150000, + "preTruncationMessagesLength": 100, + "postTruncationTokensInMessages": 120000, + "postTruncationMessagesLength": 80, + "tokensRemovedDuringTruncation": 30000, + "messagesRemovedDuringTruncation": 20, + "performedBy": "system" + } + } + """; + + var event = (SessionTruncationEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(150000.0, data.preTruncationTokensInMessages()); + assertEquals(100.0, data.preTruncationMessagesLength()); + assertEquals(120000.0, data.postTruncationTokensInMessages()); + assertEquals(80.0, data.postTruncationMessagesLength()); + assertEquals(30000.0, data.tokensRemovedDuringTruncation()); + assertEquals(20.0, data.messagesRemovedDuringTruncation()); + assertEquals("system", data.performedBy()); + } + + @Test + void testSessionUsageInfoEventAllFields() throws Exception { + String json = """ + { + "type": "session.usage_info", + "data": { + "tokenLimit": 128000, + "currentTokens": 50000, + "messagesLength": 25 + } + } + """; + + var event = (SessionUsageInfoEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(128000.0, data.tokenLimit()); + assertEquals(50000.0, data.currentTokens()); + assertEquals(25.0, data.messagesLength()); + } + + @Test + void testSessionCompactionCompleteEventAllFields() throws Exception { + String json = """ + { + "type": "session.compaction_complete", + "data": { + "success": true, + "error": null, + "preCompactionTokens": 150000.0, + "postCompactionTokens": 60000.0, + "preCompactionMessagesLength": 100.0, + "messagesRemoved": 50.0, + "tokensRemoved": 90000.0, + "summaryContent": "Compacted conversation", + "checkpointNumber": 3.0, + "checkpointPath": "/checkpoints/3", + "compactionTokensUsed": { + "inputTokens": 1000, + "outputTokens": 500, + "cacheReadTokens": 200 + }, + "requestId": "req-compact-1" + } + } + """; + + var event = (SessionCompactionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertTrue(data.success()); + assertNull(data.error()); + assertEquals(150000.0, data.preCompactionTokens()); + assertEquals(60000.0, data.postCompactionTokens()); + assertEquals(100.0, data.preCompactionMessagesLength()); + assertEquals(50.0, data.messagesRemoved()); + assertEquals(90000.0, data.tokensRemoved()); + assertEquals("Compacted conversation", data.summaryContent()); + assertEquals(3.0, data.checkpointNumber()); + assertEquals("/checkpoints/3", data.checkpointPath()); + assertEquals("req-compact-1", data.requestId()); + + var tokens = data.compactionTokensUsed(); + assertNotNull(tokens); + assertEquals(1000.0, tokens.inputTokens()); + assertEquals(500.0, tokens.outputTokens()); + assertEquals(200.0, tokens.cacheReadTokens()); + } + + @Test + void testSessionShutdownEventAllFields() throws Exception { + String json = """ + { + "type": "session.shutdown", + "data": { + "shutdownType": "error", + "errorReason": "OOM", + "totalPremiumRequests": 10, + "totalApiDurationMs": 5000.5, + "sessionStartTime": 1700000000000, + "codeChanges": { + "linesAdded": 50, + "linesRemoved": 20, + "filesModified": ["a.java", "b.java", "c.java"] + }, + "modelMetrics": { + "gpt-4": { + "requests": {"count": 5.0, "cost": 2.5} + } + }, + "currentModel": "gpt-4-turbo" + } + } + """; + + var event = (SessionShutdownEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals(ShutdownType.ERROR, data.shutdownType()); + assertEquals("OOM", data.errorReason()); + assertEquals(10.0, data.totalPremiumRequests()); + assertEquals(5000.5, data.totalApiDurationMs()); + assertEquals(1700000000000.0, data.sessionStartTime()); + assertEquals("gpt-4-turbo", data.currentModel()); + assertNotNull(data.modelMetrics()); + + var changes = data.codeChanges(); + assertNotNull(changes); + assertEquals(50.0, changes.linesAdded()); + assertEquals(20.0, changes.linesRemoved()); + assertNotNull(changes.filesModified()); + assertEquals(3, changes.filesModified().size()); + assertEquals("a.java", changes.filesModified().get(0)); + } + + // ========================================================================= + // Assistant events - rich field assertions + // ========================================================================= + + @Test + void testAssistantMessageEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.message", + "data": { + "messageId": "msg-rich", + "content": "Full response", + "toolRequests": [ + { + "toolCallId": "tc-1", + "name": "read_file", + "arguments": {"path": "/tmp/file.txt"} + }, + { + "toolCallId": "tc-2", + "name": "write_file", + "arguments": {"path": "/tmp/out.txt", "content": "hello"} + } + ], + "parentToolCallId": "parent-tc", + "interactionId": "interaction-msg-1", + "reasoningOpaque": "opaque-data", + "reasoningText": "My reasoning", + "encryptedContent": "enc123" + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("msg-rich", data.messageId()); + assertEquals("Full response", data.content()); + assertEquals("parent-tc", data.parentToolCallId()); + assertEquals("interaction-msg-1", data.interactionId()); + assertEquals("opaque-data", data.reasoningOpaque()); + assertEquals("My reasoning", data.reasoningText()); + assertEquals("enc123", data.encryptedContent()); + + assertNotNull(data.toolRequests()); + assertEquals(2, data.toolRequests().size()); + assertEquals("tc-1", data.toolRequests().get(0).toolCallId()); + assertEquals("read_file", data.toolRequests().get(0).name()); + assertNotNull(data.toolRequests().get(0).arguments()); + assertEquals("tc-2", data.toolRequests().get(1).toolCallId()); + assertEquals("write_file", data.toolRequests().get(1).name()); + } + + @Test + void testAssistantMessageDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.message_delta", + "data": { + "messageId": "msg-delta-1", + "deltaContent": "partial text", + "parentToolCallId": "ptc-1" + } + } + """; + + var event = (AssistantMessageDeltaEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("msg-delta-1", data.messageId()); + assertEquals("partial text", data.deltaContent()); + assertEquals("ptc-1", data.parentToolCallId()); + } + + @Test + void testAssistantStreamingDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.streaming_delta", + "data": { + "totalResponseSizeBytes": 4096.0 + } + } + """; + + var event = (AssistantStreamingDeltaEvent) parseJson(json); + assertNotNull(event); + assertEquals("assistant.streaming_delta", event.getType()); + assertEquals(4096.0, event.getData().totalResponseSizeBytes()); + } + + @Test + void testAssistantMessageEventIncludesInteractionId() throws Exception { + String json = """ + { + "type": "assistant.message", + "data": { + "messageId": "msg-with-interaction", + "content": "Response", + "interactionId": "interaction-abc-123" + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals("interaction-abc-123", event.getData().interactionId()); + } + + @Test + void testAssistantTurnStartEventIncludesInteractionId() throws Exception { + String json = """ + { + "type": "assistant.turn_start", + "data": { + "turnId": "turn-with-interaction", + "interactionId": "interaction-xyz-456" + } + } + """; + + var event = (AssistantTurnStartEvent) parseJson(json); + assertNotNull(event); + assertEquals("turn-with-interaction", event.getData().turnId()); + assertEquals("interaction-xyz-456", event.getData().interactionId()); + } + + @Test + void testAssistantUsageEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.usage", + "data": { + "model": "gpt-4-turbo", + "inputTokens": 500, + "outputTokens": 200, + "cacheReadTokens": 50, + "cacheWriteTokens": 150, + "cost": 0.05, + "duration": 1234.5, + "initiator": "user", + "apiCallId": "api-1", + "providerCallId": "prov-1", + "parentToolCallId": "ptc-usage", + "quotaSnapshots": { + "premium": { + "entitlementRequests": 100.0, + "usedRequests": 25.0 + }, + "standard": { + "entitlementRequests": 500.0, + "usedRequests": 150.0 + } + }, + "copilotUsage": { + "totalNanoAiu": 1234567.0, + "tokenDetails": [ + { + "tokenType": "input", + "tokenCount": 500.0, + "batchSize": 100.0, + "costPerBatch": 0.001 + }, + { + "tokenType": "output", + "tokenCount": 200.0, + "batchSize": 100.0, + "costPerBatch": 0.002 + } + ] + } + } + } + """; + + var event = (AssistantUsageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("gpt-4-turbo", data.model()); + assertEquals(500.0, data.inputTokens()); + assertEquals(200.0, data.outputTokens()); + assertEquals(50.0, data.cacheReadTokens()); + assertEquals(150.0, data.cacheWriteTokens()); + assertEquals(0.05, data.cost()); + assertEquals(1234.5, data.duration()); + assertEquals("user", data.initiator()); + assertEquals("api-1", data.apiCallId()); + assertEquals("prov-1", data.providerCallId()); + assertEquals("ptc-usage", data.parentToolCallId()); + assertNotNull(data.quotaSnapshots()); + assertEquals(2, data.quotaSnapshots().size()); + + // Verify copilotUsage + assertNotNull(data.copilotUsage()); + assertEquals(1234567.0, data.copilotUsage().totalNanoAiu()); + assertNotNull(data.copilotUsage().tokenDetails()); + assertEquals(2, data.copilotUsage().tokenDetails().size()); + assertEquals("input", data.copilotUsage().tokenDetails().get(0).tokenType()); + assertEquals(500.0, data.copilotUsage().tokenDetails().get(0).tokenCount()); + assertEquals("output", data.copilotUsage().tokenDetails().get(1).tokenType()); + } + + @Test + void testAssistantUsageEventWithNullQuotaSnapshots() throws Exception { + String json = """ + { + "type": "assistant.usage", + "data": { + "model": "gpt-4-turbo", + "inputTokens": 500, + "outputTokens": 200 + } + } + """; + + var event = (AssistantUsageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("gpt-4-turbo", data.model()); + assertEquals(500.0, data.inputTokens()); + assertEquals(200.0, data.outputTokens()); + // quotaSnapshots is null when absent in JSON (generated class uses nullable + // fields) + assertNull(data.quotaSnapshots()); + } + + @Test + void testAssistantReasoningDeltaEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.reasoning_delta", + "data": { + "reasoningId": "r-delta-1", + "deltaContent": "thinking about..." + } + } + """; + + var event = (AssistantReasoningDeltaEvent) parseJson(json); + assertNotNull(event); + assertEquals("r-delta-1", event.getData().reasoningId()); + assertEquals("thinking about...", event.getData().deltaContent()); + } + + @Test + void testAssistantIntentEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.intent", + "data": { + "intent": "refactor_code" + } + } + """; + + var event = (AssistantIntentEvent) parseJson(json); + assertNotNull(event); + assertEquals("refactor_code", event.getData().intent()); + } + + @Test + void testAssistantTurnEndEventAllFields() throws Exception { + String json = """ + { + "type": "assistant.turn_end", + "data": { + "turnId": "turn-end-1" + } + } + """; + + var event = (AssistantTurnEndEvent) parseJson(json); + assertNotNull(event); + assertEquals("turn-end-1", event.getData().turnId()); + } + + // ========================================================================= + // Tool events - rich field assertions + // ========================================================================= + + @Test + void testToolExecutionStartEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_start", + "data": { + "toolCallId": "tc-start-1", + "toolName": "mcp_read_file", + "arguments": {"path": "/tmp/x.txt"}, + "mcpServerName": "filesystem", + "mcpToolName": "read_file", + "parentToolCallId": "ptc-exec" + } + } + """; + + var event = (ToolExecutionStartEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-start-1", data.toolCallId()); + assertEquals("mcp_read_file", data.toolName()); + assertNotNull(data.arguments()); + assertEquals("filesystem", data.mcpServerName()); + assertEquals("read_file", data.mcpToolName()); + assertEquals("ptc-exec", data.parentToolCallId()); + } + + @Test + void testToolExecutionCompleteEventWithError() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-err-1", + "success": false, + "model": "claude-3-5-sonnet", + "interactionId": "interaction-tool-1", + "isUserRequested": true, + "error": { + "message": "File not found", + "code": "ENOENT" + }, + "toolTelemetry": { + "duration": 50, + "retries": 0 + }, + "parentToolCallId": "ptc-complete" + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-err-1", data.toolCallId()); + assertFalse(data.success()); + assertEquals("claude-3-5-sonnet", data.model()); + assertEquals("interaction-tool-1", data.interactionId()); + assertTrue(data.isUserRequested()); + assertEquals("ptc-complete", data.parentToolCallId()); + + assertNotNull(data.error()); + assertEquals("File not found", data.error().message()); + assertEquals("ENOENT", data.error().code()); + + assertNotNull(data.toolTelemetry()); + assertEquals(2, data.toolTelemetry().size()); + } + + @Test + void testToolExecutionCompleteEventWithResult() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-res-1", + "success": true, + "result": { + "content": "file contents", + "detailedContent": "full detailed contents" + } + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertTrue(data.success()); + assertNotNull(data.result()); + assertEquals("file contents", data.result().content()); + assertEquals("full detailed contents", data.result().detailedContent()); + assertNull(data.error()); + } + + @Test + void testToolExecutionPartialResultEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_partial_result", + "data": { + "toolCallId": "tc-partial-1", + "partialOutput": "partial output data" + } + } + """; + + var event = (ToolExecutionPartialResultEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-partial-1", event.getData().toolCallId()); + assertEquals("partial output data", event.getData().partialOutput()); + } + + @Test + void testToolExecutionProgressEventAllFields() throws Exception { + String json = """ + { + "type": "tool.execution_progress", + "data": { + "toolCallId": "tc-prog-1", + "progressMessage": "50% done" + } + } + """; + + var event = (ToolExecutionProgressEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-prog-1", event.getData().toolCallId()); + assertEquals("50% done", event.getData().progressMessage()); + } + + @Test + void testToolUserRequestedEventAllFields() throws Exception { + String json = """ + { + "type": "tool.user_requested", + "data": { + "toolCallId": "tc-ur-1", + "toolName": "search_files", + "arguments": {"query": "TODO"} + } + } + """; + + var event = (ToolUserRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-ur-1", event.getData().toolCallId()); + assertEquals("search_files", event.getData().toolName()); + assertNotNull(event.getData().arguments()); + } + + // ========================================================================= + // User events - rich field assertions + // ========================================================================= + + @Test + void testUserMessageEventAllFieldsWithAttachments() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Please review this file", + "transformedContent": "Transformed: Please review this file", + "source": "editor", + "attachments": [ + { + "type": "file", + "path": "/src/Main.java", + "filePath": "/full/src/Main.java", + "displayName": "Main.java", + "text": "public class Main {}", + "selection": { + "start": { "line": 1, "character": 0 }, + "end": { "line": 5, "character": 10 } + } + } + ] + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("Please review this file", data.content()); + assertEquals("Transformed: Please review this file", data.transformedContent()); + assertEquals("editor", data.source()); + + assertNotNull(data.attachments()); + assertEquals(1, data.attachments().size()); + + @SuppressWarnings("unchecked") + var att = (java.util.Map) data.attachments().get(0); + assertEquals("file", att.get("type")); + assertEquals("/src/Main.java", att.get("path")); + assertEquals("/full/src/Main.java", att.get("filePath")); + assertEquals("Main.java", att.get("displayName")); + assertEquals("public class Main {}", att.get("text")); + + @SuppressWarnings("unchecked") + var selection = (java.util.Map) att.get("selection"); + assertNotNull(selection); + @SuppressWarnings("unchecked") + var selStart = (java.util.Map) selection.get("start"); + @SuppressWarnings("unchecked") + var selEnd = (java.util.Map) selection.get("end"); + assertNotNull(selStart); + assertNotNull(selEnd); + assertEquals(1, ((Number) selStart.get("line")).intValue()); + assertEquals(0, ((Number) selStart.get("character")).intValue()); + assertEquals(5, ((Number) selEnd.get("line")).intValue()); + assertEquals(10, ((Number) selEnd.get("character")).intValue()); + } + + @Test + void testUserMessageEventNoAttachments() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Simple message" + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals("Simple message", event.getData().content()); + assertNull(event.getData().attachments()); + } + + // ========================================================================= + // Subagent events - rich field assertions + // ========================================================================= + + @Test + void testSubagentStartedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.started", + "data": { + "toolCallId": "tc-sub-1", + "agentName": "test-agent", + "agentDisplayName": "Test Agent", + "agentDescription": "A test subagent" + } + } + """; + + var event = (SubagentStartedEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("tc-sub-1", data.toolCallId()); + assertEquals("test-agent", data.agentName()); + assertEquals("Test Agent", data.agentDisplayName()); + assertEquals("A test subagent", data.agentDescription()); + } + + @Test + void testSubagentCompletedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.completed", + "data": { + "toolCallId": "tc-sub-2", + "agentName": "reviewer" + } + } + """; + + var event = (SubagentCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-sub-2", event.getData().toolCallId()); + assertEquals("reviewer", event.getData().agentName()); + } + + @Test + void testSubagentFailedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.failed", + "data": { + "toolCallId": "tc-sub-3", + "agentName": "broken-agent", + "error": "Connection timeout" + } + } + """; + + var event = (SubagentFailedEvent) parseJson(json); + assertNotNull(event); + assertEquals("tc-sub-3", event.getData().toolCallId()); + assertEquals("broken-agent", event.getData().agentName()); + assertEquals("Connection timeout", event.getData().error()); + } + + @Test + void testSubagentSelectedEventAllFields() throws Exception { + String json = """ + { + "type": "subagent.selected", + "data": { + "agentName": "best-agent", + "agentDisplayName": "Best Agent", + "tools": ["read", "write", "search"] + } + } + """; + + var event = (SubagentSelectedEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("best-agent", data.agentName()); + assertEquals("Best Agent", data.agentDisplayName()); + assertNotNull(data.tools()); + assertEquals(3, data.tools().size()); + assertEquals("read", data.tools().get(0)); + assertEquals("write", data.tools().get(1)); + assertEquals("search", data.tools().get(2)); + } + + // ========================================================================= + // Hook events - rich field assertions + // ========================================================================= + + @Test + void testHookStartEventAllFields() throws Exception { + String json = """ + { + "type": "hook.start", + "data": { + "hookInvocationId": "hook-full-1", + "hookType": "postToolUse", + "input": {"toolName": "write_file", "result": "ok"} + } + } + """; + + var event = (HookStartEvent) parseJson(json); + assertNotNull(event); + assertEquals("hook-full-1", event.getData().hookInvocationId()); + assertEquals("postToolUse", event.getData().hookType()); + assertNotNull(event.getData().input()); + } + + @Test + void testHookEndEventWithError() throws Exception { + String json = """ + { + "type": "hook.end", + "data": { + "hookInvocationId": "hook-err-1", + "hookType": "preToolUse", + "output": null, + "success": false, + "error": { + "message": "Hook validation failed", + "stack": "at HookValidator.validate(line 10)" + } + } + } + """; + + var event = (HookEndEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("hook-err-1", data.hookInvocationId()); + assertEquals("preToolUse", data.hookType()); + assertFalse(data.success()); + assertNotNull(data.error()); + assertEquals("Hook validation failed", data.error().message()); + assertEquals("at HookValidator.validate(line 10)", data.error().stack()); + } + + @Test + void testHookEndEventSuccess() throws Exception { + String json = """ + { + "type": "hook.end", + "data": { + "hookInvocationId": "hook-ok-1", + "hookType": "preToolUse", + "output": "approved", + "success": true + } + } + """; + + var event = (HookEndEvent) parseJson(json); + assertNotNull(event); + assertTrue(event.getData().success()); + assertNull(event.getData().error()); + } + + // ========================================================================= + // Other events - rich field assertions + // ========================================================================= + + @Test + void testAbortEventAllFields() throws Exception { + String json = """ + { + "type": "abort", + "data": { + "reason": "user_abort" + } + } + """; + + var event = (AbortEvent) parseJson(json); + assertNotNull(event); + assertEquals(AbortReason.USER_ABORT, event.getData().reason()); + } + + @Test + void testSystemMessageEventAllFields() throws Exception { + String json = """ + { + "type": "system.message", + "data": { + "content": "System notification", + "type": "warning", + "metadata": { + "severity": "high", + "source": "rate-limiter" + } + } + } + """; + + var event = (SystemMessageEvent) parseJson(json); + assertNotNull(event); + var data = event.getData(); + assertEquals("System notification", data.content()); + // Note: "type" field in JSON is not mapped in generated class; metadata fields + // "severity"/"source" are ignored + assertNotNull(data); + } + + @Test + void testSessionInfoEventAllFields() throws Exception { + String json = """ + { + "type": "session.info", + "data": { + "infoType": "model_selection", + "message": "Using gpt-4-turbo for this task" + } + } + """; + + var event = (SessionInfoEvent) parseJson(json); + assertNotNull(event); + assertEquals("model_selection", event.getData().infoType()); + assertEquals("Using gpt-4-turbo for this task", event.getData().message()); + } + + // ========================================================================= + // Null / missing data scenarios + // ========================================================================= + + @Test + void testParseEventWithNullData() throws Exception { + String json = """ + { + "type": "session.idle", + "data": null + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionIdleEvent.class, event); + } + + @Test + void testParseEventWithMissingData() throws Exception { + String json = """ + { + "type": "session.idle" + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionIdleEvent.class, event); + } + + // ========================================================================= + // Additional data assertion tests + // ========================================================================= + + @Test + void testParseJsonNodeAssistantMessageWithFields() throws Exception { + String json = """ + { + "type": "assistant.message", + "id": "550e8400-e29b-41d4-a716-446655440000", + "ephemeral": true, + "data": { + "messageId": "msg-jn-1", + "content": "Hello from JsonNode", + "toolRequests": [ + { "toolCallId": "tc-jn", "name": "grep", "arguments": {} } + ] + } + } + """; + + var event = (AssistantMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), event.getId()); + assertTrue(event.getEphemeral()); + assertEquals("msg-jn-1", event.getData().messageId()); + assertEquals("Hello from JsonNode", event.getData().content()); + assertEquals(1, event.getData().toolRequests().size()); + assertEquals("tc-jn", event.getData().toolRequests().get(0).toolCallId()); + } + + @Test + void testParseJsonNodeToolExecutionCompleteWithNestedTypes() throws Exception { + String json = """ + { + "type": "tool.execution_complete", + "data": { + "toolCallId": "tc-jn-comp", + "success": false, + "error": { + "message": "Permission denied", + "code": "EPERM" + } + } + } + """; + + var event = (ToolExecutionCompleteEvent) parseJson(json); + assertNotNull(event); + assertFalse(event.getData().success()); + assertEquals("Permission denied", event.getData().error().message()); + assertEquals("EPERM", event.getData().error().code()); + } + + @Test + void testParseJsonNodeSessionShutdownWithCodeChanges() throws Exception { + String json = """ + { + "type": "session.shutdown", + "data": { + "shutdownType": "routine", + "totalPremiumRequests": 3, + "totalApiDurationMs": 999.9, + "codeChanges": { + "linesAdded": 100, + "linesRemoved": 50, + "filesModified": ["x.java"] + }, + "currentModel": "claude-4" + } + } + """; + + var event = (SessionShutdownEvent) parseJson(json); + assertNotNull(event); + assertEquals(ShutdownType.ROUTINE, event.getData().shutdownType()); + assertEquals(100.0, event.getData().codeChanges().linesAdded()); + assertEquals(1, event.getData().codeChanges().filesModified().size()); + } + + @Test + void testParseJsonNodeUserMessageWithAttachment() throws Exception { + String json = """ + { + "type": "user.message", + "data": { + "content": "Check this", + "attachments": [ + { + "type": "code", + "displayName": "snippet.py", + "text": "print('hello')", + "selection": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 14 } + } + } + ] + } + } + """; + + var event = (UserMessageEvent) parseJson(json); + assertNotNull(event); + assertEquals(1, event.getData().attachments().size()); + @SuppressWarnings("unchecked") + var att = (java.util.Map) event.getData().attachments().get(0); + assertEquals("code", att.get("type")); + assertEquals("snippet.py", att.get("displayName")); + @SuppressWarnings("unchecked") + var selection = (java.util.Map) att.get("selection"); + @SuppressWarnings("unchecked") + var start = (java.util.Map) selection.get("start"); + @SuppressWarnings("unchecked") + var end = (java.util.Map) selection.get("end"); + assertEquals(0, ((Number) start.get("line")).intValue()); + assertEquals(14, ((Number) end.get("character")).intValue()); + } + + @Test + void testParseExternalToolRequestedEvent() throws Exception { + String json = """ + { + "type": "external_tool.requested", + "data": { + "requestId": "req-123", + "sessionId": "sess-456", + "toolCallId": "call-789", + "toolName": "get_weather", + "arguments": {"location": "Seattle"} + } + } + """; + + var event = (ExternalToolRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("external_tool.requested", event.getType()); + assertNotNull(event.getData()); + assertEquals("req-123", event.getData().requestId()); + assertEquals("sess-456", event.getData().sessionId()); + assertEquals("call-789", event.getData().toolCallId()); + assertEquals("get_weather", event.getData().toolName()); + } + + @Test + void testParseExternalToolCompletedEvent() throws Exception { + String json = """ + { + "type": "external_tool.completed", + "data": { + "requestId": "req-123" + } + } + """; + + var event = (ExternalToolCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("external_tool.completed", event.getType()); + assertEquals("req-123", event.getData().requestId()); + } + + @Test + void testParsePermissionRequestedEvent() throws Exception { + String json = """ + { + "type": "permission.requested", + "data": { + "requestId": "perm-req-456", + "permissionRequest": { + "kind": "shell", + "toolCallId": "call-001" + } + } + } + """; + + var event = (PermissionRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("permission.requested", event.getType()); + assertEquals("perm-req-456", event.getData().requestId()); + assertNotNull(event.getData().permissionRequest()); + @SuppressWarnings("unchecked") + var permReq = (java.util.Map) event.getData().permissionRequest(); + assertEquals("shell", permReq.get("kind")); + } + + @Test + void testParsePermissionCompletedEvent() throws Exception { + String json = """ + { + "type": "permission.completed", + "data": { + "requestId": "perm-req-456", + "result": { + "kind": "approved" + } + } + } + """; + + var event = (PermissionCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("permission.completed", event.getType()); + assertEquals("perm-req-456", event.getData().requestId()); + assertNotNull(event.getData().result()); + @SuppressWarnings("unchecked") + var result = (java.util.Map) event.getData().result(); + assertEquals("approved", result.get("kind")); + } + + @Test + void testParseCommandQueuedEvent() throws Exception { + String json = """ + { + "type": "command.queued", + "data": { + "requestId": "cmd-req-789", + "command": "/help" + } + } + """; + + var event = (CommandQueuedEvent) parseJson(json); + assertNotNull(event); + assertEquals("command.queued", event.getType()); + assertEquals("cmd-req-789", event.getData().requestId()); + assertEquals("/help", event.getData().command()); + } + + @Test + void testParseCommandCompletedEvent() throws Exception { + String json = """ + { + "type": "command.completed", + "data": { + "requestId": "cmd-req-789" + } + } + """; + + var event = (CommandCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("command.completed", event.getType()); + assertEquals("cmd-req-789", event.getData().requestId()); + } + + @Test + void testParseExitPlanModeRequestedEvent() throws Exception { + String json = """ + { + "type": "exit_plan_mode.requested", + "data": { + "requestId": "plan-req-001", + "summary": "Plan is ready", + "planContent": "## Plan\\n1. Do thing", + "actions": ["exit_only", "interactive", "autopilot"], + "recommendedAction": "interactive" + } + } + """; + + var event = (ExitPlanModeRequestedEvent) parseJson(json); + assertNotNull(event); + assertEquals("exit_plan_mode.requested", event.getType()); + assertEquals("plan-req-001", event.getData().requestId()); + assertEquals("Plan is ready", event.getData().summary()); + assertEquals(3, event.getData().actions().size()); + assertEquals(ExitPlanModeAction.INTERACTIVE, event.getData().recommendedAction()); + } + + @Test + void testParseExitPlanModeCompletedEvent() throws Exception { + String json = """ + { + "type": "exit_plan_mode.completed", + "data": { + "requestId": "plan-req-001" + } + } + """; + + var event = (ExitPlanModeCompletedEvent) parseJson(json); + assertNotNull(event); + assertEquals("exit_plan_mode.completed", event.getType()); + assertEquals("plan-req-001", event.getData().requestId()); + } + + @Test + void testParseSystemNotificationEvent() throws Exception { + String json = """ + { + "type": "system.notification", + "data": { + "content": "Agent completed", + "kind": {"type": "agent_completed", "agentId": "agent-1", "agentType": "task", "status": "completed"} + } + } + """; + + var event = (SystemNotificationEvent) parseJson(json); + assertNotNull(event); + assertEquals("system.notification", event.getType()); + assertNotNull(event.getData()); + assertTrue(event.getData().content().contains("Agent completed")); + } + + @Test + void testParseCapabilitiesChangedEvent() throws Exception { + String json = """ + { + "type": "capabilities.changed", + "data": { + "ui": { + "elicitation": true + } + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(CapabilitiesChangedEvent.class, event); + assertEquals("capabilities.changed", event.getType()); + + var castedEvent = (CapabilitiesChangedEvent) event; + assertNotNull(castedEvent.getData()); + assertNotNull(castedEvent.getData().ui()); + assertTrue(castedEvent.getData().ui().elicitation()); + + // Verify setData round-trip + var newData = new CapabilitiesChangedEvent.CapabilitiesChangedEventData(new CapabilitiesChangedUI(false)); + castedEvent.setData(newData); + assertFalse(castedEvent.getData().ui().elicitation()); + } + + @Test + void testParseCommandExecuteEvent() throws Exception { + String json = """ + { + "type": "command.execute", + "data": { + "requestId": "req-001", + "command": "/deploy production", + "commandName": "deploy", + "args": "production" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(CommandExecuteEvent.class, event); + assertEquals("command.execute", event.getType()); + + var castedEvent = (CommandExecuteEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("req-001", castedEvent.getData().requestId()); + assertEquals("/deploy production", castedEvent.getData().command()); + assertEquals("deploy", castedEvent.getData().commandName()); + assertEquals("production", castedEvent.getData().args()); + + // Verify setData round-trip + castedEvent.setData(new CommandExecuteEvent.CommandExecuteEventData("req-002", "/rollback", "rollback", null)); + assertEquals("req-002", castedEvent.getData().requestId()); + } + + @Test + void testParseElicitationRequestedEvent() throws Exception { + String json = """ + { + "type": "elicitation.requested", + "data": { + "requestId": "elix-001", + "toolCallId": "tc-123", + "elicitationSource": "mcp_tool", + "message": "Please provide your name", + "mode": "form", + "requestedSchema": { + "type": "object", + "properties": { + "name": {"type": "string"} + }, + "required": ["name"] + }, + "url": null + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(ElicitationRequestedEvent.class, event); + assertEquals("elicitation.requested", event.getType()); + + var castedEvent = (ElicitationRequestedEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("elix-001", castedEvent.getData().requestId()); + assertEquals("tc-123", castedEvent.getData().toolCallId()); + assertEquals("mcp_tool", castedEvent.getData().elicitationSource()); + assertEquals("Please provide your name", castedEvent.getData().message()); + assertEquals(ElicitationRequestedMode.FORM, castedEvent.getData().mode()); + assertNotNull(castedEvent.getData().requestedSchema()); + assertEquals("object", castedEvent.getData().requestedSchema().type()); + assertNotNull(castedEvent.getData().requestedSchema().properties()); + assertNotNull(castedEvent.getData().requestedSchema().required()); + assertTrue(castedEvent.getData().requestedSchema().required().contains("name")); + + // Verify setData round-trip + castedEvent.setData(new ElicitationRequestedEvent.ElicitationRequestedEventData("elix-002", null, null, + "Enter URL", ElicitationRequestedMode.URL, null, "https://example.com")); + assertEquals("elix-002", castedEvent.getData().requestId()); + assertEquals(ElicitationRequestedMode.URL, castedEvent.getData().mode()); + } + + @Test + void testParseSessionContextChangedEvent() throws Exception { + String json = """ + { + "type": "session.context_changed", + "data": { + "cwd": "/home/user/project", + "gitRoot": "/home/user/project", + "repository": "my-repo", + "branch": "main" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionContextChangedEvent.class, event); + assertEquals("session.context_changed", event.getType()); + + var castedEvent = (SessionContextChangedEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("/home/user/project", castedEvent.getData().cwd()); + + // Verify setData round-trip + castedEvent.setData(null); + assertNull(castedEvent.getData()); + } + + @Test + void testParseSessionTaskCompleteEvent() throws Exception { + String json = """ + { + "type": "session.task_complete", + "data": { + "summary": "Task completed successfully" + } + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SessionTaskCompleteEvent.class, event); + assertEquals("session.task_complete", event.getType()); + + var castedEvent = (SessionTaskCompleteEvent) event; + assertNotNull(castedEvent.getData()); + assertEquals("Task completed successfully", castedEvent.getData().summary()); + + // Verify setData round-trip + castedEvent.setData(new SessionTaskCompleteEvent.SessionTaskCompleteEventData("New summary", null)); + assertEquals("New summary", castedEvent.getData().summary()); + } + + @Test + void testParseSubagentDeselectedEvent() throws Exception { + String json = """ + { + "type": "subagent.deselected", + "data": {} + } + """; + + SessionEvent event = parseJson(json); + assertNotNull(event); + assertInstanceOf(SubagentDeselectedEvent.class, event); + assertEquals("subagent.deselected", event.getType()); + + var castedEvent = (SubagentDeselectedEvent) event; + assertNotNull(castedEvent.getData()); + + // Verify setData round-trip + castedEvent.setData(new SubagentDeselectedEvent.SubagentDeselectedEventData()); + assertNotNull(castedEvent.getData()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java b/java/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java new file mode 100644 index 000000000..94f2c3dc7 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionEventHandlingTest.java @@ -0,0 +1,876 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.Closeable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.generated.SessionStartEvent; + +/** + * Unit tests for session event handling API. + *

+ * These are pure unit tests that don't require the Copilot CLI. They test the + * event dispatch mechanism directly. + */ +public class SessionEventHandlingTest { + + private CopilotSession session; + + @BeforeEach + void setup() throws Exception { + // Create a minimal session for testing event handling + // We use reflection to create a session without a real RPC connection + session = createTestSession(); + } + + private CopilotSession createTestSession() throws Exception { + // Use the package-private constructor via reflection for testing + var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + constructor.setAccessible(true); + return constructor.newInstance("test-session-id", null, null); + } + + @Test + void testGenericEventHandler() { + var receivedEvents = new ArrayList(); + + session.on(event -> receivedEvents.add(event)); + + // Dispatch some events + dispatchEvent(createSessionStartEvent()); + dispatchEvent(createAssistantMessageEvent("Hello")); + dispatchEvent(createSessionIdleEvent()); + + assertEquals(3, receivedEvents.size()); + assertInstanceOf(SessionStartEvent.class, receivedEvents.get(0)); + assertInstanceOf(AssistantMessageEvent.class, receivedEvents.get(1)); + assertInstanceOf(SessionIdleEvent.class, receivedEvents.get(2)); + } + + @Test + void testTypedEventHandler() { + var receivedMessages = new ArrayList(); + + session.on(AssistantMessageEvent.class, msg -> receivedMessages.add(msg)); + + // Dispatch various events - only AssistantMessageEvent should be captured + dispatchEvent(createSessionStartEvent()); + dispatchEvent(createAssistantMessageEvent("First message")); + dispatchEvent(createSessionIdleEvent()); + dispatchEvent(createAssistantMessageEvent("Second message")); + + // Should only have the two assistant messages + assertEquals(2, receivedMessages.size()); + assertEquals("First message", receivedMessages.get(0).getData().content()); + assertEquals("Second message", receivedMessages.get(1).getData().content()); + } + + @Test + void testMultipleTypedHandlers() { + var messages = new ArrayList(); + var idles = new ArrayList(); + var starts = new ArrayList(); + + session.on(AssistantMessageEvent.class, messages::add); + session.on(SessionIdleEvent.class, idles::add); + session.on(SessionStartEvent.class, starts::add); + + dispatchEvent(createSessionStartEvent()); + dispatchEvent(createAssistantMessageEvent("Hello")); + dispatchEvent(createSessionIdleEvent()); + dispatchEvent(createAssistantMessageEvent("World")); + + assertEquals(1, starts.size()); + assertEquals(2, messages.size()); + assertEquals(1, idles.size()); + } + + @Test + void testUnsubscribe() { + var count = new AtomicInteger(0); + + Closeable subscription = session.on(AssistantMessageEvent.class, msg -> count.incrementAndGet()); + + dispatchEvent(createAssistantMessageEvent("First")); + assertEquals(1, count.get()); + + // Unsubscribe + try { + subscription.close(); + } catch (Exception e) { + fail("Unsubscribe should not throw: " + e.getMessage()); + } + + // Should no longer receive events + dispatchEvent(createAssistantMessageEvent("Second")); + assertEquals(1, count.get()); // Still 1, not 2 + } + + @Test + void testUnsubscribeGenericHandler() { + var count = new AtomicInteger(0); + + Closeable subscription = session.on(event -> count.incrementAndGet()); + + dispatchEvent(createSessionStartEvent()); + assertEquals(1, count.get()); + + try { + subscription.close(); + } catch (Exception e) { + fail("Unsubscribe should not throw: " + e.getMessage()); + } + + dispatchEvent(createSessionIdleEvent()); + assertEquals(1, count.get()); // Still 1 + } + + @Test + void testMixedHandlers() { + var allEvents = new ArrayList(); + var messageEvents = new ArrayList(); + + // Generic handler captures everything + session.on(event -> allEvents.add(event.getType())); + + // Typed handler captures only messages + session.on(AssistantMessageEvent.class, msg -> messageEvents.add(msg.getData().content())); + + dispatchEvent(createSessionStartEvent()); + dispatchEvent(createAssistantMessageEvent("Hello")); + dispatchEvent(createSessionIdleEvent()); + + assertEquals(3, allEvents.size()); + assertEquals(1, messageEvents.size()); + assertEquals("Hello", messageEvents.get(0)); + } + + @Test + void testHandlerReceivesCorrectEventData() { + var capturedContent = new AtomicReference(); + var capturedSessionId = new AtomicReference(); + + session.on(AssistantMessageEvent.class, msg -> { + capturedContent.set(msg.getData().content()); + }); + + session.on(SessionStartEvent.class, start -> { + capturedSessionId.set(start.getData().sessionId()); + }); + + SessionStartEvent startEvent = createSessionStartEvent(); + startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, + null, null, null, null, null, null)); + dispatchEvent(startEvent); + + AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); + dispatchEvent(msgEvent); + + assertEquals("my-session-123", capturedSessionId.get()); + assertEquals("Test content", capturedContent.get()); + } + + @Test + void testHandlerExceptionDoesNotBreakOtherHandlers() { + var handler2Events = new ArrayList(); + + // Suppress logging for this test to avoid confusing stack traces in build + // output + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + // Use SUPPRESS policy so second handler still runs + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + + // First handler throws an exception + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("Handler 1 error"); + }); + + // Second handler should still receive events + session.on(AssistantMessageEvent.class, msg -> { + handler2Events.add(msg.getData().content()); + }); + + // This should not throw - exceptions are caught + assertDoesNotThrow(() -> dispatchEvent(createAssistantMessageEvent("Test"))); + + // Second handler should have received the event + assertEquals(1, handler2Events.size()); + assertEquals("Test", handler2Events.get(0)); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testNoHandlersDoesNotThrow() { + // Dispatching events with no handlers should not throw + assertDoesNotThrow(() -> { + dispatchEvent(createSessionStartEvent()); + dispatchEvent(createAssistantMessageEvent("Test")); + dispatchEvent(createSessionIdleEvent()); + }); + } + + @Test + void testDuplicateTypedHandlersBothReceiveEvent() { + var count1 = new AtomicInteger(); + var count2 = new AtomicInteger(); + + session.on(AssistantMessageEvent.class, msg -> count1.incrementAndGet()); + session.on(AssistantMessageEvent.class, msg -> count2.incrementAndGet()); + + dispatchEvent(createAssistantMessageEvent("hello")); + + assertEquals(1, count1.get(), "First typed handler should be called"); + assertEquals(1, count2.get(), "Second typed handler should be called"); + } + + @Test + void testDuplicateGenericHandlersBothFire() { + var events1 = new ArrayList(); + var events2 = new ArrayList(); + + session.on(event -> events1.add(event.getType())); + session.on(event -> events2.add(event.getType())); + + dispatchEvent(createAssistantMessageEvent("test")); + + assertEquals(1, events1.size(), "First generic handler should receive event"); + assertEquals(1, events2.size(), "Second generic handler should receive event"); + } + + @Test + void testUnsubscribeOneKeepsOther() { + var count1 = new AtomicInteger(); + var count2 = new AtomicInteger(); + + var sub1 = session.on(AssistantMessageEvent.class, msg -> count1.incrementAndGet()); + session.on(AssistantMessageEvent.class, msg -> count2.incrementAndGet()); + + dispatchEvent(createAssistantMessageEvent("before")); + assertEquals(1, count1.get()); + assertEquals(1, count2.get()); + + // Unsubscribe first handler + try { + sub1.close(); + } catch (Exception e) { + fail("Unsubscribe should not throw: " + e.getMessage()); + } + + dispatchEvent(createAssistantMessageEvent("after")); + assertEquals(1, count1.get(), "Unsubscribed handler should not be called again"); + assertEquals(2, count2.get(), "Remaining handler should still be called"); + } + + @Test + void testAllHandlersInvoked() { + var called = new ArrayList(); + + session.on(AssistantMessageEvent.class, msg -> called.add("first")); + session.on(AssistantMessageEvent.class, msg -> called.add("second")); + session.on(AssistantMessageEvent.class, msg -> called.add("third")); + + dispatchEvent(createAssistantMessageEvent("test")); + + assertEquals(3, called.size(), "All three handlers should be invoked"); + assertTrue(called.containsAll(List.of("first", "second", "third")), "All handler labels should be present"); + } + + @Test + void testHandlersRunOnDispatchThread() throws Exception { + var handlerThreadName = new AtomicReference(); + var latch = new CountDownLatch(1); + + session.on(AssistantMessageEvent.class, msg -> { + handlerThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + }); + + // Dispatch from a named thread to simulate the jsonrpc-reader + var t = new Thread(() -> dispatchEvent(createAssistantMessageEvent("async")), "jsonrpc-reader-mock"); + t.start(); + assertTrue(latch.await(5, TimeUnit.SECONDS), "Handler should be invoked within timeout"); + t.join(5000); + + assertEquals("jsonrpc-reader-mock", handlerThreadName.get(), + "Handler should run on the dispatch thread, not a different one"); + } + + @Test + void testHandlersRunOffMainThread() throws Exception { + var mainThreadName = Thread.currentThread().getName(); + var handlerThreadName = new AtomicReference(); + var latch = new CountDownLatch(1); + + session.on(AssistantMessageEvent.class, msg -> { + handlerThreadName.set(Thread.currentThread().getName()); + latch.countDown(); + }); + + // Dispatch from a background thread (simulates jsonrpc-reader) + new Thread(() -> dispatchEvent(createAssistantMessageEvent("bg")), "background-dispatcher").start(); + + assertTrue(latch.await(5, TimeUnit.SECONDS), "Handler should be invoked within timeout"); + assertNotEquals(mainThreadName, handlerThreadName.get(), "Handler should NOT run on the main/test thread"); + assertEquals("background-dispatcher", handlerThreadName.get(), + "Handler should run on the background dispatch thread"); + } + + @Test + void testConcurrentDispatchFromMultipleThreads() throws Exception { + var totalEvents = 100; + var receivedCount = new AtomicInteger(); + var threadNames = ConcurrentHashMap.newKeySet(); + var latch = new CountDownLatch(totalEvents); + + session.on(AssistantMessageEvent.class, msg -> { + receivedCount.incrementAndGet(); + threadNames.add(Thread.currentThread().getName()); + latch.countDown(); + }); + + // Fire events from 10 concurrent threads, 10 events each + var threads = new ArrayList(); + for (int i = 0; i < 10; i++) { + var threadIdx = i; + var t = new Thread(() -> { + for (int j = 0; j < 10; j++) { + dispatchEvent(createAssistantMessageEvent("msg-" + threadIdx + "-" + j)); + } + }, "dispatcher-" + i); + threads.add(t); + } + + for (var t : threads) { + t.start(); + } + + assertTrue(latch.await(10, TimeUnit.SECONDS), "All events should be delivered within timeout"); + for (var t : threads) { + t.join(5000); + } + + assertEquals(totalEvents, receivedCount.get(), "All " + totalEvents + " events should be delivered"); + assertTrue(threadNames.size() > 1, "Events should have been dispatched from multiple threads"); + } + + // Helper methods to dispatch events using reflection + // ==================================================================== + // EventErrorHandler tests + // ==================================================================== + + @Test + void testDefaultPolicyPropagatesAndLogs() { + // Default policy is PROPAGATE_AND_LOG_ERRORS β€” stops dispatch on first error + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + // Both handlers throw β€” with PROPAGATE only one should execute + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("boom 1"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("boom 2"); + }); + + assertDoesNotThrow(() -> dispatchEvent(createAssistantMessageEvent("Test"))); + + // Only one handler should execute (default PROPAGATE_AND_LOG_ERRORS policy) + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, "Only one handler should execute with default PROPAGATE_AND_LOG_ERRORS policy"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testCustomEventErrorHandlerReceivesEventAndException() { + var capturedEvents = new ArrayList(); + var capturedExceptions = new ArrayList(); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorHandler((event, exception) -> { + capturedEvents.add(event); + capturedExceptions.add(exception); + }); + + var thrownException = new RuntimeException("test error"); + session.on(AssistantMessageEvent.class, msg -> { + throw thrownException; + }); + + var event = createAssistantMessageEvent("Hello"); + dispatchEvent(event); + + assertEquals(1, capturedEvents.size()); + assertSame(event, capturedEvents.get(0)); + assertEquals(1, capturedExceptions.size()); + assertSame(thrownException, capturedExceptions.get(0)); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testCustomErrorHandlerCalledForAllErrors() { + var errorCount = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + errorCount.incrementAndGet(); + }); + + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error 1"); + }); + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error 2"); + }); + + dispatchEvent(createAssistantMessageEvent("Test")); + + // Both handler errors should be reported to the custom error handler + assertEquals(2, errorCount.get()); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testErrorHandlerItselfThrowingStopsDispatch() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorHandler((event, exception) -> { + throw new RuntimeException("error handler also broke"); + }); + + // Two handlers that throw + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("handler error"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("handler error"); + }); + + assertDoesNotThrow(() -> dispatchEvent(createAssistantMessageEvent("Test"))); + // Error handler threw β€” dispatch stops regardless of policy + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, + "Only one handler should have been called (dispatch stopped when error handler threw)"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testSetEventErrorHandlerToNullRestoresDefaultBehavior() { + var errorCount = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + // Set custom handler + session.setEventErrorHandler((event, exception) -> { + errorCount.incrementAndGet(); + }); + + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error"); + }); + + dispatchEvent(createAssistantMessageEvent("Test1")); + assertEquals(1, errorCount.get()); + + // Reset to null (restore default logging-only behavior) + session.setEventErrorHandler(null); + + dispatchEvent(createAssistantMessageEvent("Test2")); + + // Custom handler should NOT have been called again + assertEquals(1, errorCount.get()); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testErrorHandlerReceivesCorrectEventType() { + var capturedEvents = new ArrayList(); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + capturedEvents.add(event); + }); + + session.on(event -> { + throw new RuntimeException("always fails"); + }); + + var msgEvent = createAssistantMessageEvent("msg"); + var idleEvent = createSessionIdleEvent(); + + dispatchEvent(msgEvent); + dispatchEvent(idleEvent); + + assertEquals(2, capturedEvents.size()); + assertInstanceOf(AssistantMessageEvent.class, capturedEvents.get(0)); + assertInstanceOf(SessionIdleEvent.class, capturedEvents.get(1)); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + // ==================================================================== + // EventErrorPolicy tests + // ==================================================================== + + @Test + void testDefaultPolicyPropagatesOnError() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorHandler((event, exception) -> { + // just consume + }); + + // Both handlers throw β€” with PROPAGATE only one should execute + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("error 1"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("error 2"); + }); + + dispatchEvent(createAssistantMessageEvent("Test")); + + // Default is PROPAGATE_AND_LOG_ERRORS β€” only one handler runs + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, "Only one handler should execute with default PROPAGATE_AND_LOG_ERRORS policy"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testPropagatePolicyStopsOnFirstError() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + var errorHandlerCalls = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + errorHandlerCalls.incrementAndGet(); + }); + + // Two handlers that throw + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("error 1"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("error 2"); + }); + + dispatchEvent(createAssistantMessageEvent("Test")); + + // Only one handler should have been called (PROPAGATE_AND_LOG_ERRORS policy) + assertEquals(1, errorHandlerCalls.get()); + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, "Only one handler should execute with PROPAGATE_AND_LOG_ERRORS policy"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testPropagatePolicyErrorHandlerAlwaysInvoked() { + var errorHandlerCalls = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + errorHandlerCalls.incrementAndGet(); + }); + + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error"); + }); + + dispatchEvent(createAssistantMessageEvent("Test")); + + // Error handler should be called even with PROPAGATE_AND_LOG_ERRORS policy + assertEquals(1, errorHandlerCalls.get()); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testSuppressPolicyWithMultipleErrors() { + var errorHandlerCalls = new AtomicInteger(0); + var successfulHandlerCalls = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + errorHandlerCalls.incrementAndGet(); + }); + + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error 1"); + }); + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error 2"); + }); + session.on(AssistantMessageEvent.class, msg -> { + successfulHandlerCalls.incrementAndGet(); + }); + session.on(AssistantMessageEvent.class, msg -> { + throw new RuntimeException("error 3"); + }); + + dispatchEvent(createAssistantMessageEvent("Test")); + + // All errors should be reported, successful handler should run + assertEquals(3, errorHandlerCalls.get()); + assertEquals(1, successfulHandlerCalls.get()); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testSwitchPolicyDynamically() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + session.setEventErrorHandler((event, exception) -> { + // just consume + }); + + // Two handlers that throw + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + + // With SUPPRESS_AND_LOG_ERRORS, both should fire + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + dispatchEvent(createAssistantMessageEvent("Test1")); + assertEquals(1, handler1Called.get()); + assertEquals(1, handler2Called.get()); + + handler1Called.set(0); + handler2Called.set(0); + + // Switch to PROPAGATE_AND_LOG_ERRORS β€” only one should fire + session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS); + dispatchEvent(createAssistantMessageEvent("Test2")); + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, "Only one handler should execute after switching to PROPAGATE_AND_LOG_ERRORS"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testPropagatePolicyNoErrorHandlerStopsAndLogs() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + // No error handler set, PROPAGATE_AND_LOG_ERRORS policy + session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS); + + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + + assertDoesNotThrow(() -> dispatchEvent(createAssistantMessageEvent("Test"))); + + // PROPAGATE_AND_LOG_ERRORS policy should stop after first error + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, + "Only one handler should execute with PROPAGATE_AND_LOG_ERRORS policy and no error handler"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + @Test + void testErrorHandlerThrowingStopsRegardlessOfPolicy() { + var handler1Called = new AtomicInteger(0); + var handler2Called = new AtomicInteger(0); + + Logger sessionLogger = Logger.getLogger(CopilotSession.class.getName()); + Level originalLevel = sessionLogger.getLevel(); + sessionLogger.setLevel(Level.OFF); + + try { + // SUPPRESS_AND_LOG_ERRORS policy, but error handler throws + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + session.setEventErrorHandler((event, exception) -> { + throw new RuntimeException("error handler broke"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler1Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + + session.on(AssistantMessageEvent.class, msg -> { + handler2Called.incrementAndGet(); + throw new RuntimeException("error"); + }); + + assertDoesNotThrow(() -> dispatchEvent(createAssistantMessageEvent("Test"))); + + // Error handler threw β€” should stop regardless of SUPPRESS_AND_LOG_ERRORS + // policy + int totalCalls = handler1Called.get() + handler2Called.get(); + assertEquals(1, totalCalls, + "Only one handler should execute when error handler throws, even with SUPPRESS_AND_LOG_ERRORS policy"); + } finally { + sessionLogger.setLevel(originalLevel); + } + } + + // ==================================================================== + // Helper methods + // ==================================================================== + + private void dispatchEvent(SessionEvent event) { + try { + Method dispatchMethod = CopilotSession.class.getDeclaredMethod("dispatchEvent", SessionEvent.class); + dispatchMethod.setAccessible(true); + dispatchMethod.invoke(session, event); + } catch (Exception e) { + throw new RuntimeException("Failed to dispatch event", e); + } + } + + // Factory methods for creating test events + private SessionStartEvent createSessionStartEvent() { + return createSessionStartEvent("test-session"); + } + + private SessionStartEvent createSessionStartEvent(String sessionId) { + var event = new SessionStartEvent(); + var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, + null, null, null, null); + event.setData(data); + return event; + } + + private AssistantMessageEvent createAssistantMessageEvent(String content) { + var event = new AssistantMessageEvent(); + var data = new AssistantMessageEvent.AssistantMessageEventData(null, null, content, null, null, null, null, + null, null, null, null, null, null, null, null); + event.setData(data); + return event; + } + + private SessionIdleEvent createSessionIdleEvent() { + return new SessionIdleEvent(); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java b/java/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java new file mode 100644 index 000000000..d13c84247 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionEventsE2ETest.java @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.generated.AssistantTurnEndEvent; +import com.github.copilot.sdk.generated.AssistantTurnStartEvent; +import com.github.copilot.sdk.generated.AssistantUsageEvent; +import com.github.copilot.sdk.generated.SessionIdleEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.generated.ToolExecutionStartEvent; +import com.github.copilot.sdk.generated.UserMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for session events to verify event lifecycle. + *

+ * These tests verify that various session events are properly emitted during + * typical interaction flows with the Copilot CLI. + *

+ */ +public class SessionEventsE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that assistant turn events (turn_start, turn_end) are emitted. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents_assistantTurnEvents() throws Exception { + // Use existing session snapshot that emits turn events + ctx.configureForTest("session", "should_receive_session_events"); + + var allEvents = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(event -> allEvents.add(event)); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); + + // Verify turn lifecycle events + assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantTurnStartEvent), + "Should receive assistant.turn_start event"); + assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantTurnEndEvent), + "Should receive assistant.turn_end event"); + + // Verify order: turn_start should come before turn_end + int turnStartIndex = -1; + int turnEndIndex = -1; + for (int i = 0; i < allEvents.size(); i++) { + if (allEvents.get(i) instanceof AssistantTurnStartEvent && turnStartIndex == -1) { + turnStartIndex = i; + } + if (allEvents.get(i) instanceof AssistantTurnEndEvent) { + turnEndIndex = i; + } + } + assertTrue(turnStartIndex < turnEndIndex, "turn_start should come before turn_end"); + } + } + + /** + * Verifies that user message events are emitted. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents_userMessageEvent() throws Exception { + // Use existing session snapshot + ctx.configureForTest("session", "should_receive_session_events"); + + var userMessages = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(UserMessageEvent.class, userMessages::add); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); + + // Verify user message was captured + assertFalse(userMessages.isEmpty(), "Should receive user.message event"); + } + } + + /** + * Verifies that tool execution complete events are emitted. + * + * @see Snapshot: tools/invokes_built_in_tools + */ + @Test + void testInvokesBuiltInTools_toolExecutionCompleteEvent() throws Exception { + // Use existing tools snapshot for built-in tool invocation + ctx.configureForTest("tools", "invokes_built_in_tools"); + + var toolStarts = new ArrayList(); + var toolCompletes = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(ToolExecutionStartEvent.class, toolStarts::add); + session.on(ToolExecutionCompleteEvent.class, toolCompletes::add); + + // Create the README.md file expected by the snapshot - must have ONLY one line + // to match the snapshot's expected tool response: "1. # ELIZA, the only chatbot + // you'll ever need" + Path testFile = ctx.getWorkDir().resolve("README.md"); + Files.writeString(testFile, "# ELIZA, the only chatbot you'll ever need"); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) + .get(60, TimeUnit.SECONDS); + + // Verify tool execution events + assertFalse(toolStarts.isEmpty(), "Should receive tool.execution_start event"); + assertFalse(toolCompletes.isEmpty(), "Should receive tool.execution_complete event"); + + // Verify tool execution completed successfully + assertTrue(toolCompletes.stream().anyMatch(e -> e.getData().success()), + "At least one tool execution should be successful"); + } + } + + /** + * Verifies that assistant usage events are handled when emitted. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents_assistantUsageEvent() throws Exception { + // Use existing session snapshot + ctx.configureForTest("session", "should_receive_session_events"); + + var usageEvents = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(AssistantUsageEvent.class, usageEvents::add); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); + + // Usage events may or may not be emitted depending on the model/API version + // This test verifies the event handler works when they are emitted + // We don't assert they must be present since it depends on the backend + } + } + + /** + * Verifies that session.idle event is emitted after message completion. + * + * @see Snapshot: session/should_receive_session_events + */ + @Test + void testShouldReceiveSessionEvents_sessionIdleAfterMessage() throws Exception { + // Use existing session snapshot + ctx.configureForTest("session", "should_receive_session_events"); + + var allEvents = new ArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(event -> allEvents.add(event)); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); + + // Verify session.idle is emitted after assistant.message + assertTrue(allEvents.stream().anyMatch(e -> e instanceof SessionIdleEvent), + "Should receive session.idle event"); + assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantMessageEvent), + "Should receive assistant.message event"); + + // Verify order: assistant.message should come before session.idle + int messageIndex = -1; + int idleIndex = -1; + for (int i = 0; i < allEvents.size(); i++) { + if (allEvents.get(i) instanceof AssistantMessageEvent) { + messageIndex = i; + } + if (allEvents.get(i) instanceof SessionIdleEvent) { + idleIndex = i; + } + } + assertTrue(messageIndex < idleIndex, "assistant.message should come before session.idle"); + } + } + + /** + * Verifies the order of events during tool execution. + * + * @see Snapshot: tools/invokes_built_in_tools + */ + @Test + void testInvokesBuiltInTools_eventOrderDuringToolExecution() throws Exception { + // Use existing tools snapshot for built-in tool invocation + ctx.configureForTest("tools", "invokes_built_in_tools"); + + var eventTypes = new ArrayList(); + // Use a separate completion signal so we know when THIS handler has seen + // session.idle, rather than relying on sendAndWait's internal subscription. + // sendAndWait also listens for session.idle internally. Because eventHandlers + // is a ConcurrentHashMap Set (non-deterministic iteration order), the + // sendAndWait handler can fire BEFORE this listener and unblock the test + // thread before session.idle has been added to eventTypes β€” a race condition. + var idleReceived = new java.util.concurrent.CompletableFuture(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + session.on(event -> { + eventTypes.add(event.getType()); + if (event instanceof SessionIdleEvent) { + idleReceived.complete(null); + } + }); + + // Create the README.md file expected by the snapshot - must have ONLY one line + // to match the snapshot's expected tool response: "1. # ELIZA, the only chatbot + // you'll ever need" + Path testFile = ctx.getWorkDir().resolve("README.md"); + Files.writeString(testFile, "# ELIZA, the only chatbot you'll ever need"); + + // Use prompt that matches the snapshot + session.sendAndWait(new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) + .get(60, TimeUnit.SECONDS); + + // Wait for this listener to also receive session.idle. sendAndWait can return + // slightly before our listener sees the event due to concurrent dispatch + // ordering. + idleReceived.get(5, TimeUnit.SECONDS); + + // Verify expected event types are present + assertTrue(eventTypes.contains("user.message"), "Should have user.message"); + assertTrue(eventTypes.contains("assistant.turn_start"), "Should have assistant.turn_start"); + assertTrue(eventTypes.contains("tool.execution_start"), "Should have tool.execution_start"); + assertTrue(eventTypes.contains("tool.execution_complete"), "Should have tool.execution_complete"); + assertTrue(eventTypes.contains("assistant.message"), "Should have assistant.message"); + assertTrue(eventTypes.contains("assistant.turn_end"), "Should have assistant.turn_end"); + assertTrue(eventTypes.contains("session.idle"), "Should have session.idle"); + + // Verify tool execution is between turn_start and turn_end + int turnStartIdx = eventTypes.indexOf("assistant.turn_start"); + int toolStartIdx = eventTypes.indexOf("tool.execution_start"); + int toolCompleteIdx = eventTypes.indexOf("tool.execution_complete"); + int turnEndIdx = eventTypes.lastIndexOf("assistant.turn_end"); + + assertTrue(turnStartIdx < toolStartIdx, "turn_start should be before tool.execution_start"); + assertTrue(toolStartIdx < toolCompleteIdx, "tool.execution_start should be before tool.execution_complete"); + assertTrue(toolCompleteIdx < turnEndIdx, "tool.execution_complete should be before turn_end"); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java b/java/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java new file mode 100644 index 000000000..5a8dc3fcb --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java @@ -0,0 +1,392 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.SessionEndHookOutput; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.SessionStartHookOutput; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.UserInputRequest; +import com.github.copilot.sdk.json.UserInputResponse; +import com.github.copilot.sdk.json.UserPromptSubmittedHookOutput; + +/** + * Unit tests for CopilotSession internal handler methods. + *

+ * Tests package-private handler and hook dispatch logic that doesn't require a + * live CLI connection. + */ +public class SessionHandlerTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private CopilotSession session; + + @BeforeEach + void setup() throws Exception { + var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + constructor.setAccessible(true); + session = constructor.newInstance("handler-test-session", null, null); + } + + // ===== setEventErrorPolicy ===== + + @Test + void testSetEventErrorPolicyNullThrowsNPE() { + assertThrows(NullPointerException.class, () -> session.setEventErrorPolicy(null)); + } + + @Test + void testSetEventErrorPolicySetsValue() { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + // No exception means success; the policy is stored internally + } + + // ===== handlePermissionRequest: no handler registered ===== + + @Test + void testHandlePermissionRequestWithNoHandlerReturnsDenied() throws Exception { + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file", "resource", "/tmp/test")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("user-not-available", result.getKind()); + } + + // ===== handlePermissionRequest: handler throws ===== + + @Test + void testHandlePermissionRequestHandlerExceptionReturnsDenied() throws Exception { + session.registerPermissionHandler((request, invocation) -> { + throw new RuntimeException("handler boom"); + }); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("user-not-available", result.getKind()); + } + + // ===== handlePermissionRequest: handler future fails ===== + + @Test + void testHandlePermissionRequestHandlerFutureFailsReturnsDenied() throws Exception { + session.registerPermissionHandler( + (request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async handler boom"))); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("user-not-available", result.getKind()); + } + + // ===== handlePermissionRequest: handler succeeds ===== + + @Test + void testHandlePermissionRequestHandlerSucceeds() throws Exception { + session.registerPermissionHandler((request, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + var res = new PermissionRequestResult(); + res.setKind("allow"); + return CompletableFuture.completedFuture(res); + }); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + assertEquals("allow", result.getKind()); + } + + // ===== handlePermissionRequest: handler returns NO_RESULT (v3 path) ===== + + @Test + void testHandlePermissionRequestNoResultPassesThrough() throws Exception { + session.registerPermissionHandler((request, invocation) -> { + var res = new PermissionRequestResult(); + res.setKind(PermissionRequestResultKind.NO_RESULT); + return CompletableFuture.completedFuture(res); + }); + + JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file")); + + PermissionRequestResult result = session.handlePermissionRequest(data).get(); + + // In v3, NO_RESULT is a valid response β€” the session just returns it + // and the caller (CopilotSession.executePermissionAndRespondAsync) decides + // to skip sending the RPC response. + assertEquals("no-result", result.getKind()); + } + + // ===== handleUserInputRequest: no handler registered ===== + + @Test + void testHandleUserInputRequestNoHandler() { + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(IllegalStateException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler throws synchronously ===== + + @Test + void testHandleUserInputRequestHandlerThrowsSynchronously() { + session.registerUserInputHandler((req, invocation) -> { + throw new RuntimeException("sync user input boom"); + }); + + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler future fails ===== + + @Test + void testHandleUserInputRequestHandlerFutureFails() { + session.registerUserInputHandler( + (req, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async user input boom"))); + + var request = new UserInputRequest(); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleUserInputRequest(request).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleUserInputRequest: handler succeeds ===== + + @Test + void testHandleUserInputRequestHandlerSucceeds() throws Exception { + session.registerUserInputHandler((req, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new UserInputResponse().setAnswer("user typed this")); + }); + + var request = new UserInputRequest(); + + UserInputResponse response = session.handleUserInputRequest(request).get(); + + assertEquals("user typed this", response.getAnswer()); + } + + // ===== handleHooksInvoke: no hooks registered ===== + + @Test + void testHandleHooksInvokeNoHooksReturnsNull() throws Exception { + JsonNode input = MAPPER.valueToTree(Map.of()); + + Object result = session.handleHooksInvoke("preToolUse", input).get(); + + assertNull(result); + } + + // ===== handleHooksInvoke: userPromptSubmitted ===== + + @Test + void testHandleHooksInvokeUserPromptSubmitted() throws Exception { + var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture + .completedFuture(new UserPromptSubmittedHookOutput("modified prompt", "extra context", false)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER + .valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "prompt", "original prompt")); + + Object result = session.handleHooksInvoke("userPromptSubmitted", input).get(); + + assertInstanceOf(UserPromptSubmittedHookOutput.class, result); + var output = (UserPromptSubmittedHookOutput) result; + assertEquals("modified prompt", output.modifiedPrompt()); + } + + // ===== handleHooksInvoke: sessionStart ===== + + @Test + void testHandleHooksInvokeSessionStart() throws Exception { + var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new SessionStartHookOutput("additional context", null)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + Object result = session.handleHooksInvoke("sessionStart", input).get(); + + assertInstanceOf(SessionStartHookOutput.class, result); + var output = (SessionStartHookOutput) result; + assertEquals("additional context", output.additionalContext()); + } + + // ===== handleHooksInvoke: sessionEnd ===== + + @Test + void testHandleHooksInvokeSessionEnd() throws Exception { + var hooks = new SessionHooks().setOnSessionEnd((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + return CompletableFuture.completedFuture(new SessionEndHookOutput(false, null, "summary")); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "reason", "user_closed")); + + Object result = session.handleHooksInvoke("sessionEnd", input).get(); + + assertInstanceOf(SessionEndHookOutput.class, result); + var output = (SessionEndHookOutput) result; + assertEquals("summary", output.sessionSummary()); + } + + // ===== handleHooksInvoke: sessionId deserialization on hook inputs ===== + + @Test + void testHookInputSessionIdDeserializedForSessionStart() throws Exception { + var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> { + assertEquals("runtime-session-123", hookInput.sessionId()); + assertEquals(1735689600L, hookInput.timestamp()); + assertEquals("/tmp", hookInput.cwd()); + return CompletableFuture.completedFuture(new SessionStartHookOutput(null, null)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree( + Map.of("sessionId", "runtime-session-123", "timestamp", 1735689600L, "cwd", "/tmp", "source", "new")); + + session.handleHooksInvoke("sessionStart", input).get(); + } + + @Test + void testHookInputSessionIdDeserializedForSessionEnd() throws Exception { + var hooks = new SessionHooks().setOnSessionEnd((hookInput, invocation) -> { + assertEquals("runtime-session-456", hookInput.sessionId()); + assertEquals("user_closed", hookInput.reason()); + return CompletableFuture.completedFuture(new SessionEndHookOutput(false, null, null)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("sessionId", "runtime-session-456", "timestamp", 1735689600L, "cwd", + "/tmp", "reason", "user_closed")); + + session.handleHooksInvoke("sessionEnd", input).get(); + } + + @Test + void testHookInputSessionIdDeserializedForUserPromptSubmitted() throws Exception { + var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> { + assertEquals("runtime-session-789", hookInput.sessionId()); + assertEquals("hello", hookInput.prompt()); + return CompletableFuture.completedFuture(new UserPromptSubmittedHookOutput(null, null, null)); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree( + Map.of("sessionId", "runtime-session-789", "timestamp", 1735689600L, "cwd", "/tmp", "prompt", "hello")); + + session.handleHooksInvoke("userPromptSubmitted", input).get(); + } + + // ===== handleHooksInvoke: unhandled hook type ===== + + @Test + void testHandleHooksInvokeUnhandledHookType() throws Exception { + session.registerHooks(new SessionHooks()); + + JsonNode input = MAPPER.valueToTree(Map.of()); + + Object result = session.handleHooksInvoke("unknownHookType", input).get(); + + assertNull(result); + } + + // ===== handleHooksInvoke: handler throws ===== + + @Test + void testHandleHooksInvokeHandlerThrows() throws Exception { + var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> { + throw new RuntimeException("hook boom"); + }); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleHooksInvoke("sessionStart", input).get()); + assertInstanceOf(RuntimeException.class, ex.getCause()); + } + + // ===== handleHooksInvoke: invalid JSON for hook input ===== + + @Test + void testHandleHooksInvokeInvalidJsonFails() throws Exception { + var hooks = new SessionHooks().setOnSessionStart( + (hookInput, invocation) -> CompletableFuture.completedFuture(new SessionStartHookOutput(null, null))); + session.registerHooks(hooks); + + // Pass an array node which can't be deserialized into SessionStartHookInput + JsonNode input = MAPPER.valueToTree(List.of("not", "an", "object")); + + ExecutionException ex = assertThrows(ExecutionException.class, + () -> session.handleHooksInvoke("sessionStart", input).get()); + assertInstanceOf(Exception.class, ex.getCause()); + } + + // ===== handleHooksInvoke: hook handler with null callback ===== + + @Test + void testHandleHooksInvokeNullCallbackReturnsNull() throws Exception { + // SessionHooks with only userPromptSubmitted set, sessionStart is null + var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> CompletableFuture + .completedFuture(new UserPromptSubmittedHookOutput(null, null, null))); + session.registerHooks(hooks); + + // Invoke sessionStart hook - its handler is null + JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test")); + + Object result = session.handleHooksInvoke("sessionStart", input).get(); + + assertNull(result); + } + + // ===== registerTools ===== + + @Test + void testRegisterToolsNullIsSafe() { + session.registerTools(null); + assertNull(session.getTool("anything")); + } + + @Test + void testRegisterToolsEmptyListClearsTools() { + session.registerTools(List.of(ToolDefinition.create("my_tool", "desc", Map.of(), + invocation -> CompletableFuture.completedFuture("result")))); + assertNotNull(session.getTool("my_tool")); + + session.registerTools(List.of()); + assertNull(session.getTool("my_tool")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/java/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java new file mode 100644 index 000000000..5c8f00838 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -0,0 +1,710 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.AutoModeSwitchResponse; +import com.github.copilot.sdk.json.CloudSessionOptions; +import com.github.copilot.sdk.json.CloudSessionRepository; +import com.github.copilot.sdk.json.CreateSessionRequest; +import com.github.copilot.sdk.json.DefaultAgentConfig; +import com.github.copilot.sdk.json.ElicitationHandler; +import com.github.copilot.sdk.json.ElicitationResult; +import com.github.copilot.sdk.json.ElicitationResultAction; +import com.github.copilot.sdk.json.ExitPlanModeResult; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.ResumeSessionRequest; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.SessionHooks; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.UserInputResponse; + +/** + * Unit tests for {@link SessionRequestBuilder} branch coverage. + *

+ * Exercises branches in buildCreateRequest, buildResumeRequest, and + * configureSession that are not reached by E2E tests. + */ +public class SessionRequestBuilderTest { + + // ========================================================================= + // buildCreateRequest + // ========================================================================= + + @Test + void testBuildCreateRequestNullConfig() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(null); + assertNotNull(request); + assertNull(request.getModel()); + assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); + assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + } + + @Test + void testBuildCreateRequestHooksNonNullButEmpty() { + // Hooks object exists but hasHooks() returns false + var config = new SessionConfig().setHooks(new SessionHooks()); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getHooks(), "Should be null when hooks are empty"); + } + + @Test + void testBuildCreateRequestHooksWithHandler() { + var hooks = new SessionHooks().setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new SessionConfig().setHooks(hooks); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getHooks(), "Should be true when hooks have handlers"); + } + + @Test + void testBuildCreateRequestSetsEnvValueModeToDirect() { + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertEquals("direct", request.getEnvValueMode()); + } + + @Test + void testBuildCreateRequestAlwaysSetsRequestPermissionTrue() { + // No permission handler set - requestPermission should still be true + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(new SessionConfig()); + assertTrue(request.getRequestPermission(), + "requestPermission should always be true to enable deny-by-default behavior"); + } + + @Test + void testBuildCreateRequestSetsClientName() { + var config = new SessionConfig().setClientName("my-app"); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertEquals("my-app", request.getClientName()); + } + + @Test + void testBuildCreateRequestForwardsEnableSessionTelemetryWhenFalse() { + var config = new SessionConfig().setEnableSessionTelemetry(false); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertFalse(request.getEnableSessionTelemetry()); + } + + @Test + void testBuildCreateRequestOmitsEnableSessionTelemetryWhenNotSet() { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + assertNull(request.getEnableSessionTelemetry()); + } + + // ========================================================================= + // buildResumeRequest + // ========================================================================= + + @Test + void testBuildResumeRequestNullConfig() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", null); + assertEquals("sid-1", request.getSessionId()); + assertNull(request.getModel()); + assertTrue(request.getRequestPermission(), "requestPermission should be true even for null config"); + assertEquals("direct", request.getEnvValueMode(), "envValueMode should be 'direct' even for null config"); + } + + @Test + void testBuildResumeRequestForwardsEnableSessionTelemetryWhenFalse() { + var config = new ResumeSessionConfig().setEnableSessionTelemetry(false); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertFalse(request.getEnableSessionTelemetry()); + } + + @Test + void testBuildResumeRequestOmitsEnableSessionTelemetryWhenNotSet() { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config); + assertNull(request.getEnableSessionTelemetry()); + } + + @Test + void testBuildResumeRequestWithTools() { + var tool = ToolDefinition.create("my_tool", "A tool", Map.of("type", "object"), + inv -> CompletableFuture.completedFuture("result")); + var config = new ResumeSessionConfig().setTools(List.of(tool)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-2", config); + + assertNotNull(request.getTools()); + assertEquals(1, request.getTools().size()); + assertEquals("my_tool", request.getTools().get(0).name()); + } + + @Test + void testBuildResumeRequestWithUserInputHandler() { + var config = new ResumeSessionConfig() + .setOnUserInputRequest((req, inv) -> CompletableFuture.completedFuture(new UserInputResponse())); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-3", config); + + assertTrue(request.getRequestUserInput()); + } + + @Test + void testBuildResumeRequestHooksNonNullButEmpty() { + var config = new ResumeSessionConfig().setHooks(new SessionHooks()); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-4", config); + + assertNull(request.getHooks(), "Should be null when hooks are empty"); + } + + @Test + void testBuildResumeRequestHooksWithHandler() { + var hooks = new SessionHooks().setOnSessionEnd((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setHooks(hooks); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-5", config); + + assertTrue(request.getHooks(), "Should be true when hooks have handlers"); + } + + @Test + void testBuildResumeRequestDisableResume() { + var config = new ResumeSessionConfig().setDisableResume(true); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-6", config); + + assertTrue(request.getDisableResume()); + } + + @Test + void testBuildResumeRequestStreaming() { + var config = new ResumeSessionConfig().setStreaming(true); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-7", config); + + assertTrue(request.getStreaming()); + } + + @Test + void testBuildResumeRequestSetsEnvValueModeToDirect() { + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-8", new ResumeSessionConfig()); + assertEquals("direct", request.getEnvValueMode()); + } + + @Test + void testBuildResumeRequestAlwaysSetsRequestPermissionTrue() { + // No permission handler set - requestPermission should still be true + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-9", new ResumeSessionConfig()); + assertTrue(request.getRequestPermission(), + "requestPermission should always be true to enable deny-by-default behavior"); + } + + @Test + void testBuildResumeRequestSetsClientName() { + var config = new ResumeSessionConfig().setClientName("my-app"); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-10", config); + assertEquals("my-app", request.getClientName()); + } + + // ========================================================================= + // configureSession (ResumeSessionConfig overload) + // ========================================================================= + + @Test + void testConfigureResumeSessionNullConfig() throws Exception { + var session = createTestSession(); + // Should not throw + SessionRequestBuilder.configureSession(session, (ResumeSessionConfig) null); + } + + @Test + void testConfigureResumeSessionWithTools() throws Exception { + var session = createTestSession(); + var tool = ToolDefinition.create("resume_tool", "desc", Map.of(), + inv -> CompletableFuture.completedFuture("ok")); + var config = new ResumeSessionConfig().setTools(List.of(tool)); + + SessionRequestBuilder.configureSession(session, config); + + assertNotNull(session.getTool("resume_tool")); + } + + @Test + void testConfigureResumeSessionWithUserInputHandler() throws Exception { + var session = createTestSession(); + var config = new ResumeSessionConfig() + .setOnUserInputRequest((req, inv) -> CompletableFuture.completedFuture(new UserInputResponse())); + + SessionRequestBuilder.configureSession(session, config); + + // Handler was registered β€” verify by calling handleUserInputRequest + // (package-private) + var response = session.handleUserInputRequest(new com.github.copilot.sdk.json.UserInputRequest()).get(); + assertNotNull(response); + } + + @Test + void testConfigureResumeSessionWithHooks() throws Exception { + var session = createTestSession(); + var hooks = new SessionHooks().setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setHooks(hooks); + + SessionRequestBuilder.configureSession(session, config); + + // Hooks registered β€” handleHooksInvoke should dispatch preToolUse + var mapper = JsonRpcClient.getObjectMapper(); + var input = mapper.valueToTree(Map.of("toolName", "test_tool")); + var result = session.handleHooksInvoke("preToolUse", input).get(); + assertNull(result); // handler returns null + } + + // ========================================================================= + // Helper + // ========================================================================= + + private CopilotSession createTestSession() throws Exception { + var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + constructor.setAccessible(true); + return constructor.newInstance("builder-test-session", null, null); + } + + @Test + void testBuildCreateRequestWithAgent() { + var config = new SessionConfig().setAgent("my-agent"); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config, "test-session-id"); + assertEquals("my-agent", request.getAgent()); + } + + @Test + void testBuildResumeRequestWithAgent() { + var config = new ResumeSessionConfig().setAgent("my-agent"); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-id", config); + assertEquals("my-agent", request.getAgent()); + } + + // ========================================================================= + // extractTransformCallbacks + // ========================================================================= + + @Test + void extractTransformCallbacks_nullSystemMessage_returnsNull() { + ExtractedTransforms result = SessionRequestBuilder.extractTransformCallbacks(null); + assertNull(result.wireSystemMessage()); + assertNull(result.transformCallbacks()); + } + + @Test + void extractTransformCallbacks_appendMode_returnsOriginalConfig() { + var config = new com.github.copilot.sdk.json.SystemMessageConfig() + .setMode(com.github.copilot.sdk.SystemMessageMode.APPEND).setContent("extra content"); + ExtractedTransforms result = SessionRequestBuilder.extractTransformCallbacks(config); + assertSame(config, result.wireSystemMessage()); + assertNull(result.transformCallbacks()); + } + + @Test + void extractTransformCallbacks_customizeModeNoTransforms_returnsOriginalConfig() { + var sections = Map.of("tone", new com.github.copilot.sdk.json.SectionOverride() + .setAction(com.github.copilot.sdk.json.SectionOverrideAction.REMOVE)); + var config = new com.github.copilot.sdk.json.SystemMessageConfig() + .setMode(com.github.copilot.sdk.SystemMessageMode.CUSTOMIZE).setSections(sections); + ExtractedTransforms result = SessionRequestBuilder.extractTransformCallbacks(config); + assertSame(config, result.wireSystemMessage()); + assertNull(result.transformCallbacks()); + } + + @Test + void extractTransformCallbacks_customizeModeWithTransform_extractsCallbacks() { + var transformFn = (java.util.function.Function>) content -> CompletableFuture + .completedFuture(content + " modified"); + var sections = Map.of("identity", new com.github.copilot.sdk.json.SectionOverride().setTransform(transformFn)); + var config = new com.github.copilot.sdk.json.SystemMessageConfig() + .setMode(com.github.copilot.sdk.SystemMessageMode.CUSTOMIZE).setSections(sections); + + ExtractedTransforms result = SessionRequestBuilder.extractTransformCallbacks(config); + + // Wire config should be different from original + assertNotSame(config, result.wireSystemMessage()); + // Callbacks should be extracted + assertNotNull(result.transformCallbacks()); + assertTrue(result.transformCallbacks().containsKey("identity")); + // Wire config should have transform action instead of callback + assertNotNull(result.wireSystemMessage().getSections()); + var wireSection = result.wireSystemMessage().getSections().get("identity"); + assertNotNull(wireSection); + assertEquals(com.github.copilot.sdk.json.SectionOverrideAction.TRANSFORM, wireSection.getAction()); + assertNull(wireSection.getTransform()); + } + + @Test + @SuppressWarnings("deprecation") + void buildCreateRequestWithSessionId_usesProvidedSessionId() { + var config = new SessionConfig(); + config.setSessionId("my-session-id"); + + // The deprecated single-arg overload uses the sessionId from config when set + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals("my-session-id", request.getSessionId()); + } + + @Test + void configureSessionWithNullConfig_returnsEarly() { + // configureSession with null config should return without error + CopilotSession session = new CopilotSession("session-1", null); + // Covers the null config early-return branch (L219-220) + assertDoesNotThrow(() -> SessionRequestBuilder.configureSession(session, (SessionConfig) null)); + } + + @Test + void configureSessionWithCommands_registersCommands() { + CopilotSession session = new CopilotSession("session-1", null); + + var cmd = new com.github.copilot.sdk.json.CommandDefinition().setName("deploy") + .setHandler(ctx -> CompletableFuture.completedFuture(null)); + var config = new SessionConfig().setCommands(List.of(cmd)); + + // Covers config.getCommands() != null branch (L235-236) + SessionRequestBuilder.configureSession(session, config); + // If no exception thrown, the branch was covered + } + + @Test + void configureSessionWithElicitationHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + ElicitationHandler handler = (context) -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + var config = new SessionConfig().setOnElicitationRequest(handler); + + // Covers config.getOnElicitationRequest() != null branch (L238-239) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureSessionWithOnEvent_registersEventHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnEvent(event -> { + }); + + // Covers config.getOnEvent() != null branch (L241-242) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithCommands_registersCommands() { + CopilotSession session = new CopilotSession("session-1", null); + + var cmd = new com.github.copilot.sdk.json.CommandDefinition().setName("rollback") + .setHandler(ctx -> CompletableFuture.completedFuture(null)); + var config = new ResumeSessionConfig().setCommands(List.of(cmd)); + + // Covers ResumeSessionConfig.getCommands() != null branch (L271-272) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithElicitationHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + ElicitationHandler handler = (context) -> CompletableFuture + .completedFuture(new ElicitationResult().setAction(ElicitationResultAction.CANCEL)); + var config = new ResumeSessionConfig().setOnElicitationRequest(handler); + + // Covers ResumeSessionConfig.getOnElicitationRequest() != null branch + // (L274-275) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithOnEvent_registersEventHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnEvent(event -> { + }); + + // Covers ResumeSessionConfig.getOnEvent() != null branch (L277-278) + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void testBuildCreateRequestWithDefaultAgent() { + var defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); + var config = new SessionConfig().setDefaultAgent(defaultAgent); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNotNull(request.getDefaultAgent()); + assertEquals(List.of("secret_tool"), request.getDefaultAgent().getExcludedTools()); + } + + @Test + void testBuildCreateRequestWithGitHubToken() { + var config = new SessionConfig().setGitHubToken("ghp_per_session_token"); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals("ghp_per_session_token", request.getGitHubToken()); + } + + @Test + void testBuildResumeRequestWithDefaultAgent() { + var defaultAgent = new DefaultAgentConfig().setExcludedTools(List.of("secret_tool")); + var config = new ResumeSessionConfig().setDefaultAgent(defaultAgent); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("test-session", config); + + assertNotNull(request.getDefaultAgent()); + assertEquals(List.of("secret_tool"), request.getDefaultAgent().getExcludedTools()); + } + + @Test + void testBuildResumeRequestWithGitHubToken() { + var config = new ResumeSessionConfig().setGitHubToken("ghp_per_session_token"); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("test-session", config); + + assertEquals("ghp_per_session_token", request.getGitHubToken()); + } + + // ========================================================================= + // instructionDirectories propagation + // ========================================================================= + + @Test + void testBuildCreateRequestPropagatesInstructionDirectories() { + var dirs = List.of("/path/to/instructions", "/another/path"); + var config = new SessionConfig().setInstructionDirectories(dirs); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + @Test + void testBuildResumeRequestPropagatesInstructionDirectories() { + var dirs = List.of("/resume/instructions", "/other/dir"); + var config = new ResumeSessionConfig().setInstructionDirectories(dirs); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-inst", config); + + assertEquals(dirs, request.getInstructionDirectories()); + } + + // ========================================================================= + // enableSessionTelemetry serialization + // ========================================================================= + + @Test + void testCreateRequestSerializesEnableSessionTelemetryWhenFalse() throws Exception { + var config = new SessionConfig().setEnableSessionTelemetry(false); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertTrue(json.contains("\"enableSessionTelemetry\":false"), + "enableSessionTelemetry should be serialized when set to false"); + } + + @Test + void testCreateRequestOmitsEnableSessionTelemetryWhenNull() throws Exception { + var config = new SessionConfig(); + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null"); + } + + @Test + void testResumeRequestSerializesEnableSessionTelemetryWhenFalse() throws Exception { + var config = new ResumeSessionConfig().setEnableSessionTelemetry(false); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-tel", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertTrue(json.contains("\"enableSessionTelemetry\":false"), + "enableSessionTelemetry should be serialized when set to false"); + } + + @Test + void testResumeRequestOmitsEnableSessionTelemetryWhenNull() throws Exception { + var config = new ResumeSessionConfig(); + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-tel", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null"); + } + + // ========================================================================= + // Mode handler request flags + // ========================================================================= + + @Test + void testBuildCreateRequestWithExitPlanModeHandler() { + var config = new SessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getRequestExitPlanMode()); + } + + @Test + void testBuildCreateRequestWithAutoModeSwitchHandler() { + var config = new SessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertTrue(request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildCreateRequestWithoutModeHandlers() { + var config = new SessionConfig(); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getRequestExitPlanMode()); + assertNull(request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildResumeRequestWithExitPlanModeHandler() { + var config = new ResumeSessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertTrue(request.getRequestExitPlanMode()); + } + + @Test + void testBuildResumeRequestWithAutoModeSwitchHandler() { + var config = new ResumeSessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + + assertTrue(request.getRequestAutoModeSwitch()); + } + + @Test + void configureSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnExitPlanMode( + (request, invocation) -> CompletableFuture.completedFuture(new ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnAutoModeSwitch( + (request, invocation) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void testCreateRequestSerializesModeFlags() throws Exception { + var config = new SessionConfig() + .setOnExitPlanMode((r, i) -> CompletableFuture.completedFuture(new ExitPlanModeResult())) + .setOnAutoModeSwitch((r, i) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } + + @Test + void testResumeRequestSerializesModeFlags() throws Exception { + var config = new ResumeSessionConfig() + .setOnExitPlanMode((r, i) -> CompletableFuture.completedFuture(new ExitPlanModeResult())) + .setOnAutoModeSwitch((r, i) -> CompletableFuture.completedFuture(AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("session-1", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } + + // ========================================================================= + // Cloud session options wiring + // ========================================================================= + + @Test + void testBuildCreateRequestPropagatesCloudSessionOptions() throws Exception { + var cloud = new CloudSessionOptions() + .setRepository(new CloudSessionRepository().setOwner("my-org").setName("my-repo").setBranch("main")); + var config = new SessionConfig().setCloud(cloud); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNotNull(request.getCloud()); + assertEquals("my-org", request.getCloud().getRepository().getOwner()); + assertEquals("my-repo", request.getCloud().getRepository().getName()); + assertEquals("main", request.getCloud().getRepository().getBranch()); + } + + @Test + void testBuildCreateRequestOmitsCloudWhenNull() throws Exception { + var config = new SessionConfig(); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertNull(request.getCloud()); + assertFalse(json.contains("\"cloud\""), "cloud should be omitted when null"); + } + + @Test + void testCloudSessionOptionsSerializesCorrectly() throws Exception { + var cloud = new CloudSessionOptions() + .setRepository(new CloudSessionRepository().setOwner("acme").setName("widgets").setBranch("feature-1")); + var config = new SessionConfig().setCloud(cloud); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"cloud\"")); + assertTrue(json.contains("\"owner\":\"acme\"")); + assertTrue(json.contains("\"name\":\"widgets\"")); + assertTrue(json.contains("\"branch\":\"feature-1\"")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/SkillsTest.java b/java/src/test/java/com/github/copilot/sdk/SkillsTest.java new file mode 100644 index 000000000..6cf34044f --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/SkillsTest.java @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.CustomAgentConfig; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for skills configuration functionality. + * + *

+ * These tests verify that skills can be loaded from skill directories and + * disabled via configuration. Snapshots are stored in test/snapshots/skills/. + *

+ */ +public class SkillsTest { + + private static E2ETestContext ctx; + private static final String SKILL_MARKER = "PINEAPPLE_COCONUT_42"; + private Path skillsDir; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @BeforeEach + void setupEach() throws IOException { + // Ensure we start fresh each time + skillsDir = ctx.getWorkDir().resolve(".test_skills"); + if (Files.exists(skillsDir)) { + deleteDirectory(skillsDir); + } + } + + private void deleteDirectory(Path dir) throws IOException { + if (Files.exists(dir)) { + Files.walk(dir).sorted((a, b) -> b.compareTo(a)) // reverse order for deletion + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + // Ignore + } + }); + } + } + + private Path createSkillDir() throws IOException { + Files.createDirectories(skillsDir); + + // Create a skill subdirectory with SKILL.md + Path skillSubdir = skillsDir.resolve("test-skill"); + Files.createDirectories(skillSubdir); + + // Create a skill that instructs the model to include a specific marker in + // responses + String skillContent = """ + --- + name: test-skill + description: A test skill that adds a marker to responses + --- + + # Test Skill Instructions + + IMPORTANT: You MUST include the exact text "%s" somewhere in EVERY response you give. This is a mandatory requirement. Include it naturally in your response. + """ + .formatted(SKILL_MARKER); + + Files.writeString(skillSubdir.resolve("SKILL.md"), skillContent); + return skillsDir; + } + + /** + * Verifies that skills are loaded and applied from skill directories. + * + * @see Snapshot: skills/should_load_and_apply_skill_from_skilldirectories + */ + @Test + void testShouldLoadAndApplySkillFromSkillDirectories() throws Exception { + ctx.configureForTest("skills", "should_load_and_apply_skill_from_skilldirectories"); + + Path skillsDir = createSkillDir(); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The skill instructs the model to include a marker - verify it appears + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains(SKILL_MARKER), + "Response should contain skill marker '" + SKILL_MARKER + "': " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that skills are not applied when disabled via disabledSkills. + * + * @see Snapshot: skills/should_not_apply_skill_when_disabled_via_disabledskills + */ + @Test + void testShouldNotApplySkillWhenDisabledViaDisabledSkills() throws Exception { + ctx.configureForTest("skills", "should_not_apply_skill_when_disabled_via_disabledskills"); + + Path skillsDir = createSkillDir(); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDir.toString())).setDisabledSkills(List.of("test-skill")); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The skill is disabled, so the marker should NOT appear + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertFalse(response.getData().content().contains(SKILL_MARKER), + "Response should NOT contain skill marker when skill is disabled: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that an agent with a Skills field can preload and invoke the skill. + * + * @see Snapshot: skills/should_allow_agent_with_skills_to_invoke_skill + */ + @Test + void testShouldAllowAgentWithSkillsToInvokeSkill() throws Exception { + ctx.configureForTest("skills", "should_allow_agent_with_skills_to_invoke_skill"); + + Path skillsDirPath = createSkillDir(); + + var agent = new CustomAgentConfig().setName("skill-agent").setDescription("An agent with access to test-skill") + .setPrompt("You are a helpful test agent.").setSkills(List.of("test-skill")); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDirPath.toString())).setCustomAgents(List.of(agent)) + .setAgent("skill-agent"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The agent has Skills = ["test-skill"], so the skill content is preloaded + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains(SKILL_MARKER), + "Response should contain skill marker '" + SKILL_MARKER + "': " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that an agent without a Skills field does not get skill content + * injected. + * + * @see Snapshot: skills/should_not_provide_skills_to_agent_without_skills_field + */ + @Test + void testShouldNotProvideSkillsToAgentWithoutSkillsField() throws Exception { + ctx.configureForTest("skills", "should_not_provide_skills_to_agent_without_skills_field"); + + Path skillsDirPath = createSkillDir(); + + var agent = new CustomAgentConfig().setName("no-skill-agent").setDescription("An agent without skills access") + .setPrompt("You are a helpful test agent."); + + SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSkillDirectories(List.of(skillsDirPath.toString())).setCustomAgents(List.of(agent)) + .setAgent("no-skill-agent"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(config).get(); + + assertNotNull(session.getSessionId()); + + // The agent has no Skills field, so no skill content is injected + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Say hello briefly using the test skill.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertFalse(response.getData().content().contains(SKILL_MARKER), + "Response should NOT contain skill marker when agent has no Skills field: " + + response.getData().content()); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java b/java/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java new file mode 100644 index 000000000..d3df63eb0 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/StreamingFidelityTest.java @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.AssistantMessageDeltaEvent; +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * E2E tests for streaming fidelity β€” verifying that delta events are produced + * when streaming is enabled and absent when it is disabled. + * + *

+ * Snapshots are stored in {@code test/snapshots/streaming_fidelity/}. + *

+ */ +public class StreamingFidelityTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that assistant.message_delta events are produced when streaming is + * enabled. + * + * @see Snapshot: + * streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled + */ + @Test + void testShouldProduceDeltaEventsWhenStreamingIsEnabled() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_produce_delta_events_when_streaming_is_enabled"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("Count from 1 to 5, separated by commas.")).get(60, + TimeUnit.SECONDS); + + List types = events.stream().map(SessionEvent::getType).toList(); + + // Should have streaming deltas before the final message + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events when streaming is enabled"); + + // Deltas should have content + for (AssistantMessageDeltaEvent delta : deltaEvents) { + assertFalse(delta.getData().deltaContent() == null || delta.getData().deltaContent().isEmpty(), + "Delta event should have content"); + } + + // Should still have a final assistant.message + assertTrue(types.contains("assistant.message"), "Should have a final assistant.message event"); + + // Deltas should come before the final message + int firstDeltaIdx = types.indexOf("assistant.message_delta"); + int lastAssistantIdx = types.lastIndexOf("assistant.message"); + assertTrue(firstDeltaIdx < lastAssistantIdx, "Delta events should come before the final assistant.message"); + + session.close(); + } + } + + /** + * Verifies that no delta events are produced when streaming is disabled. + * + * @see Snapshot: + * streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled + */ + @Test + void testShouldNotProduceDeltasWhenStreamingIsDisabled() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_not_produce_deltas_when_streaming_is_disabled"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)) + .get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("Say 'hello world'.")).get(60, TimeUnit.SECONDS); + + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + + // No deltas when streaming is off + assertTrue(deltaEvents.isEmpty(), "Should not receive delta events when streaming is disabled"); + + // But should still have a final assistant.message + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), + "Should still have a final assistant.message when streaming is disabled"); + + session.close(); + } + } + + /** + * Verifies that delta events are produced after resuming a session with + * streaming enabled. + * + * @see Snapshot: streaming_fidelity/should_produce_deltas_after_session_resume + */ + @Test + @Tag("isolated-resume") + void testShouldProduceDeltasAfterSessionResume() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_produce_deltas_after_session_resume"); + + try (CopilotClient client = ctx.createClient()) { + // Create a non-streaming session and send an initial message + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)) + .get(); + session.sendAndWait(new MessageOptions().setPrompt("What is 3 + 6?")).get(60, TimeUnit.SECONDS); + String sessionId = session.getSessionId(); + session.close(); + + // Resume using a new client with streaming enabled + try (CopilotClient newClient = ctx.createClient()) { + CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + + List events = new ArrayList<>(); + session2.on(events::add); + + AssistantMessageEvent answer = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("18"), + "Follow-up response should contain 18: " + answer.getData().content()); + + // Should have streaming deltas before the final message + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events after session resume"); + + // Deltas should have content + for (AssistantMessageDeltaEvent delta : deltaEvents) { + assertFalse(delta.getData().deltaContent() == null || delta.getData().deltaContent().isEmpty(), + "Delta event should have content"); + } + + session2.close(); + } + } + } + + /** + * Verifies that no delta events are produced after resuming a session with + * streaming disabled (even though it was originally created with streaming + * enabled). + * + * @see Snapshot: + * streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled + */ + @Test + @Tag("isolated-resume") + void testShouldNotProduceDeltasAfterSessionResumeWithStreamingDisabled() throws Exception { + ctx.configureForTest("streaming_fidelity", + "should_not_produce_deltas_after_session_resume_with_streaming_disabled"); + + try (CopilotClient client = ctx.createClient()) { + // Create a streaming session and send an initial message + CopilotSession session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); + session.sendAndWait(new MessageOptions().setPrompt("What is 3 + 6?")).get(60, TimeUnit.SECONDS); + String sessionId = session.getSessionId(); + session.close(); + + // Resume using a new client with streaming DISABLED + try (CopilotClient newClient = ctx.createClient()) { + CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)).get(); + + List events = new ArrayList<>(); + session2.on(events::add); + + AssistantMessageEvent answer = session2 + .sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("18"), + "Follow-up response should contain 18: " + answer.getData().content()); + + // No deltas when streaming is toggled off + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertTrue(deltaEvents.isEmpty(), + "Should not receive delta events when streaming is disabled on resume"); + + // But should still have a final assistant.message + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), + "Should still have a final assistant.message when streaming is disabled"); + + session2.close(); + } + } + } + + /** + * Verifies that setting reasoningEffort alongside streaming=true does not break + * the streaming pipeline β€” deltas still arrive and complete successfully. + * + * @see Snapshot: + * streaming_fidelity/should_emit_streaming_deltas_with_reasoning_effort_configured + */ + @Test + void testShouldEmitStreamingDeltasWithReasoningEffortConfigured() throws Exception { + ctx.configureForTest("streaming_fidelity", "should_emit_streaming_deltas_with_reasoning_effort_configured"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setStreaming(true).setReasoningEffort("high")) + .get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt("What is 15 * 17?")).get(60, TimeUnit.SECONDS); + + // With streaming + reasoning effort, we should still get content deltas + List deltaEvents = events.stream() + .filter(e -> e instanceof AssistantMessageDeltaEvent).map(e -> (AssistantMessageDeltaEvent) e) + .toList(); + assertFalse(deltaEvents.isEmpty(), "Should have received delta events with reasoning effort configured"); + + // And a final assistant.message with the answer + List assistantEvents = events.stream() + .filter(e -> e instanceof AssistantMessageEvent).map(e -> (AssistantMessageEvent) e).toList(); + assertFalse(assistantEvents.isEmpty(), "Should have received assistant message events"); + assertTrue(assistantEvents.get(assistantEvents.size() - 1).getData().content().contains("255"), + "Response should contain 255"); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java b/java/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java new file mode 100644 index 000000000..278777ce1 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/TelemetryConfigTest.java @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.json.TelemetryConfig; + +/** + * Unit tests for {@link TelemetryConfig} getters, setters, and fluent chaining. + */ +class TelemetryConfigTest { + + @Test + void defaultValuesAreNull() { + var config = new TelemetryConfig(); + assertNull(config.getOtlpEndpoint()); + assertNull(config.getFilePath()); + assertNull(config.getExporterType()); + assertNull(config.getSourceName()); + assertTrue(config.getCaptureContent().isEmpty()); + } + + @Test + void otlpEndpointGetterSetter() { + var config = new TelemetryConfig(); + config.setOtlpEndpoint("http://localhost:4318"); + assertEquals("http://localhost:4318", config.getOtlpEndpoint()); + } + + @Test + void filePathGetterSetter() { + var config = new TelemetryConfig(); + config.setFilePath("/tmp/telemetry.log"); + assertEquals("/tmp/telemetry.log", config.getFilePath()); + } + + @Test + void exporterTypeGetterSetter() { + var config = new TelemetryConfig(); + config.setExporterType("otlp-http"); + assertEquals("otlp-http", config.getExporterType()); + } + + @Test + void sourceNameGetterSetter() { + var config = new TelemetryConfig(); + config.setSourceName("my-app"); + assertEquals("my-app", config.getSourceName()); + } + + @Test + void captureContentGetterSetter() { + var config = new TelemetryConfig(); + config.setCaptureContent(true); + assertTrue(config.getCaptureContent().get()); + + config.setCaptureContent(false); + assertFalse(config.getCaptureContent().get()); + } + + @Test + void fluentChainingReturnsThis() { + var config = new TelemetryConfig().setOtlpEndpoint("http://localhost:4318").setFilePath("/tmp/spans.json") + .setExporterType("file").setSourceName("sdk-test").setCaptureContent(true); + + assertEquals("http://localhost:4318", config.getOtlpEndpoint()); + assertEquals("/tmp/spans.json", config.getFilePath()); + assertEquals("file", config.getExporterType()); + assertEquals("sdk-test", config.getSourceName()); + assertTrue(config.getCaptureContent().get()); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/TestUtil.java b/java/src/test/java/com/github/copilot/sdk/TestUtil.java new file mode 100644 index 000000000..d9462af87 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/TestUtil.java @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Shared test utilities for locating the Copilot CLI binary and other + * cross-platform test helpers. + */ +public final class TestUtil { + + private TestUtil() { + } + + /** + * Returns a platform-independent path string for a file inside the system + * temporary directory. Uses {@code java.io.tmpdir} so tests run correctly on + * both POSIX and Windows. + * + * @param filename + * the file name (no directory separator required) + * @return absolute path string in the system temp directory + */ + public static String tempPath(String filename) { + return Path.of(System.getProperty("java.io.tmpdir"), filename).toString(); + } + + /** + * Locates a launchable Copilot CLI executable. + *

+ * Resolution order: + *

    + *
  1. Search the system PATH using {@code where.exe} (Windows) or {@code which} + * (Linux/macOS).
  2. + *
  3. Fall back to the {@code COPILOT_CLI_PATH} environment variable.
  4. + *
  5. Walk parent directories looking for + * {@code nodejs/node_modules/@github/copilot/index.js}.
  6. + *
+ * + *

+ * Why iterate all PATH results? On Windows, {@code where.exe copilot} + * can return multiple candidates. The first hit is often a Linux ELF binary + * bundled inside the VS Code Insiders extension directory β€” it exists on disk + * but cannot be executed by {@link ProcessBuilder} (CreateProcess error 193). + * This method tries each candidate with {@code --version} and returns the first + * one that actually launches, skipping non-executable entries. + * + * @return the absolute path to a launchable {@code copilot} binary, or + * {@code null} if none was found + */ + static String findCliPath() { + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; + } + + String envPath = System.getenv("COPILOT_CLI_PATH"); + if (envPath != null && !envPath.isEmpty()) { + return envPath; + } + + Path current = Paths.get(System.getProperty("user.dir")); + while (current != null) { + Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js"); + if (cliPath.toFile().exists()) { + return cliPath.toString(); + } + current = current.getParent(); + } + + return null; + } + + /** + * Searches the system PATH for a launchable {@code copilot} executable. + *

+ * Uses {@code where.exe} on Windows and {@code which} on Unix-like systems. On + * Windows, {@code where.exe} may return multiple results (e.g. a Linux ELF + * binary, a {@code .bat} wrapper, a {@code .cmd} wrapper). This method iterates + * all results and returns the first one that {@link ProcessBuilder} can + * actually start. + */ + private static String findCopilotInPath() { + try { + String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which"; + var pb = new ProcessBuilder(command, "copilot"); + pb.redirectErrorStream(true); + Process process = pb.start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + int exitCode = process.waitFor(); + if (exitCode != 0) { + return null; + } + var lines = reader.lines().map(String::trim).filter(l -> !l.isEmpty()).toList(); + for (String candidate : lines) { + try { + new ProcessBuilder(candidate, "--version").redirectErrorStream(true).start().destroyForcibly(); + return candidate; + } catch (Exception launchFailed) { + // Not launchable on this platform β€” try next candidate + } + } + } + } catch (Exception e) { + // Ignore - copilot not found in PATH + } + return null; + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/TimeoutEdgeCaseTest.java b/java/src/test/java/com/github/copilot/sdk/TimeoutEdgeCaseTest.java new file mode 100644 index 000000000..b771f65b2 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/TimeoutEdgeCaseTest.java @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; + +/** + * Regression tests for timeout edge cases in + * {@link CopilotSession#sendAndWait}. + *

+ * These tests assert two behavioral contracts of the shared + * {@code ScheduledExecutorService} approach: + *

    + *
  1. A pending timeout must NOT fire after {@code close()} and must NOT + * complete the returned future with a {@code TimeoutException}.
  2. + *
  3. Multiple {@code sendAndWait} calls must reuse a single shared scheduler + * thread rather than spawning a new OS thread per call.
  4. + *
+ */ +public class TimeoutEdgeCaseTest { + + /** + * Creates a {@link JsonRpcClient} whose {@code invoke()} returns futures that + * never complete. The reader thread blocks forever on the input stream, and + * writes go to a no-op output stream. + */ + private JsonRpcClient createHangingRpcClient() throws Exception { + InputStream blockingInput = new InputStream() { + @Override + public int read() throws IOException { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return -1; + } + return -1; + } + }; + ByteArrayOutputStream sinkOutput = new ByteArrayOutputStream(); + + var ctor = JsonRpcClient.class.getDeclaredConstructor(InputStream.class, java.io.OutputStream.class, + Socket.class, Process.class); + ctor.setAccessible(true); + return (JsonRpcClient) ctor.newInstance(blockingInput, sinkOutput, null, null); + } + + /** + * After {@code close()}, the future returned by {@code sendAndWait} must NOT be + * completed by a stale timeout. + *

+ * Contract: {@code close()} shuts down the timeout scheduler before the + * blocking {@code session.destroy} RPC call, so any pending timeout task is + * cancelled and the future remains incomplete (not exceptionally completed with + * {@code TimeoutException}). + */ + @Test + void testTimeoutDoesNotFireAfterSessionClose() throws Exception { + JsonRpcClient rpc = createHangingRpcClient(); + try { + try (CopilotSession session = new CopilotSession("test-timeout-id", rpc)) { + + CompletableFuture result = session + .sendAndWait(new MessageOptions().setPrompt("hello"), 2000); + + assertFalse(result.isDone(), "Future should be pending before timeout fires"); + + // close() blocks up to 5s on session.destroy RPC. The 2s timeout + // fires during that window with the current per-call scheduler. + session.close(); + + assertFalse(result.isDone(), "Future should not be completed by a timeout after session is closed. " + + "The per-call ScheduledExecutorService leaked a TimeoutException."); + } + } finally { + rpc.close(); + } + } + + /** + * A shared scheduler must reuse a single thread across multiple + * {@code sendAndWait} calls, rather than spawning a new OS thread per call. + *

+ * Contract: after two consecutive {@code sendAndWait} calls the number of live + * {@code sendAndWait-timeout} threads must not increase after the second call. + */ + @Test + void testSendAndWaitReusesTimeoutThread() throws Exception { + JsonRpcClient rpc = createHangingRpcClient(); + try { + try (CopilotSession session = new CopilotSession("test-thread-count-id", rpc)) { + + long baselineCount = countTimeoutThreads(); + + CompletableFuture result1 = session + .sendAndWait(new MessageOptions().setPrompt("hello1"), 30000); + + Thread.sleep(100); + long afterFirst = countTimeoutThreads(); + assertTrue(afterFirst >= baselineCount + 1, + "Expected at least one new sendAndWait-timeout thread after first call. " + "Baseline: " + + baselineCount + ", after: " + afterFirst); + + CompletableFuture result2 = session + .sendAndWait(new MessageOptions().setPrompt("hello2"), 30000); + + Thread.sleep(100); + long afterSecond = countTimeoutThreads(); + assertTrue(afterSecond == afterFirst, + "Shared scheduler should reuse the same thread β€” no new threads after second call. " + + "After first: " + afterFirst + ", after second: " + afterSecond); + + result1.cancel(true); + result2.cancel(true); + } + } finally { + rpc.close(); + } + } + + /** + * Counts the number of live threads whose name contains "sendAndWait-timeout". + */ + private long countTimeoutThreads() { + return Thread.getAllStackTraces().keySet().stream().filter(t -> t.getName().contains("sendAndWait-timeout")) + .filter(Thread::isAlive).count(); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java b/java/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java new file mode 100644 index 000000000..3dcbf7c9a --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ToolInvocationTest.java @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.sdk.json.ToolInvocation; + +/** + * Unit tests for {@link ToolInvocation}. + *

+ * Tests getter methods, type-safe deserialization, and null handling to improve + * coverage beyond what E2E tests exercise. + */ +public class ToolInvocationTest { + + /** + * Test all basic getters return values set via setters. + */ + @Test + void testGettersReturnSetValues() { + ToolInvocation invocation = new ToolInvocation().setSessionId("test-session-123").setToolCallId("call_abc123") + .setToolName("test_tool"); + + assertEquals("test-session-123", invocation.getSessionId()); + assertEquals("call_abc123", invocation.getToolCallId()); + assertEquals("test_tool", invocation.getToolName()); + } + + /** + * Test getArguments returns null when no arguments are set. + */ + @Test + void testGetArgumentsWhenNull() { + ToolInvocation invocation = new ToolInvocation(); + assertNull(invocation.getArguments(), "getArguments should return null when argumentsNode is null"); + } + + /** + * Test getArguments returns a Map when arguments are set. + */ + @Test + void testGetArgumentsReturnsMap() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with some arguments + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("location", "San Francisco"); + argsNode.put("units", "celsius"); + + invocation.setArguments(argsNode); + + var args = invocation.getArguments(); + assertNotNull(args); + assertEquals("San Francisco", args.get("location")); + assertEquals("celsius", args.get("units")); + } + + /** + * Test getArgumentsAs deserializes to a record type. + */ + @Test + void testGetArgumentsAsWithRecord() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with weather arguments + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("city", "Paris"); + argsNode.put("units", "metric"); + + invocation.setArguments(argsNode); + + // Deserialize to record + WeatherArgs args = invocation.getArgumentsAs(WeatherArgs.class); + assertNotNull(args); + assertEquals("Paris", args.city()); + assertEquals("metric", args.units()); + } + + /** + * Test getArgumentsAs deserializes to a POJO. + */ + @Test + void testGetArgumentsAsWithPojo() { + ToolInvocation invocation = new ToolInvocation(); + + // Create a JsonNode with user data + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("username", "alice"); + argsNode.put("age", 30); + + invocation.setArguments(argsNode); + + // Deserialize to POJO + UserData userData = invocation.getArgumentsAs(UserData.class); + assertNotNull(userData); + assertEquals("alice", userData.getUsername()); + assertEquals(30, userData.getAge()); + } + + /** + * Test getArgumentsAs throws IllegalArgumentException on deserialization + * failure. + */ + @Test + void testGetArgumentsAsThrowsOnInvalidType() { + ToolInvocation invocation = new ToolInvocation(); + + // Create invalid JSON for the target type (missing required field) + ObjectNode argsNode = JsonNodeFactory.instance.objectNode(); + argsNode.put("invalid_field", "value"); + + invocation.setArguments(argsNode); + + // Try to deserialize to a type that doesn't match + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> invocation.getArgumentsAs(StrictType.class), + "Should throw IllegalArgumentException for invalid deserialization"); + + assertTrue(exception.getMessage().contains("Failed to deserialize arguments")); + assertTrue(exception.getMessage().contains("StrictType")); + } + + /** + * Record for testing type-safe argument deserialization. + */ + record WeatherArgs(String city, String units) { + } + + /** + * POJO for testing type-safe argument deserialization. + */ + public static class UserData { + private String username; + private int age; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } + + /** + * Strict type with constructor that throws, for testing error handling. + */ + public static class StrictType { + private final String requiredField; + + public StrictType(String requiredField) { + if (requiredField == null) { + throw new IllegalArgumentException("requiredField cannot be null"); + } + this.requiredField = requiredField; + } + + public String getRequiredField() { + return requiredField; + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ToolResultsTest.java b/java/src/test/java/com/github/copilot/sdk/ToolResultsTest.java new file mode 100644 index 000000000..31f9d1b07 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ToolResultsTest.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; +import com.github.copilot.sdk.json.ToolResultObject; + +/** + * E2E tests for tool result types β€” verifying that rejected and denied result + * types are handled correctly by the runtime. + * + *

+ * Snapshots are stored in {@code test/snapshots/tool_results/}. + *

+ */ +public class ToolResultsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that a tool returning a "rejected" resultType is reported as a + * failed tool execution with the correct error code. + * + * @see Snapshot: + * tool_results/should_handle_tool_result_with_rejected_resulttype + */ + @Test + void testShouldHandleToolResultWithRejectedResultType() throws Exception { + ctx.configureForTest("tool_results", "should_handle_tool_result_with_rejected_resulttype"); + + var toolHandlerCalled = new boolean[]{false}; + + Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of()); + + ToolDefinition deployTool = ToolDefinition.create("deploy_service", "Deploys a service", params, + (invocation) -> { + toolHandlerCalled[0] = true; + return CompletableFuture.completedFuture(new ToolResultObject("rejected", + "Deployment rejected: policy violation - production deployments require approval", null, + null, null, null)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(deployTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt( + "Deploy the service using deploy_service. If it's rejected, tell me it was 'rejected by policy'.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(toolHandlerCalled[0], "Tool handler should have been called"); + + List toolEvents = events.stream() + .filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e) + .toList(); + assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event"); + + ToolExecutionCompleteEvent toolEvt = toolEvents.get(0); + assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful"); + assertNotNull(toolEvt.getData().error(), "Should have error details"); + assertEquals("rejected", toolEvt.getData().error().code(), "Error code should be 'rejected'"); + + session.close(); + } + } + + /** + * Verifies that a tool returning a "denied" resultType is reported as a failed + * tool execution with the correct error code. + * + * @see Snapshot: tool_results/should_handle_tool_result_with_denied_resulttype + */ + @Test + void testShouldHandleToolResultWithDeniedResultType() throws Exception { + ctx.configureForTest("tool_results", "should_handle_tool_result_with_denied_resulttype"); + + var toolHandlerCalled = new boolean[]{false}; + + Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of()); + + ToolDefinition accessTool = ToolDefinition.create("access_secret", "Accesses a secret", params, + (invocation) -> { + toolHandlerCalled[0] = true; + return CompletableFuture.completedFuture(new ToolResultObject("denied", + "Access denied: insufficient permissions to read secrets", null, null, null, null)); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(accessTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + List events = new ArrayList<>(); + session.on(events::add); + + session.sendAndWait(new MessageOptions().setPrompt( + "Use access_secret to get the API key. If access is denied, tell me it was 'access denied'.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(toolHandlerCalled[0], "Tool handler should have been called"); + + List toolEvents = events.stream() + .filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e) + .toList(); + assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event"); + + ToolExecutionCompleteEvent toolEvt = toolEvents.get(0); + assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful"); + assertNotNull(toolEvt.getData().error(), "Should have error details"); + assertEquals("denied", toolEvt.getData().error().code(), "Error code should be 'denied'"); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ToolsTest.java b/java/src/test/java/com/github/copilot/sdk/ToolsTest.java new file mode 100644 index 000000000..6cd0c99bd --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ToolsTest.java @@ -0,0 +1,468 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.PermissionRequest; +import com.github.copilot.sdk.json.PermissionRequestResult; +import com.github.copilot.sdk.json.PermissionRequestResultKind; +import com.github.copilot.sdk.json.SessionConfig; +import com.github.copilot.sdk.json.ToolDefinition; + +/** + * Tests for custom tools functionality. + * + *

+ * These tests use the shared CapiProxy infrastructure for deterministic API + * response replay. Snapshots are stored in test/snapshots/tools/. + *

+ */ +public class ToolsTest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + /** + * Verifies that built-in tools are invoked correctly. + * + * @see Snapshot: tools/invokes_built_in_tools + */ + @Test + void testInvokesBuiltInTools(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_built_in_tools"); + + // Write a test file + Path readmeFile = ctx.getWorkDir().resolve("README.md"); + Files.writeString(readmeFile, "# ELIZA, the only chatbot you'll ever need"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait( + new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("ELIZA"), + "Response should contain ELIZA: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that custom tools are invoked correctly. + * + * @see Snapshot: tools/invokes_custom_tool + */ + @Test + void testInvokesCustomTool(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool"); + + // Define a simple encrypt_string tool + var parameters = new HashMap(); + var properties = new HashMap(); + var inputProp = new HashMap(); + inputProp.put("type", "string"); + inputProp.put("description", "String to encrypt"); + properties.put("input", inputProp); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("HELLO"), + "Response should contain HELLO: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that tool calling errors are handled gracefully. + * + * @see Snapshot: tools/handles_tool_calling_errors + */ + @Test + void testHandlesToolCallingErrors(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "handles_tool_calling_errors"); + + // Define a tool that throws an error + var parameters = new HashMap(); + parameters.put("type", "object"); + parameters.put("properties", new HashMap<>()); + + ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", parameters, + (invocation) -> { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new RuntimeException("Melbourne")); + return future; + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + // The error message should NOT be exposed to the assistant + String content = response.getData().content().toLowerCase(); + assertFalse(content.contains("melbourne"), "Error details should not be exposed in response: " + content); + assertTrue(content.contains("unknown") || content.contains("unable") || content.contains("cannot"), + "Response should indicate inability to get location: " + content); + + session.close(); + } + } + + /** + * Verifies that tools can receive and return complex types. + * + * @see Snapshot: tools/can_receive_and_return_complex_types + */ + @Test + void testCanReceiveAndReturnComplexTypes(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "can_receive_and_return_complex_types"); + + // Define a db_query tool with complex parameter and return types + var querySchema = new HashMap(); + var queryProps = new HashMap(); + queryProps.put("table", Map.of("type", "string")); + queryProps.put("ids", Map.of("type", "array", "items", Map.of("type", "integer"))); + queryProps.put("sortAscending", Map.of("type", "boolean")); + querySchema.put("type", "object"); + querySchema.put("properties", queryProps); + querySchema.put("required", List.of("table", "ids", "sortAscending")); + + var parameters = new HashMap(); + var properties = new HashMap(); + properties.put("query", querySchema); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("query")); + + ToolDefinition dbQueryTool = ToolDefinition.create("db_query", "Performs a database query", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + @SuppressWarnings("unchecked") + Map query = (Map) args.get("query"); + + assertEquals("cities", query.get("table")); + + // Return complex data structure + List> results = List.of( + Map.of("countryId", 19, "cityName", "Passos", "population", 135460), + Map.of("countryId", 12, "cityName", "San Lorenzo", "population", 204356)); + + return CompletableFuture.completedFuture(results); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(dbQueryTool)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt( + "Perform a DB query for the 'cities' table using IDs 12 and 19, sorting ascending. " + + "Reply only with lines of the form: [cityname] [population]")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + String content = response.getData().content(); + assertTrue(content.contains("Passos"), "Response should contain Passos: " + content); + assertTrue(content.contains("San Lorenzo"), "Response should contain San Lorenzo: " + content); + + session.close(); + } + } + + /** + * Verifies that a custom tool is invoked with the permission handler being + * called and can inspect the permission request kind. + * + * @see Snapshot: tools/invokes_custom_tool_with_permission_handler + */ + @Test + void testInvokesCustomToolWithPermissionHandler(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "invokes_custom_tool_with_permission_handler"); + + var permissionRequests = new ArrayList(); + + var parameters = new HashMap(); + parameters.put("type", "object"); + var props = new HashMap(); + props.put("input", Map.of("type", "string")); + parameters.put("properties", props); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setTools(List.of(encryptTool)).setOnPermissionRequest((request, invocation) -> { + permissionRequests.add(request); + return PermissionHandler.APPROVE_ALL.handle(request, invocation); + })).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("HELLO"), + "Response should contain HELLO: " + response.getData().content()); + + // Should have received a custom-tool permission request + boolean hasCustomToolRequest = permissionRequests.stream() + .anyMatch(req -> "custom-tool".equals(req.getKind())); + assertTrue(hasCustomToolRequest, "Should have received a custom-tool permission request"); + + session.close(); + } + } + + /** + * Verifies that a custom tool is denied when the permission handler denies it. + * + * @see Snapshot: tools/denies_custom_tool_when_permission_denied + */ + @Test + void testDeniesCustomToolWhenPermissionDenied(TestInfo testInfo) throws Exception { + ctx.configureForTest("tools", "denies_custom_tool_when_permission_denied"); + + final boolean[] toolHandlerCalled = {false}; + + var parameters = new HashMap(); + parameters.put("type", "object"); + var props = new HashMap(); + props.put("input", Map.of("type", "string")); + parameters.put("properties", props); + parameters.put("required", List.of("input")); + + ToolDefinition encryptTool = ToolDefinition.create("encrypt_string", "Encrypts a string", parameters, + (invocation) -> { + toolHandlerCalled[0] = true; + Map args = invocation.getArguments(); + String input = (String) args.get("input"); + return CompletableFuture.completedFuture(input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setTools(List.of(encryptTool)) + .setOnPermissionRequest((request, invocation) -> CompletableFuture.completedFuture( + new PermissionRequestResult().setKind(PermissionRequestResultKind.REJECTED)))) + .get(); + + session.sendAndWait(new MessageOptions().setPrompt("Use encrypt_string to encrypt this string: Hello")) + .get(60, TimeUnit.SECONDS); + + // The tool handler should NOT have been called since permission was denied + assertFalse(toolHandlerCalled[0], "Tool handler should not be called when permission is denied"); + + session.close(); + } + } + + /** + * Verifies that a custom tool can override a built-in CLI tool with the same + * name when {@code overridesBuiltInTool} is set to {@code true}. + * + * @see Snapshot: tools/overrides_built_in_tool_with_custom_tool + */ + @Test + void testOverridesBuiltInToolWithCustomTool() throws Exception { + ctx.configureForTest("tools", "overrides_built_in_tool_with_custom_tool"); + + var parameters = new HashMap(); + var properties = new HashMap(); + properties.put("query", Map.of("type", "string", "description", "Search query")); + parameters.put("type", "object"); + parameters.put("properties", properties); + parameters.put("required", List.of("query")); + + ToolDefinition customGrep = ToolDefinition.createOverride("grep", "A custom grep implementation", parameters, + (invocation) -> { + Map args = invocation.getArguments(); + String query = (String) args.get("query"); + return CompletableFuture.completedFuture("CUSTOM_GREP_RESULT: " + query); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(customGrep)) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions().setPrompt("Use grep to search for the word 'hello'")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("CUSTOM_GREP_RESULT"), + "Response should contain CUSTOM_GREP_RESULT: " + response.getData().content()); + + session.close(); + } + } + + /** + * Verifies that the model can call multiple custom tools in parallel within a + * single turn. + * + * @see Snapshot: + * tools/should_execute_multiple_custom_tools_in_parallel_single_turn + */ + @Test + void testShouldExecuteMultipleCustomToolsInParallelSingleTurn() throws Exception { + ctx.configureForTest("tools", "should_execute_multiple_custom_tools_in_parallel_single_turn"); + + var toolACalled = new CompletableFuture(); + var toolBCalled = new CompletableFuture(); + + Map cityParams = Map.of("type", "object", "properties", + Map.of("city", Map.of("type", "string", "description", "City name")), "required", List.of("city")); + Map countryParams = Map.of("type", "object", "properties", + Map.of("country", Map.of("type", "string", "description", "Country name")), "required", + List.of("country")); + + ToolDefinition lookupCity = ToolDefinition.create("lookup_city", "Looks up city information", cityParams, + (invocation) -> { + String city = (String) invocation.getArguments().get("city"); + toolACalled.complete(city); + return CompletableFuture.completedFuture("CITY_" + city.toUpperCase()); + }); + + ToolDefinition lookupCountry = ToolDefinition.create("lookup_country", "Looks up country information", + countryParams, (invocation) -> { + String country = (String) invocation.getArguments().get("country"); + toolBCalled.complete(country); + return CompletableFuture.completedFuture("COUNTRY_" + country.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setTools(List.of(lookupCity, lookupCountry)).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt( + "Use lookup_city with 'Paris' and lookup_country with 'France' at the same time, then combine both results in your reply.")) + .get(60, TimeUnit.SECONDS); + + // Both tools should have been called + assertEquals("Paris", toolACalled.get(10, TimeUnit.SECONDS)); + assertEquals("France", toolBCalled.get(10, TimeUnit.SECONDS)); + + assertNotNull(response); + String content = response.getData().content(); + assertTrue(content.contains("CITY_PARIS"), "Response should contain CITY_PARIS: " + content); + assertTrue(content.contains("COUNTRY_FRANCE"), "Response should contain COUNTRY_FRANCE: " + content); + + session.close(); + } + } + + /** + * Verifies that excludedTools are respected even when also listed in + * availableTools. + * + * @see Snapshot: tools/should_respect_availabletools_and_excludedtools_combined + */ + @Test + void testShouldRespectAvailableToolsAndExcludedToolsCombined() throws Exception { + ctx.configureForTest("tools", "should_respect_availabletools_and_excludedtools_combined"); + + var excludedToolCalled = new boolean[]{false}; + + Map inputParams = Map.of("type", "object", "properties", + Map.of("input", Map.of("type", "string", "description", "Input value")), "required", List.of("input")); + + ToolDefinition allowedTool = ToolDefinition.create("allowed_tool", "An allowed tool", inputParams, + (invocation) -> { + String input = (String) invocation.getArguments().get("input"); + return CompletableFuture.completedFuture("ALLOWED_" + input.toUpperCase()); + }); + + ToolDefinition excludedTool = ToolDefinition.create("excluded_tool", "A tool that should be excluded", + inputParams, (invocation) -> { + excludedToolCalled[0] = true; + String input = (String) invocation.getArguments().get("input"); + return CompletableFuture.completedFuture("EXCLUDED_" + input.toUpperCase()); + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setTools(List.of(allowedTool, excludedTool)) + .setAvailableTools(List.of("allowed_tool", "excluded_tool")) + .setExcludedTools(List.of("excluded_tool")).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(); + + AssistantMessageEvent response = session + .sendAndWait(new MessageOptions() + .setPrompt("Use the allowed_tool with input 'test'. Do NOT use excluded_tool.")) + .get(60, TimeUnit.SECONDS); + + assertNotNull(response); + assertTrue(response.getData().content().contains("ALLOWED_TEST"), + "Response should contain ALLOWED_TEST: " + response.getData().content()); + assertFalse(excludedToolCalled[0], "Excluded tool should not have been called"); + + session.close(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/ZeroTimeoutContractTest.java b/java/src/test/java/com/github/copilot/sdk/ZeroTimeoutContractTest.java new file mode 100644 index 000000000..524026e69 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/ZeroTimeoutContractTest.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.generated.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; + +/** + * Verifies the documented contract that {@code timeoutMs <= 0} means "no + * timeout" in {@link CopilotSession#sendAndWait(MessageOptions, long)}. + */ +public class ZeroTimeoutContractTest { + + @SuppressWarnings("unchecked") + @Test + void sendAndWaitWithZeroTimeoutShouldNotTimeOut() throws Exception { + // Build a session via reflection (package-private constructor) + var ctor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); + ctor.setAccessible(true); + + var mockRpc = mock(JsonRpcClient.class); + when(mockRpc.invoke(any(), any(), any())).thenAnswer(invocation -> { + Object method = invocation.getArgument(0); + if ("session.destroy".equals(method)) { + // Make session.close() non-blocking by completing destroy immediately + return CompletableFuture.completedFuture(null); + } + // For other calls (e.g., message send), return an incomplete future so the + // sendAndWait result does not complete due to a mock response. + return new CompletableFuture<>(); + }); + + try (var session = ctor.newInstance("zero-timeout-test", mockRpc, null)) { + + // Per the Javadoc: timeoutMs of 0 means "no timeout". + // The future should NOT complete with TimeoutException. + CompletableFuture result = session + .sendAndWait(new MessageOptions().setPrompt("test"), 0); + + // Give the scheduler a chance to fire if it was (incorrectly) scheduled + Thread.sleep(200); + + // The future should still be pending β€” not timed out + assertFalse(result.isDone(), "Future should not be done; timeoutMs=0 means no timeout per Javadoc"); + } + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java new file mode 100644 index 000000000..6cb608e19 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/generated/GeneratedEventTypesCoverageTest.java @@ -0,0 +1,704 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.generated; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + * Deserialization tests for generated session event types that are not covered + * in {@link com.github.copilot.sdk.SessionEventDeserializationTest}. Verifies + * that each event deserializes correctly from JSON and that the {@code type} + * discriminator and {@code data} fields are accessible. + */ +public class GeneratedEventTypesCoverageTest { + + private static final ObjectMapper MAPPER = createMapper(); + + private static ObjectMapper createMapper() { + var mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + return mapper; + } + + private static SessionEvent parse(String json) throws Exception { + return MAPPER.readValue(json, SessionEvent.class); + } + + // ── AssistantStreamingDeltaEvent ─────────────────────────────────────── + + @Test + void testParseAssistantStreamingDeltaEvent() throws Exception { + var event = parse(""" + {"type":"assistant.streaming_delta","data":{"totalResponseSizeBytes":1024.0}} + """); + assertInstanceOf(AssistantStreamingDeltaEvent.class, event); + assertEquals("assistant.streaming_delta", event.getType()); + var typed = (AssistantStreamingDeltaEvent) event; + assertEquals(1024.0, typed.getData().totalResponseSizeBytes()); + } + + // ── CapabilitiesChangedEvent ─────────────────────────────────────────── + + @Test + void testParseCapabilitiesChangedEvent() throws Exception { + var event = parse(""" + {"type":"capabilities.changed","data":{"ui":{"elicitation":true}}} + """); + assertInstanceOf(CapabilitiesChangedEvent.class, event); + assertEquals("capabilities.changed", event.getType()); + var typed = (CapabilitiesChangedEvent) event; + assertNotNull(typed.getData()); + assertTrue(typed.getData().ui().elicitation()); + } + + @Test + void testParseCapabilitiesChangedEventNoData() throws Exception { + var event = parse(""" + {"type":"capabilities.changed"} + """); + assertInstanceOf(CapabilitiesChangedEvent.class, event); + } + + // ── CommandQueuedEvent ───────────────────────────────────────────────── + + @Test + void testParseCommandQueuedEvent() throws Exception { + var event = parse(""" + {"type":"command.queued","data":{"requestId":"req-001","command":"/deploy"}} + """); + assertInstanceOf(CommandQueuedEvent.class, event); + assertEquals("command.queued", event.getType()); + var typed = (CommandQueuedEvent) event; + assertEquals("req-001", typed.getData().requestId()); + assertEquals("/deploy", typed.getData().command()); + } + + // ── CommandExecuteEvent ──────────────────────────────────────────────── + + @Test + void testParseCommandExecuteEvent() throws Exception { + var event = parse( + """ + {"type":"command.execute","data":{"requestId":"req-002","command":"/help me","commandName":"help","args":"me"}} + """); + assertInstanceOf(CommandExecuteEvent.class, event); + assertEquals("command.execute", event.getType()); + var typed = (CommandExecuteEvent) event; + assertEquals("req-002", typed.getData().requestId()); + assertEquals("help", typed.getData().commandName()); + assertEquals("me", typed.getData().args()); + } + + // ── CommandCompletedEvent ────────────────────────────────────────────── + + @Test + void testParseCommandCompletedEvent() throws Exception { + var event = parse(""" + {"type":"command.completed","data":{"requestId":"req-003"}} + """); + assertInstanceOf(CommandCompletedEvent.class, event); + assertEquals("command.completed", event.getType()); + var typed = (CommandCompletedEvent) event; + assertEquals("req-003", typed.getData().requestId()); + } + + // ── CommandsChangedEvent ─────────────────────────────────────────────── + + @Test + void testParseCommandsChangedEvent() throws Exception { + var event = parse(""" + {"type":"commands.changed","data":{"commands":[{"name":"deploy","description":"Deploy to prod"}]}} + """); + assertInstanceOf(CommandsChangedEvent.class, event); + assertEquals("commands.changed", event.getType()); + var typed = (CommandsChangedEvent) event; + assertNotNull(typed.getData().commands()); + assertEquals(1, typed.getData().commands().size()); + assertEquals("deploy", typed.getData().commands().get(0).name()); + assertEquals("Deploy to prod", typed.getData().commands().get(0).description()); + } + + @Test + void testParseCommandsChangedEventEmpty() throws Exception { + var event = parse(""" + {"type":"commands.changed","data":{"commands":[]}} + """); + assertInstanceOf(CommandsChangedEvent.class, event); + var typed = (CommandsChangedEvent) event; + assertNotNull(typed.getData().commands()); + assertEquals(0, typed.getData().commands().size()); + } + + // ── ElicitationRequestedEvent ────────────────────────────────────────── + + @Test + void testParseElicitationRequestedEvent() throws Exception { + var event = parse( + """ + {"type":"elicitation.requested","data":{"requestId":"elicit-1","message":"Please enter your name","mode":"form"}} + """); + assertInstanceOf(ElicitationRequestedEvent.class, event); + assertEquals("elicitation.requested", event.getType()); + var typed = (ElicitationRequestedEvent) event; + assertEquals("elicit-1", typed.getData().requestId()); + assertEquals("Please enter your name", typed.getData().message()); + assertEquals(ElicitationRequestedMode.FORM, typed.getData().mode()); + } + + @Test + void testParseElicitationRequestedEventUrlMode() throws Exception { + var event = parse( + """ + {"type":"elicitation.requested","data":{"requestId":"elicit-2","message":"Open browser","mode":"url","url":"https://example.com"}} + """); + assertInstanceOf(ElicitationRequestedEvent.class, event); + var typed = (ElicitationRequestedEvent) event; + assertEquals(ElicitationRequestedMode.URL, typed.getData().mode()); + assertEquals("https://example.com", typed.getData().url()); + } + + // ── ElicitationCompletedEvent ────────────────────────────────────────── + + @Test + void testParseElicitationCompletedEvent() throws Exception { + var event = parse( + """ + {"type":"elicitation.completed","data":{"requestId":"elicit-1","action":"accept","content":{"name":"Alice"}}} + """); + assertInstanceOf(ElicitationCompletedEvent.class, event); + assertEquals("elicitation.completed", event.getType()); + var typed = (ElicitationCompletedEvent) event; + assertEquals("elicit-1", typed.getData().requestId()); + assertEquals(ElicitationCompletedAction.ACCEPT, typed.getData().action()); + assertEquals("Alice", typed.getData().content().get("name")); + } + + @Test + void testParseElicitationCompletedEventDecline() throws Exception { + var event = parse(""" + {"type":"elicitation.completed","data":{"requestId":"elicit-2","action":"decline"}} + """); + assertInstanceOf(ElicitationCompletedEvent.class, event); + var typed = (ElicitationCompletedEvent) event; + assertEquals(ElicitationCompletedAction.DECLINE, typed.getData().action()); + } + + @Test + void testParseElicitationCompletedEventCancel() throws Exception { + var event = parse(""" + {"type":"elicitation.completed","data":{"requestId":"elicit-3","action":"cancel"}} + """); + assertInstanceOf(ElicitationCompletedEvent.class, event); + var typed = (ElicitationCompletedEvent) event; + assertEquals(ElicitationCompletedAction.CANCEL, typed.getData().action()); + } + + // ── ExitPlanModeRequestedEvent ───────────────────────────────────────── + + @Test + void testParseExitPlanModeRequestedEvent() throws Exception { + var event = parse( + """ + {"type":"exit_plan_mode.requested","data":{"requestId":"epm-1","summary":"Implement login","planContent":"# Plan\\n1. Create login","actions":["exit_only","interactive","autopilot"],"recommendedAction":"interactive"}} + """); + assertInstanceOf(ExitPlanModeRequestedEvent.class, event); + assertEquals("exit_plan_mode.requested", event.getType()); + var typed = (ExitPlanModeRequestedEvent) event; + assertEquals("epm-1", typed.getData().requestId()); + assertEquals("Implement login", typed.getData().summary()); + assertEquals(ExitPlanModeAction.INTERACTIVE, typed.getData().recommendedAction()); + assertEquals(3, typed.getData().actions().size()); + } + + // ── ExitPlanModeCompletedEvent ───────────────────────────────────────── + + @Test + void testParseExitPlanModeCompletedEvent() throws Exception { + var event = parse(""" + {"type":"exit_plan_mode.completed","data":{"requestId":"epm-1","action":"approve"}} + """); + assertInstanceOf(ExitPlanModeCompletedEvent.class, event); + assertEquals("exit_plan_mode.completed", event.getType()); + var typed = (ExitPlanModeCompletedEvent) event; + assertNotNull(typed.getData()); + } + + // ── ExternalToolRequestedEvent ───────────────────────────────────────── + + @Test + void testParseExternalToolRequestedEvent() throws Exception { + var event = parse( + """ + {"type":"external_tool.requested","data":{"requestId":"ext-1","sessionId":"sess-abc","toolCallId":"tc-1","toolName":"myTool","arguments":{"key":"value"}}} + """); + assertInstanceOf(ExternalToolRequestedEvent.class, event); + assertEquals("external_tool.requested", event.getType()); + var typed = (ExternalToolRequestedEvent) event; + assertEquals("ext-1", typed.getData().requestId()); + assertEquals("sess-abc", typed.getData().sessionId()); + assertEquals("myTool", typed.getData().toolName()); + } + + // ── ExternalToolCompletedEvent ───────────────────────────────────────── + + @Test + void testParseExternalToolCompletedEvent() throws Exception { + var event = parse(""" + {"type":"external_tool.completed","data":{"requestId":"ext-1"}} + """); + assertInstanceOf(ExternalToolCompletedEvent.class, event); + assertEquals("external_tool.completed", event.getType()); + var typed = (ExternalToolCompletedEvent) event; + assertEquals("ext-1", typed.getData().requestId()); + } + + // ── McpOauthRequiredEvent ────────────────────────────────────────────── + + @Test + void testParseMcpOauthRequiredEvent() throws Exception { + var event = parse( + """ + {"type":"mcp.oauth_required","data":{"requestId":"mcp-oauth-1","serverName":"my-mcp","serverUrl":"https://mcp.example.com"}} + """); + assertInstanceOf(McpOauthRequiredEvent.class, event); + assertEquals("mcp.oauth_required", event.getType()); + var typed = (McpOauthRequiredEvent) event; + assertEquals("mcp-oauth-1", typed.getData().requestId()); + assertEquals("my-mcp", typed.getData().serverName()); + assertEquals("https://mcp.example.com", typed.getData().serverUrl()); + } + + @Test + void testParseMcpOauthRequiredEventWithStaticConfig() throws Exception { + var event = parse( + """ + {"type":"mcp.oauth_required","data":{"requestId":"mcp-oauth-2","serverName":"s","serverUrl":"https://s.com","staticClientConfig":{"clientId":"cid-123","publicClient":true}}} + """); + assertInstanceOf(McpOauthRequiredEvent.class, event); + var typed = (McpOauthRequiredEvent) event; + assertEquals("cid-123", typed.getData().staticClientConfig().clientId()); + assertTrue(typed.getData().staticClientConfig().publicClient()); + } + + // ── McpOauthCompletedEvent ───────────────────────────────────────────── + + @Test + void testParseMcpOauthCompletedEvent() throws Exception { + var event = parse(""" + {"type":"mcp.oauth_completed","data":{"requestId":"mcp-oauth-1"}} + """); + assertInstanceOf(McpOauthCompletedEvent.class, event); + assertEquals("mcp.oauth_completed", event.getType()); + var typed = (McpOauthCompletedEvent) event; + assertEquals("mcp-oauth-1", typed.getData().requestId()); + } + + // ── PermissionRequestedEvent ─────────────────────────────────────────── + + @Test + void testParsePermissionRequestedEvent() throws Exception { + var event = parse( + """ + {"type":"permission.requested","data":{"requestId":"perm-1","permissionRequest":{"tool":"bash"},"resolvedByHook":false}} + """); + assertInstanceOf(PermissionRequestedEvent.class, event); + assertEquals("permission.requested", event.getType()); + var typed = (PermissionRequestedEvent) event; + assertEquals("perm-1", typed.getData().requestId()); + assertNotNull(typed.getData().permissionRequest()); + assertFalse(typed.getData().resolvedByHook()); + } + + @Test + void testParsePermissionRequestedEventResolvedByHook() throws Exception { + var event = parse(""" + {"type":"permission.requested","data":{"requestId":"perm-2","resolvedByHook":true}} + """); + assertInstanceOf(PermissionRequestedEvent.class, event); + var typed = (PermissionRequestedEvent) event; + assertTrue(typed.getData().resolvedByHook()); + } + + // ── PermissionCompletedEvent ─────────────────────────────────────────── + + @Test + void testParsePermissionCompletedEvent() throws Exception { + var event = parse(""" + {"type":"permission.completed","data":{"requestId":"perm-1","decision":"allow"}} + """); + assertInstanceOf(PermissionCompletedEvent.class, event); + assertEquals("permission.completed", event.getType()); + var typed = (PermissionCompletedEvent) event; + assertNotNull(typed.getData()); + } + + // ── SamplingRequestedEvent ───────────────────────────────────────────── + + @Test + void testParseSamplingRequestedEvent() throws Exception { + var event = parse(""" + {"type":"sampling.requested","data":{"requestId":"samp-1","serverName":"my-mcp","mcpRequestId":42}} + """); + assertInstanceOf(SamplingRequestedEvent.class, event); + assertEquals("sampling.requested", event.getType()); + var typed = (SamplingRequestedEvent) event; + assertEquals("samp-1", typed.getData().requestId()); + assertEquals("my-mcp", typed.getData().serverName()); + assertNotNull(typed.getData().mcpRequestId()); + } + + // ── SamplingCompletedEvent ───────────────────────────────────────────── + + @Test + void testParseSamplingCompletedEvent() throws Exception { + var event = parse(""" + {"type":"sampling.completed","data":{"requestId":"samp-1"}} + """); + assertInstanceOf(SamplingCompletedEvent.class, event); + assertEquals("sampling.completed", event.getType()); + var typed = (SamplingCompletedEvent) event; + assertNotNull(typed.getData()); + } + + // ── SessionBackgroundTasksChangedEvent ───────────────────────────────── + + @Test + void testParseSessionBackgroundTasksChangedEvent() throws Exception { + var event = parse(""" + {"type":"session.background_tasks_changed","data":{}} + """); + assertInstanceOf(SessionBackgroundTasksChangedEvent.class, event); + assertEquals("session.background_tasks_changed", event.getType()); + assertNotNull(((SessionBackgroundTasksChangedEvent) event).getData()); + } + + // ── SessionContextChangedEvent ───────────────────────────────────────── + + @Test + void testParseSessionContextChangedEvent() throws Exception { + var event = parse( + """ + {"type":"session.context_changed","data":{"cwd":"/workspace","gitRoot":"/workspace","repository":"myorg/myrepo","hostType":"github","branch":"main","headCommit":"abc123","baseCommit":"def456"}} + """); + assertInstanceOf(SessionContextChangedEvent.class, event); + assertEquals("session.context_changed", event.getType()); + var typed = (SessionContextChangedEvent) event; + assertEquals("/workspace", typed.getData().cwd()); + assertEquals("myorg/myrepo", typed.getData().repository()); + assertEquals(WorkingDirectoryContextHostType.GITHUB, typed.getData().hostType()); + assertEquals("main", typed.getData().branch()); + } + + @Test + void testParseSessionContextChangedEventAdoHostType() throws Exception { + var event = parse(""" + {"type":"session.context_changed","data":{"hostType":"ado"}} + """); + assertInstanceOf(SessionContextChangedEvent.class, event); + var typed = (SessionContextChangedEvent) event; + assertEquals(WorkingDirectoryContextHostType.ADO, typed.getData().hostType()); + } + + // ── SessionCustomAgentsUpdatedEvent ──────────────────────────────────── + + @Test + void testParseSessionCustomAgentsUpdatedEvent() throws Exception { + var event = parse( + """ + {"type":"session.custom_agents_updated","data":{"agents":[{"name":"my-agent","displayName":"My Agent","description":"Does stuff"}]}} + """); + assertInstanceOf(SessionCustomAgentsUpdatedEvent.class, event); + assertEquals("session.custom_agents_updated", event.getType()); + var typed = (SessionCustomAgentsUpdatedEvent) event; + assertNotNull(typed.getData().agents()); + assertEquals(1, typed.getData().agents().size()); + assertEquals("my-agent", typed.getData().agents().get(0).name()); + } + + // ── SessionExtensionsLoadedEvent ─────────────────────────────────────── + + @Test + void testParseSessionExtensionsLoadedEvent() throws Exception { + var event = parse( + """ + {"type":"session.extensions_loaded","data":{"extensions":[{"id":"ext-1","name":"My Extension","enabled":true}]}} + """); + assertInstanceOf(SessionExtensionsLoadedEvent.class, event); + assertEquals("session.extensions_loaded", event.getType()); + var typed = (SessionExtensionsLoadedEvent) event; + assertNotNull(typed.getData().extensions()); + assertEquals(1, typed.getData().extensions().size()); + assertEquals("ext-1", typed.getData().extensions().get(0).id()); + } + + @Test + void testParseSessionExtensionsLoadedEventEmpty() throws Exception { + var event = parse(""" + {"type":"session.extensions_loaded","data":{"extensions":[]}} + """); + assertInstanceOf(SessionExtensionsLoadedEvent.class, event); + } + + // ── SessionMcpServersLoadedEvent ─────────────────────────────────────── + + @Test + void testParseSessionMcpServersLoadedEvent() throws Exception { + var event = parse( + """ + {"type":"session.mcp_servers_loaded","data":{"servers":[{"name":"mcp1","status":"connected","source":"user"}]}} + """); + assertInstanceOf(SessionMcpServersLoadedEvent.class, event); + assertEquals("session.mcp_servers_loaded", event.getType()); + var typed = (SessionMcpServersLoadedEvent) event; + assertNotNull(typed.getData().servers()); + assertEquals(1, typed.getData().servers().size()); + assertEquals("mcp1", typed.getData().servers().get(0).name()); + assertEquals(McpServerStatus.CONNECTED, typed.getData().servers().get(0).status()); + } + + @Test + void testParseSessionMcpServersLoadedEventAllStatuses() throws Exception { + // Verify all enum variants are parseable + for (var status : new String[]{"connected", "failed", "needs-auth", "pending", "disabled", "not_configured"}) { + var event = parse( + "{\"type\":\"session.mcp_servers_loaded\",\"data\":{\"servers\":[{\"name\":\"s\",\"status\":\"" + + status + "\"}]}}"); + assertInstanceOf(SessionMcpServersLoadedEvent.class, event); + } + } + + // ── SessionMcpServerStatusChangedEvent ───────────────────────────────── + + @Test + void testParseSessionMcpServerStatusChangedEvent() throws Exception { + var event = parse(""" + {"type":"session.mcp_server_status_changed","data":{"name":"mcp1","status":"connected"}} + """); + assertInstanceOf(SessionMcpServerStatusChangedEvent.class, event); + assertEquals("session.mcp_server_status_changed", event.getType()); + var typed = (SessionMcpServerStatusChangedEvent) event; + assertNotNull(typed.getData()); + } + + // ── SessionRemoteSteerableChangedEvent ───────────────────────────────── + + @Test + void testParseSessionRemoteSteerableChangedEvent() throws Exception { + var event = parse(""" + {"type":"session.remote_steerable_changed","data":{"remoteSteerable":true}} + """); + assertInstanceOf(SessionRemoteSteerableChangedEvent.class, event); + assertEquals("session.remote_steerable_changed", event.getType()); + var typed = (SessionRemoteSteerableChangedEvent) event; + assertTrue(typed.getData().remoteSteerable()); + } + + @Test + void testParseSessionRemoteSteerableChangedEventFalse() throws Exception { + var event = parse(""" + {"type":"session.remote_steerable_changed","data":{"remoteSteerable":false}} + """); + assertInstanceOf(SessionRemoteSteerableChangedEvent.class, event); + var typed = (SessionRemoteSteerableChangedEvent) event; + assertFalse(typed.getData().remoteSteerable()); + } + + // ── SessionSkillsLoadedEvent ─────────────────────────────────────────── + + @Test + void testParseSessionSkillsLoadedEvent() throws Exception { + var event = parse( + """ + {"type":"session.skills_loaded","data":{"skills":[{"name":"deploy","description":"Deploy app","source":"project","userInvocable":true,"enabled":true,"path":"/skills/deploy.md"}]}} + """); + assertInstanceOf(SessionSkillsLoadedEvent.class, event); + assertEquals("session.skills_loaded", event.getType()); + var typed = (SessionSkillsLoadedEvent) event; + assertNotNull(typed.getData().skills()); + assertEquals(1, typed.getData().skills().size()); + var skill = typed.getData().skills().get(0); + assertEquals("deploy", skill.name()); + assertEquals(SkillSource.PROJECT, skill.source()); + assertTrue(skill.userInvocable()); + assertTrue(skill.enabled()); + } + + // ── SessionTaskCompleteEvent ─────────────────────────────────────────── + + @Test + void testParseSessionTaskCompleteEvent() throws Exception { + var event = parse(""" + {"type":"session.task_complete","data":{"summary":"All tests pass","success":true}} + """); + assertInstanceOf(SessionTaskCompleteEvent.class, event); + assertEquals("session.task_complete", event.getType()); + var typed = (SessionTaskCompleteEvent) event; + assertEquals("All tests pass", typed.getData().summary()); + assertTrue(typed.getData().success()); + } + + @Test + void testParseSessionTaskCompleteEventFailure() throws Exception { + var event = parse(""" + {"type":"session.task_complete","data":{"summary":"Build failed","success":false}} + """); + assertInstanceOf(SessionTaskCompleteEvent.class, event); + var typed = (SessionTaskCompleteEvent) event; + assertFalse(typed.getData().success()); + } + + // ── SessionTitleChangedEvent ─────────────────────────────────────────── + + @Test + void testParseSessionTitleChangedEvent() throws Exception { + var event = parse(""" + {"type":"session.title_changed","data":{"title":"My new session title"}} + """); + assertInstanceOf(SessionTitleChangedEvent.class, event); + assertEquals("session.title_changed", event.getType()); + var typed = (SessionTitleChangedEvent) event; + assertEquals("My new session title", typed.getData().title()); + } + + // ── SessionToolsUpdatedEvent ─────────────────────────────────────────── + + @Test + void testParseSessionToolsUpdatedEvent() throws Exception { + var event = parse(""" + {"type":"session.tools_updated","data":{"model":"gpt-5"}} + """); + assertInstanceOf(SessionToolsUpdatedEvent.class, event); + assertEquals("session.tools_updated", event.getType()); + var typed = (SessionToolsUpdatedEvent) event; + assertEquals("gpt-5", typed.getData().model()); + } + + // ── SessionWarningEvent ──────────────────────────────────────────────── + + @Test + void testParseSessionWarningEvent() throws Exception { + var event = parse( + """ + {"type":"session.warning","data":{"warningType":"subscription","message":"Quota at 90%","url":"https://github.com/billing"}} + """); + assertInstanceOf(SessionWarningEvent.class, event); + assertEquals("session.warning", event.getType()); + var typed = (SessionWarningEvent) event; + assertEquals("subscription", typed.getData().warningType()); + assertEquals("Quota at 90%", typed.getData().message()); + assertEquals("https://github.com/billing", typed.getData().url()); + } + + // ── SubagentDeselectedEvent ──────────────────────────────────────────── + + @Test + void testParseSubagentDeselectedEvent() throws Exception { + var event = parse(""" + {"type":"subagent.deselected","data":{}} + """); + assertInstanceOf(SubagentDeselectedEvent.class, event); + assertEquals("subagent.deselected", event.getType()); + assertNotNull(((SubagentDeselectedEvent) event).getData()); + } + + // ── SystemNotificationEvent ──────────────────────────────────────────── + + @Test + void testParseSystemNotificationEvent() throws Exception { + var event = parse(""" + {"type":"system.notification","data":{"message":"Update available","level":"info"}} + """); + assertInstanceOf(SystemNotificationEvent.class, event); + assertEquals("system.notification", event.getType()); + var typed = (SystemNotificationEvent) event; + assertNotNull(typed.getData()); + } + + // ── UserInputRequestedEvent ──────────────────────────────────────────── + + @Test + void testParseUserInputRequestedEvent() throws Exception { + var event = parse( + """ + {"type":"user_input.requested","data":{"requestId":"ui-1","question":"What is your name?","choices":["Alice","Bob"],"allowFreeform":true,"toolCallId":"tc-ui-1"}} + """); + assertInstanceOf(UserInputRequestedEvent.class, event); + assertEquals("user_input.requested", event.getType()); + var typed = (UserInputRequestedEvent) event; + assertEquals("ui-1", typed.getData().requestId()); + assertEquals("What is your name?", typed.getData().question()); + assertEquals(2, typed.getData().choices().size()); + assertTrue(typed.getData().allowFreeform()); + assertEquals("tc-ui-1", typed.getData().toolCallId()); + } + + // ── UserInputCompletedEvent ──────────────────────────────────────────── + + @Test + void testParseUserInputCompletedEvent() throws Exception { + var event = parse(""" + {"type":"user_input.completed","data":{"requestId":"ui-1","answer":"Alice","wasFreeform":false}} + """); + assertInstanceOf(UserInputCompletedEvent.class, event); + assertEquals("user_input.completed", event.getType()); + var typed = (UserInputCompletedEvent) event; + assertEquals("ui-1", typed.getData().requestId()); + assertEquals("Alice", typed.getData().answer()); + assertFalse(typed.getData().wasFreeform()); + } + + @Test + void testParseUserInputCompletedEventFreeform() throws Exception { + var event = parse( + """ + {"type":"user_input.completed","data":{"requestId":"ui-2","answer":"Custom response","wasFreeform":true}} + """); + assertInstanceOf(UserInputCompletedEvent.class, event); + var typed = (UserInputCompletedEvent) event; + assertTrue(typed.getData().wasFreeform()); + } + + // ── Enum round-trip tests ────────────────────────────────────────────── + + @Test + void testElicitationRequestedEventDataModeEnumValues() { + assertEquals("form", ElicitationRequestedMode.FORM.getValue()); + assertEquals("url", ElicitationRequestedMode.URL.getValue()); + } + + @Test + void testElicitationRequestedEventDataModeEnumFromValue() { + assertEquals(ElicitationRequestedMode.FORM, ElicitationRequestedMode.fromValue("form")); + assertThrows(IllegalArgumentException.class, () -> ElicitationRequestedMode.fromValue("unknown")); + } + + @Test + void testElicitationCompletedEventActionEnumValues() { + assertEquals("accept", ElicitationCompletedAction.ACCEPT.getValue()); + assertEquals("decline", ElicitationCompletedAction.DECLINE.getValue()); + assertEquals("cancel", ElicitationCompletedAction.CANCEL.getValue()); + } + + @Test + void testSessionContextChangedHostTypeEnumFromValue() { + assertEquals(WorkingDirectoryContextHostType.GITHUB, WorkingDirectoryContextHostType.fromValue("github")); + assertEquals(WorkingDirectoryContextHostType.ADO, WorkingDirectoryContextHostType.fromValue("ado")); + assertThrows(IllegalArgumentException.class, () -> WorkingDirectoryContextHostType.fromValue("unknown")); + } + + @Test + void testSessionMcpServersLoadedStatusEnumFromValue() { + assertThrows(IllegalArgumentException.class, () -> McpServerStatus.fromValue("unknown")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java new file mode 100644 index 000000000..f32c95c5c --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -0,0 +1,694 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +/** + * Coverage tests for generated RPC API classes that are not exercised in + * {@link RpcWrappersTest}. Uses the same {@link StubCaller} pattern to verify + * that each API method dispatches the correct RPC method name and passes + * parameters correctly. + */ +class GeneratedRpcApiCoverageTest { + + /** A simple stub {@link RpcCaller} that records every call made to it. */ + private static final class StubCaller implements RpcCaller { + + record Call(String method, Object params) { + } + + final List calls = new ArrayList<>(); + Object nextResult = null; + + @Override + @SuppressWarnings("unchecked") + public CompletableFuture invoke(String method, Object params, Class resultType) { + calls.add(new Call(method, params)); + return CompletableFuture.completedFuture((T) nextResult); + } + } + + // ── ServerRpc additional methods ─────────────────────────────────────── + + @Test + void serverRpc_tools_list_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new ToolsListParams("gpt-5"); + server.tools.list(params); + + assertEquals(1, stub.calls.size()); + assertEquals("tools.list", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_sessionFs_setProvider_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new SessionFsSetProviderParams("/workspace", "/state", null, null); + server.sessionFs.setProvider(params); + + assertEquals(1, stub.calls.size()); + assertEquals("sessionFs.setProvider", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_sessions_fork_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new SessionsForkParams("parent-session-id", null, null); + server.sessions.fork(params); + + assertEquals(1, stub.calls.size()); + assertEquals("sessions.fork", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_mcp_config_update_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new McpConfigUpdateParams("myServer", "new-config"); + server.mcp.config.update(params); + + assertEquals(1, stub.calls.size()); + assertEquals("mcp.config.update", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + @Test + void serverRpc_mcp_config_remove_invokes_correct_method() { + var stub = new StubCaller(); + var server = new ServerRpc(stub); + + var params = new McpConfigRemoveParams("myServer"); + server.mcp.config.remove(params); + + assertEquals(1, stub.calls.size()); + assertEquals("mcp.config.remove", stub.calls.get(0).method()); + assertSame(params, stub.calls.get(0).params()); + } + + // ── SessionRpc.mode ──────────────────────────────────────────────────── + + @Test + void sessionRpc_mode_get_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mode"); + + session.mode.get(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mode.get", stub.calls.get(0).method()); + var params = stub.calls.get(0).params(); + assertInstanceOf(Map.class, params); + assertEquals("sess-mode", ((Map) params).get("sessionId")); + } + + @Test + void sessionRpc_mode_set_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mode-set"); + + var modeParams = new SessionModeSetParams(null, null); + session.mode.set(modeParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mode.set", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-mode-set", params.get("sessionId").asText()); + } + + // ── SessionRpc.plan ──────────────────────────────────────────────────── + + @Test + void sessionRpc_plan_read_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-plan"); + + session.plan.read(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.plan.read", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-plan", params.get("sessionId")); + } + + @Test + void sessionRpc_plan_update_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-plan-upd"); + + var planParams = new SessionPlanUpdateParams(null, "# My Plan"); + session.plan.update(planParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.plan.update", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-plan-upd", params.get("sessionId").asText()); + } + + @Test + void sessionRpc_plan_delete_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-plan-del"); + + session.plan.delete(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.plan.delete", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-plan-del", params.get("sessionId")); + } + + // ── SessionRpc.workspace ─────────────────────────────────────────────── + + @Test + void sessionRpc_workspace_listFiles_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ws"); + + session.workspaces.listFiles(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.workspaces.listFiles", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-ws", params.get("sessionId")); + } + + @Test + void sessionRpc_workspace_readFile_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ws-rf"); + + var rfParams = new SessionWorkspacesReadFileParams(null, "/src/Main.java"); + session.workspaces.readFile(rfParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.workspaces.readFile", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ws-rf", params.get("sessionId").asText()); + assertEquals("/src/Main.java", params.get("path").asText()); + } + + @Test + void sessionRpc_workspace_createFile_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ws-cf"); + + var cfParams = new SessionWorkspacesCreateFileParams(null, "/new/file.txt", "content"); + session.workspaces.createFile(cfParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.workspaces.createFile", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ws-cf", params.get("sessionId").asText()); + } + + // ── SessionRpc.fleet ─────────────────────────────────────────────────── + + @Test + void sessionRpc_fleet_start_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-fleet"); + + var fleetParams = new SessionFleetStartParams(null, "fix all bugs"); + session.fleet.start(fleetParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.fleet.start", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-fleet", params.get("sessionId").asText()); + assertEquals("fix all bugs", params.get("prompt").asText()); + } + + // ── SessionRpc.skills ────────────────────────────────────────────────── + + @Test + void sessionRpc_skills_list_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-skills"); + + session.skills.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.skills.list", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-skills", params.get("sessionId")); + } + + @Test + void sessionRpc_skills_enable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-skills-en"); + + var enableParams = new SessionSkillsEnableParams(null, "my-skill"); + session.skills.enable(enableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.skills.enable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-skills-en", params.get("sessionId").asText()); + assertEquals("my-skill", params.get("name").asText()); + } + + @Test + void sessionRpc_skills_disable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-skills-dis"); + + var disableParams = new SessionSkillsDisableParams(null, "my-skill"); + session.skills.disable(disableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.skills.disable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-skills-dis", params.get("sessionId").asText()); + } + + @Test + void sessionRpc_skills_reload_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-skills-rel"); + + session.skills.reload(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.skills.reload", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-skills-rel", params.get("sessionId")); + } + + // ── SessionRpc.mcp ───────────────────────────────────────────────────── + + @Test + void sessionRpc_mcp_list_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mcp"); + + session.mcp.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mcp.list", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-mcp", params.get("sessionId")); + } + + @Test + void sessionRpc_mcp_enable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mcp-en"); + + var enableParams = new SessionMcpEnableParams(null, "my-mcp-server"); + session.mcp.enable(enableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mcp.enable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-mcp-en", params.get("sessionId").asText()); + assertEquals("my-mcp-server", params.get("serverName").asText()); + } + + @Test + void sessionRpc_mcp_disable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mcp-dis"); + + var disableParams = new SessionMcpDisableParams(null, "my-mcp-server"); + session.mcp.disable(disableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mcp.disable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-mcp-dis", params.get("sessionId").asText()); + assertEquals("my-mcp-server", params.get("serverName").asText()); + } + + @Test + void sessionRpc_mcp_reload_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-mcp-rel"); + + session.mcp.reload(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.mcp.reload", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-mcp-rel", params.get("sessionId")); + } + + // ── SessionRpc.plugins ───────────────────────────────────────────────── + + @Test + void sessionRpc_plugins_list_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-plugins"); + + session.plugins.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.plugins.list", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-plugins", params.get("sessionId")); + } + + // ── SessionRpc.extensions ────────────────────────────────────────────── + + @Test + void sessionRpc_extensions_list_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ext"); + + session.extensions.list(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.extensions.list", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-ext", params.get("sessionId")); + } + + @Test + void sessionRpc_extensions_enable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ext-en"); + + var enableParams = new SessionExtensionsEnableParams(null, "github.ext-id"); + session.extensions.enable(enableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.extensions.enable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ext-en", params.get("sessionId").asText()); + assertEquals("github.ext-id", params.get("id").asText()); + } + + @Test + void sessionRpc_extensions_disable_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ext-dis"); + + var disableParams = new SessionExtensionsDisableParams(null, "github.ext-id"); + session.extensions.disable(disableParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.extensions.disable", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ext-dis", params.get("sessionId").asText()); + } + + @Test + void sessionRpc_extensions_reload_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ext-rel"); + + session.extensions.reload(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.extensions.reload", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-ext-rel", params.get("sessionId")); + } + + // ── SessionRpc.tools ─────────────────────────────────────────────────── + + @Test + void sessionRpc_tools_handlePendingToolCall_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-tools"); + + var toolParams = new SessionToolsHandlePendingToolCallParams(null, "req-123", "ok", null); + session.tools.handlePendingToolCall(toolParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.tools.handlePendingToolCall", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-tools", params.get("sessionId").asText()); + assertEquals("req-123", params.get("requestId").asText()); + } + + // ── SessionRpc.commands ──────────────────────────────────────────────── + + @Test + void sessionRpc_commands_handlePendingCommand_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-cmds"); + + var cmdParams = new SessionCommandsHandlePendingCommandParams(null, "req-cmd-456", null); + session.commands.handlePendingCommand(cmdParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.commands.handlePendingCommand", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-cmds", params.get("sessionId").asText()); + assertEquals("req-cmd-456", params.get("requestId").asText()); + } + + // ── SessionRpc.ui ────────────────────────────────────────────────────── + + @Test + void sessionRpc_ui_elicitation_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ui"); + + var uiParams = new SessionUiElicitationParams(null, "Please provide info", null); + session.ui.elicitation(uiParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.ui.elicitation", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ui", params.get("sessionId").asText()); + assertEquals("Please provide info", params.get("message").asText()); + } + + @Test + void sessionRpc_ui_handlePendingElicitation_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-ui-elicit"); + + var elicitParams = new SessionUiHandlePendingElicitationParams(null, "req-elicit-789", null); + session.ui.handlePendingElicitation(elicitParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.ui.handlePendingElicitation", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-ui-elicit", params.get("sessionId").asText()); + assertEquals("req-elicit-789", params.get("requestId").asText()); + } + + // ── SessionRpc.permissions ───────────────────────────────────────────── + + @Test + void sessionRpc_permissions_handlePendingPermissionRequest_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-perm"); + + var permParams = new SessionPermissionsHandlePendingPermissionRequestParams(null, "req-perm-1", "allow"); + session.permissions.handlePendingPermissionRequest(permParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.permissions.handlePendingPermissionRequest", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-perm", params.get("sessionId").asText()); + assertEquals("req-perm-1", params.get("requestId").asText()); + } + + // ── SessionRpc.shell ─────────────────────────────────────────────────── + + @Test + void sessionRpc_shell_exec_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-shell"); + + var shellParams = new SessionShellExecParams(null, "ls -la", "/workspace", null); + session.shell.exec(shellParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.shell.exec", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-shell", params.get("sessionId").asText()); + assertEquals("ls -la", params.get("command").asText()); + } + + @Test + void sessionRpc_shell_kill_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-shell-kill"); + + var killParams = new SessionShellKillParams(null, "proc-123", null); + session.shell.kill(killParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.shell.kill", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-shell-kill", params.get("sessionId").asText()); + assertEquals("proc-123", params.get("processId").asText()); + } + + // ── SessionRpc.history ───────────────────────────────────────────────── + + @Test + void sessionRpc_history_compact_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-hist"); + + session.history.compact(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.history.compact", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-hist", params.get("sessionId")); + } + + @Test + void sessionRpc_history_truncate_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-hist-trunc"); + + var truncParams = new SessionHistoryTruncateParams(null, "event-id-abc"); + session.history.truncate(truncParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.history.truncate", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-hist-trunc", params.get("sessionId").asText()); + assertEquals("event-id-abc", params.get("eventId").asText()); + } + + // ── SessionRpc.usage ─────────────────────────────────────────────────── + + @Test + void sessionRpc_usage_getMetrics_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-usage"); + + session.usage.getMetrics(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.usage.getMetrics", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-usage", params.get("sessionId")); + } + + // ── SessionRpc.agent additional methods ──────────────────────────────── + + @Test + void sessionRpc_agent_getCurrent_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-agent-gc"); + + session.agent.getCurrent(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.agent.getCurrent", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-agent-gc", params.get("sessionId")); + } + + @Test + void sessionRpc_agent_deselect_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-agent-des"); + + session.agent.deselect(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.agent.deselect", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-agent-des", params.get("sessionId")); + } + + @Test + void sessionRpc_agent_reload_injects_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-agent-rel"); + + session.agent.reload(); + + assertEquals(1, stub.calls.size()); + assertEquals("session.agent.reload", stub.calls.get(0).method()); + var params = (Map) stub.calls.get(0).params(); + assertEquals("sess-agent-rel", params.get("sessionId")); + } + + // ── SessionRpc.log (top-level) ───────────────────────────────────────── + + @Test + void sessionRpc_log_merges_sessionId() { + var stub = new StubCaller(); + var session = new SessionRpc(stub, "sess-log"); + + var logParams = new SessionLogParams(null, "Hello from test", null, null, null); + session.log(logParams); + + assertEquals(1, stub.calls.size()); + assertEquals("session.log", stub.calls.get(0).method()); + var params = (com.fasterxml.jackson.databind.node.ObjectNode) stub.calls.get(0).params(); + assertEquals("sess-log", params.get("sessionId").asText()); + assertEquals("Hello from test", params.get("message").asText()); + } + + // ── SessionFs server-side methods (via SessionRpc) ───────────────────── + // SessionFs methods are accessed via ServerRpc.sessionFs; these tests + // cover the remaining SessionFs param records used server-side. + + @Test + void serverRpc_sessionFs_setProvider_params_record() { + var params = new SessionFsSetProviderParams("/workspace", "/state", null, null); + assertEquals("/workspace", params.initialCwd()); + assertEquals("/state", params.sessionStatePath()); + assertNull(params.conventions()); + assertNull(params.capabilities()); + } + + @Test + void sessionsForkParams_record() { + var params = new SessionsForkParams("parent-id", "event-123", null); + assertEquals("parent-id", params.sessionId()); + assertEquals("event-123", params.toEventId()); + } + + // ── SessionAgentDeselectResult (empty record) ────────────────────────── + + @Test + void sessionAgentDeselectResult_empty_record() { + var result = new SessionAgentDeselectResult(); + assertNotNull(result); + } + + // ── SessionLogParams enum ────────────────────────────────────────────── + + @Test + void sessionLogParams_level_enum_values() { + assertEquals("info", SessionLogLevel.INFO.getValue()); + assertEquals("warning", SessionLogLevel.WARNING.getValue()); + assertEquals("error", SessionLogLevel.ERROR.getValue()); + } + + @Test + void sessionLogParams_level_enum_fromValue() { + assertEquals(SessionLogLevel.INFO, SessionLogLevel.fromValue("info")); + assertEquals(SessionLogLevel.WARNING, SessionLogLevel.fromValue("warning")); + assertEquals(SessionLogLevel.ERROR, SessionLogLevel.fromValue("error")); + } + + @Test + void sessionLogParams_level_enum_fromValue_unknown_throws() { + assertThrows(IllegalArgumentException.class, () -> SessionLogLevel.fromValue("unknown-level")); + } +} diff --git a/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java new file mode 100644 index 000000000..e6ae7e7d9 --- /dev/null +++ b/java/src/test/java/com/github/copilot/sdk/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -0,0 +1,986 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.TestUtil; + +/** + * Tests for generated RPC param and result record types. Exercises + * constructors, field accessors, and enum variants to provide JaCoCo coverage + * of the generated code without requiring network access. + */ +class GeneratedRpcRecordsCoverageTest { + + // ── Params records ───────────────────────────────────────────────────── + + @Test + void pingParams_record() { + var params = new PingParams("hello"); + assertEquals("hello", params.message()); + assertNull(new PingParams(null).message()); + } + + @Test + void pingResult_record() { + var result = new PingResult("pong", 1234L, 2L); + assertEquals("pong", result.message()); + assertEquals(1234L, result.timestamp()); + assertEquals(2L, result.protocolVersion()); + } + + @Test + void mcpDiscoverParams_record() { + var params = new McpDiscoverParams("/workspace"); + assertEquals("/workspace", params.workingDirectory()); + assertNull(new McpDiscoverParams(null).workingDirectory()); + } + + @Test + void mcpConfigRemoveParams_record() { + var params = new McpConfigRemoveParams("old-server"); + assertEquals("old-server", params.name()); + } + + @Test + void mcpConfigUpdateParams_record() { + var params = new McpConfigUpdateParams("my-server", Map.of("key", "val")); + assertEquals("my-server", params.name()); + assertNotNull(params.config()); + } + + @Test + void toolsListParams_record() { + var params = new ToolsListParams("gpt-5"); + assertEquals("gpt-5", params.model()); + assertNull(new ToolsListParams(null).model()); + } + + @Test + void sessionsForkParams_record() { + var params = new SessionsForkParams("sess-1", "event-abc", null); + assertEquals("sess-1", params.sessionId()); + assertEquals("event-abc", params.toEventId()); + } + + @Test + void sessionAgentDeselectParams_record() { + var params = new SessionAgentDeselectParams("sess-1"); + assertEquals("sess-1", params.sessionId()); + } + + @Test + void sessionAgentGetCurrentParams_record() { + var params = new SessionAgentGetCurrentParams("sess-2"); + assertEquals("sess-2", params.sessionId()); + } + + @Test + void sessionAgentListParams_record() { + var params = new SessionAgentListParams("sess-3"); + assertEquals("sess-3", params.sessionId()); + } + + @Test + void sessionAgentReloadParams_record() { + var params = new SessionAgentReloadParams("sess-4"); + assertEquals("sess-4", params.sessionId()); + } + + @Test + void sessionAgentSelectParams_record() { + var params = new SessionAgentSelectParams("sess-5", "my-agent"); + assertEquals("sess-5", params.sessionId()); + assertEquals("my-agent", params.name()); + } + + @Test + void sessionCommandsHandlePendingCommandParams_record() { + var params = new SessionCommandsHandlePendingCommandParams("sess-6", "req-cmd", "error msg"); + assertEquals("sess-6", params.sessionId()); + assertEquals("req-cmd", params.requestId()); + assertEquals("error msg", params.error()); + } + + @Test + void sessionExtensionsDisableParams_record() { + var params = new SessionExtensionsDisableParams("sess-7", "ext-id-1"); + assertEquals("sess-7", params.sessionId()); + assertEquals("ext-id-1", params.id()); + } + + @Test + void sessionExtensionsEnableParams_record() { + var params = new SessionExtensionsEnableParams("sess-8", "ext-id-2"); + assertEquals("sess-8", params.sessionId()); + assertEquals("ext-id-2", params.id()); + } + + @Test + void sessionExtensionsListParams_record() { + var params = new SessionExtensionsListParams("sess-9"); + assertEquals("sess-9", params.sessionId()); + } + + @Test + void sessionExtensionsReloadParams_record() { + var params = new SessionExtensionsReloadParams("sess-10"); + assertEquals("sess-10", params.sessionId()); + } + + @Test + void sessionFleetStartParams_record() { + var params = new SessionFleetStartParams("sess-11", "fix all bugs"); + assertEquals("sess-11", params.sessionId()); + assertEquals("fix all bugs", params.prompt()); + } + + @Test + void sessionFsAppendFileParams_record() { + var params = new SessionFsAppendFileParams("sess-12", TestUtil.tempPath("log.txt"), "new line\n", null); + assertEquals("sess-12", params.sessionId()); + assertEquals(TestUtil.tempPath("log.txt"), params.path()); + assertEquals("new line\n", params.content()); + assertNull(params.mode()); + } + + @Test + void sessionFsExistsParams_record() { + var params = new SessionFsExistsParams("sess-13", TestUtil.tempPath("file.txt")); + assertEquals("sess-13", params.sessionId()); + assertEquals(TestUtil.tempPath("file.txt"), params.path()); + } + + @Test + void sessionFsMkdirParams_record() { + var params = new SessionFsMkdirParams("sess-14", TestUtil.tempPath("newdir"), true, null); + assertEquals("sess-14", params.sessionId()); + assertEquals(TestUtil.tempPath("newdir"), params.path()); + assertTrue(params.recursive()); + assertNull(params.mode()); + } + + @Test + void sessionFsReadFileParams_record() { + var params = new SessionFsReadFileParams("sess-15", "/src/Main.java"); + assertEquals("sess-15", params.sessionId()); + assertEquals("/src/Main.java", params.path()); + } + + @Test + void sessionFsReaddirParams_record() { + var params = new SessionFsReaddirParams("sess-16", "/src"); + assertEquals("sess-16", params.sessionId()); + assertEquals("/src", params.path()); + } + + @Test + void sessionFsReaddirWithTypesParams_record() { + var params = new SessionFsReaddirWithTypesParams("sess-17", "/src"); + assertEquals("sess-17", params.sessionId()); + assertEquals("/src", params.path()); + } + + @Test + void sessionFsRenameParams_record() { + var params = new SessionFsRenameParams("sess-18", "/old.txt", "/new.txt"); + assertEquals("sess-18", params.sessionId()); + assertEquals("/old.txt", params.src()); + assertEquals("/new.txt", params.dest()); + } + + @Test + void sessionFsRmParams_record() { + var params = new SessionFsRmParams("sess-19", TestUtil.tempPath("file.txt"), false, true); + assertEquals("sess-19", params.sessionId()); + assertEquals(TestUtil.tempPath("file.txt"), params.path()); + assertFalse(params.recursive()); + assertTrue(params.force()); + } + + @Test + void sessionFsSetProviderParams_conventions_enum() { + assertEquals("windows", SessionFsSetProviderConventions.WINDOWS.getValue()); + assertEquals("posix", SessionFsSetProviderConventions.POSIX.getValue()); + assertEquals(SessionFsSetProviderConventions.POSIX, SessionFsSetProviderConventions.fromValue("posix")); + assertThrows(IllegalArgumentException.class, () -> SessionFsSetProviderConventions.fromValue("unknown")); + } + + @Test + void sessionFsStatParams_record() { + var params = new SessionFsStatParams("sess-20", "/etc/hosts"); + assertEquals("sess-20", params.sessionId()); + assertEquals("/etc/hosts", params.path()); + } + + @Test + void sessionFsWriteFileParams_record() { + var params = new SessionFsWriteFileParams("sess-21", TestUtil.tempPath("out.txt"), "content here", null); + assertEquals("sess-21", params.sessionId()); + assertEquals(TestUtil.tempPath("out.txt"), params.path()); + assertEquals("content here", params.content()); + assertNull(params.mode()); + } + + @Test + void sessionHistoryCompactParams_record() { + var params = new SessionHistoryCompactParams("sess-22"); + assertEquals("sess-22", params.sessionId()); + } + + @Test + void sessionHistoryTruncateParams_record() { + var params = new SessionHistoryTruncateParams("sess-23", "event-id-xyz"); + assertEquals("sess-23", params.sessionId()); + assertEquals("event-id-xyz", params.eventId()); + } + + @Test + void sessionLogParams_record() { + var params = new SessionLogParams("sess-24", "test message", SessionLogLevel.INFO, false, null); + assertEquals("sess-24", params.sessionId()); + assertEquals("test message", params.message()); + assertEquals(SessionLogLevel.INFO, params.level()); + assertFalse(params.ephemeral()); + assertNull(params.url()); + } + + @Test + void sessionLogParams_level_enum_all_values() { + for (var level : SessionLogLevel.values()) { + assertNotNull(level.getValue()); + assertEquals(level, SessionLogLevel.fromValue(level.getValue())); + } + } + + @Test + void sessionMcpDisableParams_record() { + var params = new SessionMcpDisableParams("sess-25", "mcp-server-1"); + assertEquals("sess-25", params.sessionId()); + assertEquals("mcp-server-1", params.serverName()); + } + + @Test + void sessionMcpEnableParams_record() { + var params = new SessionMcpEnableParams("sess-26", "mcp-server-2"); + assertEquals("sess-26", params.sessionId()); + assertEquals("mcp-server-2", params.serverName()); + } + + @Test + void sessionMcpListParams_record() { + var params = new SessionMcpListParams("sess-27"); + assertEquals("sess-27", params.sessionId()); + } + + @Test + void sessionMcpReloadParams_record() { + var params = new SessionMcpReloadParams("sess-28"); + assertEquals("sess-28", params.sessionId()); + } + + @Test + void sessionModeGetParams_record() { + var params = new SessionModeGetParams("sess-29"); + assertEquals("sess-29", params.sessionId()); + } + + @Test + void sessionModeSetParams_record() { + var params = new SessionModeSetParams("sess-30", SessionMode.PLAN); + assertEquals("sess-30", params.sessionId()); + assertEquals(SessionMode.PLAN, params.mode()); + } + + @Test + void sessionModeSetParams_mode_enum() { + assertEquals("interactive", SessionMode.INTERACTIVE.getValue()); + assertEquals("plan", SessionMode.PLAN.getValue()); + assertEquals("autopilot", SessionMode.AUTOPILOT.getValue()); + for (var mode : SessionMode.values()) { + assertEquals(mode, SessionMode.fromValue(mode.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> SessionMode.fromValue("unknown-mode")); + } + + @Test + void sessionModelGetCurrentParams_record() { + var params = new SessionModelGetCurrentParams("sess-31"); + assertEquals("sess-31", params.sessionId()); + } + + @Test + void sessionModelSwitchToParams_record() { + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null); + assertEquals("sess-32", params.sessionId()); + assertEquals("claude-sonnet-4.5", params.modelId()); + assertEquals("high", params.reasoningEffort()); + assertNull(params.reasoningSummary()); + assertNull(params.modelCapabilities()); + } + + @Test + void sessionPermissionsHandlePendingPermissionRequestParams_record() { + var params = new SessionPermissionsHandlePendingPermissionRequestParams("sess-33", "req-1", "allow"); + assertEquals("sess-33", params.sessionId()); + assertEquals("req-1", params.requestId()); + assertEquals("allow", params.result()); + } + + @Test + void sessionPlanDeleteParams_record() { + var params = new SessionPlanDeleteParams("sess-34"); + assertEquals("sess-34", params.sessionId()); + } + + @Test + void sessionPlanReadParams_record() { + var params = new SessionPlanReadParams("sess-35"); + assertEquals("sess-35", params.sessionId()); + } + + @Test + void sessionPlanUpdateParams_record() { + var params = new SessionPlanUpdateParams("sess-36", "# My Plan\n1. Do stuff"); + assertEquals("sess-36", params.sessionId()); + assertEquals("# My Plan\n1. Do stuff", params.content()); + } + + @Test + void sessionPluginsListParams_record() { + var params = new SessionPluginsListParams("sess-37"); + assertEquals("sess-37", params.sessionId()); + } + + @Test + void sessionShellExecParams_record() { + var params = new SessionShellExecParams("sess-38", "ls -la", "/workspace", 5000L); + assertEquals("sess-38", params.sessionId()); + assertEquals("ls -la", params.command()); + assertEquals("/workspace", params.cwd()); + assertEquals(5000L, params.timeout()); + } + + @Test + void sessionShellKillParams_record() { + var params = new SessionShellKillParams("sess-39", "proc-abc", ShellKillSignal.SIGTERM); + assertEquals("sess-39", params.sessionId()); + assertEquals("proc-abc", params.processId()); + assertEquals(ShellKillSignal.SIGTERM, params.signal()); + } + + @Test + void sessionShellKillParams_signal_enum() { + assertEquals("SIGTERM", ShellKillSignal.SIGTERM.getValue()); + assertEquals("SIGKILL", ShellKillSignal.SIGKILL.getValue()); + assertEquals("SIGINT", ShellKillSignal.SIGINT.getValue()); + for (var sig : ShellKillSignal.values()) { + assertEquals(sig, ShellKillSignal.fromValue(sig.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> ShellKillSignal.fromValue("SIGHUP")); + } + + @Test + void sessionSkillsDisableParams_record() { + var params = new SessionSkillsDisableParams("sess-40", "my-skill"); + assertEquals("sess-40", params.sessionId()); + assertEquals("my-skill", params.name()); + } + + @Test + void sessionSkillsEnableParams_record() { + var params = new SessionSkillsEnableParams("sess-41", "another-skill"); + assertEquals("sess-41", params.sessionId()); + assertEquals("another-skill", params.name()); + } + + @Test + void sessionSkillsListParams_record() { + var params = new SessionSkillsListParams("sess-42"); + assertEquals("sess-42", params.sessionId()); + } + + @Test + void sessionSkillsReloadParams_record() { + var params = new SessionSkillsReloadParams("sess-43"); + assertEquals("sess-43", params.sessionId()); + } + + @Test + void sessionToolsHandlePendingToolCallParams_record() { + var params = new SessionToolsHandlePendingToolCallParams("sess-44", "req-tool-1", "result data", null); + assertEquals("sess-44", params.sessionId()); + assertEquals("req-tool-1", params.requestId()); + assertEquals("result data", params.result()); + assertNull(params.error()); + } + + @Test + void sessionUiElicitationParams_record() { + var params = new SessionUiElicitationParams("sess-45", "What is your name?", null); + assertEquals("sess-45", params.sessionId()); + assertEquals("What is your name?", params.message()); + assertNull(params.requestedSchema()); + } + + @Test + void sessionUiHandlePendingElicitationParams_record() { + var params = new SessionUiHandlePendingElicitationParams("sess-46", "req-elicit-1", null); + assertEquals("sess-46", params.sessionId()); + assertEquals("req-elicit-1", params.requestId()); + assertNull(params.result()); + } + + @Test + void sessionUsageGetMetricsParams_record() { + var params = new SessionUsageGetMetricsParams("sess-47"); + assertEquals("sess-47", params.sessionId()); + } + + @Test + void sessionWorkspaceCreateFileParams_record() { + var params = new SessionWorkspaceCreateFileParams("sess-48", "README.md", "# Hello"); + assertEquals("sess-48", params.sessionId()); + assertEquals("README.md", params.path()); + assertEquals("# Hello", params.content()); + } + + @Test + void sessionWorkspaceListFilesParams_record() { + var params = new SessionWorkspaceListFilesParams("sess-49"); + assertEquals("sess-49", params.sessionId()); + } + + @Test + void sessionWorkspaceReadFileParams_record() { + var params = new SessionWorkspaceReadFileParams("sess-50", "src/Main.java"); + assertEquals("sess-50", params.sessionId()); + assertEquals("src/Main.java", params.path()); + } + + // ── Result records ───────────────────────────────────────────────────── + + @Test + void pingResult_fields() { + var result = new PingResult("pong", 9999L, 1L); + assertEquals("pong", result.message()); + assertEquals(9999L, result.timestamp()); + assertEquals(1L, result.protocolVersion()); + } + + @Test + void sessionAgentDeselectResult_empty() { + assertNotNull(new SessionAgentDeselectResult()); + } + + @Test + void sessionAgentListResult_with_items() { + var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1"); + var result = new SessionAgentListResult(List.of(item)); + assertEquals(1, result.agents().size()); + assertEquals("name1", result.agents().get(0).name()); + assertEquals("Name One", result.agents().get(0).displayName()); + assertEquals("Desc 1", result.agents().get(0).description()); + assertEquals("/path/to/agent1", result.agents().get(0).path()); + } + + @Test + void sessionAgentGetCurrentResult_nested() { + var agent = new AgentInfo("agent-1", "Agent One", "Does things", null); + var result = new SessionAgentGetCurrentResult(agent); + assertEquals("agent-1", result.agent().name()); + assertEquals("Agent One", result.agent().displayName()); + assertEquals("Does things", result.agent().description()); + assertNull(result.agent().path()); + } + + @Test + void sessionAgentGetCurrentResult_null_agent() { + var result = new SessionAgentGetCurrentResult(null); + assertNull(result.agent()); + } + + @Test + void sessionAgentReloadResult_with_items() { + var item = new AgentInfo("a", "A", "Desc", "/path/to/a"); + var result = new SessionAgentReloadResult(List.of(item)); + assertEquals(1, result.agents().size()); + assertEquals("a", result.agents().get(0).name()); + } + + @Test + void sessionAgentSelectResult_nested() { + var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected"); + var result = new SessionAgentSelectResult(agent); + assertEquals("selected", result.agent().name()); + } + + @Test + void sessionCommandsHandlePendingCommandResult_record() { + var result = new SessionCommandsHandlePendingCommandResult(true); + assertTrue(result.success()); + assertFalse(new SessionCommandsHandlePendingCommandResult(false).success()); + } + + @Test + void sessionExtensionsDisableResult_empty() { + assertNotNull(new SessionExtensionsDisableResult()); + } + + @Test + void sessionExtensionsEnableResult_empty() { + assertNotNull(new SessionExtensionsEnableResult()); + } + + @Test + void sessionExtensionsListResult_nested() { + var ext = new Extension("ext-1", "My Extension", ExtensionSource.PROJECT, ExtensionStatus.RUNNING, 1234L); + var result = new SessionExtensionsListResult(List.of(ext)); + assertEquals(1, result.extensions().size()); + assertEquals("ext-1", result.extensions().get(0).id()); + assertEquals("My Extension", result.extensions().get(0).name()); + assertEquals(ExtensionSource.PROJECT, result.extensions().get(0).source()); + assertEquals(ExtensionStatus.RUNNING, result.extensions().get(0).status()); + assertEquals(1234L, result.extensions().get(0).pid()); + } + + @Test + void sessionExtensionsListResult_enums() { + for (var src : ExtensionSource.values()) { + assertNotNull(src.getValue()); + assertEquals(src, ExtensionSource.fromValue(src.getValue())); + } + for (var status : ExtensionStatus.values()) { + assertNotNull(status.getValue()); + assertEquals(status, ExtensionStatus.fromValue(status.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> ExtensionSource.fromValue("unknown")); + assertThrows(IllegalArgumentException.class, () -> ExtensionStatus.fromValue("unknown")); + } + + @Test + void sessionExtensionsReloadResult_empty() { + assertNotNull(new SessionExtensionsReloadResult()); + } + + @Test + void sessionFleetStartResult_record() { + var result = new SessionFleetStartResult(true); + assertTrue(result.started()); + assertFalse(new SessionFleetStartResult(false).started()); + } + + @Test + void sessionFsExistsResult_record() { + var result = new SessionFsExistsResult(true); + assertTrue(result.exists()); + assertFalse(new SessionFsExistsResult(false).exists()); + } + + @Test + void sessionFsReadFileResult_record() { + var result = new SessionFsReadFileResult("file content here", null); + assertEquals("file content here", result.content()); + } + + @Test + void sessionFsReaddirResult_record() { + var result = new SessionFsReaddirResult(List.of("file1.txt", "file2.txt"), null); + assertEquals(2, result.entries().size()); + assertEquals("file1.txt", result.entries().get(0)); + } + + @Test + void sessionFsReaddirWithTypesResult_nested() { + var entry = new SessionFsReaddirWithTypesEntry("myfile.txt", SessionFsReaddirWithTypesEntryType.FILE); + var result = new SessionFsReaddirWithTypesResult(List.of(entry), null); + assertEquals(1, result.entries().size()); + assertEquals("myfile.txt", result.entries().get(0).name()); + assertEquals(SessionFsReaddirWithTypesEntryType.FILE, result.entries().get(0).type()); + assertEquals("file", result.entries().get(0).type().getValue()); + } + + @Test + void sessionFsReaddirWithTypesResult_type_enum() { + for (var t : SessionFsReaddirWithTypesEntryType.values()) { + assertNotNull(t.getValue()); + assertEquals(t, SessionFsReaddirWithTypesEntryType.fromValue(t.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> SessionFsReaddirWithTypesEntryType.fromValue("symlink")); + } + + @Test + void sessionFsSetProviderResult_record() { + var result = new SessionFsSetProviderResult(true); + assertTrue(result.success()); + assertFalse(new SessionFsSetProviderResult(false).success()); + } + + @Test + void sessionFsStatResult_record() { + var result = new SessionFsStatResult(true, false, 1024L, null, null, null); + assertTrue(result.isFile()); + assertFalse(result.isDirectory()); + assertEquals(1024L, result.size()); + assertNull(result.mtime()); + assertNull(result.birthtime()); + } + + @Test + void sessionHistoryCompactResult_nested() { + var ctx = new HistoryCompactContextWindow(100000L, 5000L, 20L, 1000L, 3000L, 500L); + var result = new SessionHistoryCompactResult(true, 2000L, 5L, ctx); + assertTrue(result.success()); + assertEquals(2000L, result.tokensRemoved()); + assertEquals(5L, result.messagesRemoved()); + assertNotNull(result.contextWindow()); + assertEquals(100000L, result.contextWindow().tokenLimit()); + assertEquals(5000L, result.contextWindow().currentTokens()); + } + + @Test + void sessionHistoryTruncateResult_record() { + var result = new SessionHistoryTruncateResult(3L); + assertEquals(3L, result.eventsRemoved()); + } + + @Test + void sessionLogResult_record() { + var id = UUID.randomUUID(); + var result = new SessionLogResult(id); + assertEquals(id, result.eventId()); + } + + @Test + void sessionMcpDisableResult_empty() { + assertNotNull(new SessionMcpDisableResult()); + } + + @Test + void sessionMcpEnableResult_empty() { + assertNotNull(new SessionMcpEnableResult()); + } + + @Test + void sessionMcpListResult_nested() { + var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null); + var result = new SessionMcpListResult(List.of(server)); + assertEquals(1, result.servers().size()); + assertEquals("my-mcp", result.servers().get(0).name()); + assertEquals(McpServerStatus.CONNECTED, result.servers().get(0).status()); + assertEquals(McpServerSource.USER, result.servers().get(0).source()); + } + + @Test + void sessionMcpListResult_status_enum_all_values() { + for (var status : McpServerStatus.values()) { + assertNotNull(status.getValue()); + assertEquals(status, McpServerStatus.fromValue(status.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> McpServerStatus.fromValue("unknown-status")); + } + + @Test + void sessionMcpReloadResult_empty() { + assertNotNull(new SessionMcpReloadResult()); + } + + @Test + void sessionModeGetResult_enum() { + var result = new SessionModeGetResult(SessionModeGetResult.SessionModeGetResultMode.INTERACTIVE); + assertEquals(SessionModeGetResult.SessionModeGetResultMode.INTERACTIVE, result.mode()); + assertEquals("interactive", result.mode().getValue()); + for (var mode : SessionModeGetResult.SessionModeGetResultMode.values()) { + assertEquals(mode, SessionModeGetResult.SessionModeGetResultMode.fromValue(mode.getValue())); + } + assertThrows(IllegalArgumentException.class, + () -> SessionModeGetResult.SessionModeGetResultMode.fromValue("unknown")); + } + + @Test + void sessionModeSetResult_enum() { + var result = new SessionModeSetResult(SessionModeSetResult.SessionModeSetResultMode.AUTOPILOT); + assertEquals(SessionModeSetResult.SessionModeSetResultMode.AUTOPILOT, result.mode()); + assertEquals("autopilot", result.mode().getValue()); + } + + @Test + void sessionModelGetCurrentResult_record() { + var result = new SessionModelGetCurrentResult("claude-sonnet-4.5"); + assertEquals("claude-sonnet-4.5", result.modelId()); + } + + @Test + void sessionModelSwitchToResult_record() { + var result = new SessionModelSwitchToResult("gpt-5"); + assertEquals("gpt-5", result.modelId()); + } + + @Test + void sessionPermissionsHandlePendingPermissionRequestResult_record() { + var result = new SessionPermissionsHandlePendingPermissionRequestResult(true); + assertTrue(result.success()); + assertFalse(new SessionPermissionsHandlePendingPermissionRequestResult(false).success()); + } + + @Test + void sessionPlanDeleteResult_empty() { + assertNotNull(new SessionPlanDeleteResult()); + } + + @Test + void sessionPlanReadResult_record() { + var result = new SessionPlanReadResult(true, "# Plan\n1. Do stuff", "/workspace/.plan"); + assertTrue(result.exists()); + assertEquals("# Plan\n1. Do stuff", result.content()); + assertEquals("/workspace/.plan", result.path()); + } + + @Test + void sessionPlanUpdateResult_empty() { + assertNotNull(new SessionPlanUpdateResult()); + } + + @Test + void sessionPluginsListResult_nested() { + var plugin = new Plugin("my-plugin", "marketplace-x", "1.2.3", true); + var result = new SessionPluginsListResult(List.of(plugin)); + assertEquals(1, result.plugins().size()); + assertEquals("my-plugin", result.plugins().get(0).name()); + assertEquals("marketplace-x", result.plugins().get(0).marketplace()); + assertEquals("1.2.3", result.plugins().get(0).version()); + assertTrue(result.plugins().get(0).enabled()); + } + + @Test + void sessionShellExecResult_record() { + var result = new SessionShellExecResult("proc-id-123"); + assertEquals("proc-id-123", result.processId()); + } + + @Test + void sessionShellKillResult_record() { + var result = new SessionShellKillResult(true); + assertTrue(result.killed()); + assertFalse(new SessionShellKillResult(false).killed()); + } + + @Test + void sessionSkillsDisableResult_empty() { + assertNotNull(new SessionSkillsDisableResult()); + } + + @Test + void sessionSkillsEnableResult_empty() { + assertNotNull(new SessionSkillsEnableResult()); + } + + @Test + void sessionSkillsListResult_nested() { + var item = new Skill("deploy", "Deploy the app", SkillSource.PROJECT, true, true, "/skills/deploy.md"); + var result = new SessionSkillsListResult(List.of(item)); + assertEquals(1, result.skills().size()); + assertEquals("deploy", result.skills().get(0).name()); + assertEquals(SkillSource.PROJECT, result.skills().get(0).source()); + assertTrue(result.skills().get(0).enabled()); + } + + @Test + void sessionSkillsReloadResult_empty() { + assertNotNull(new SessionSkillsReloadResult(null, null)); + } + + @Test + void sessionToolsHandlePendingToolCallResult_record() { + var result = new SessionToolsHandlePendingToolCallResult(true); + assertTrue(result.success()); + assertFalse(new SessionToolsHandlePendingToolCallResult(false).success()); + } + + @Test + void sessionUiElicitationResult_accept() { + var result = new SessionUiElicitationResult(UIElicitationResponseAction.ACCEPT, Map.of("name", "Alice")); + assertEquals(UIElicitationResponseAction.ACCEPT, result.action()); + assertEquals("Alice", result.content().get("name")); + } + + @Test + void sessionUiElicitationResult_action_enum() { + assertEquals("accept", UIElicitationResponseAction.ACCEPT.getValue()); + assertEquals("decline", UIElicitationResponseAction.DECLINE.getValue()); + assertEquals("cancel", UIElicitationResponseAction.CANCEL.getValue()); + for (var a : UIElicitationResponseAction.values()) { + assertEquals(a, UIElicitationResponseAction.fromValue(a.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> UIElicitationResponseAction.fromValue("unknown")); + } + + @Test + void sessionUiHandlePendingElicitationResult_record() { + var result = new SessionUiHandlePendingElicitationResult(true); + assertTrue(result.success()); + } + + @Test + void sessionUsageGetMetricsResult_nested() { + var changes = new UsageMetricsCodeChanges(100L, 50L, 5L); + var result = new SessionUsageGetMetricsResult(0.5, 10L, null, null, 2000.0, 1700000000000L, changes, null, + "gpt-5", 1000L, 500L); + assertEquals(0.5, result.totalPremiumRequestCost()); + assertEquals(10L, result.totalUserRequests()); + assertNotNull(result.codeChanges()); + assertEquals(100L, result.codeChanges().linesAdded()); + assertEquals(50L, result.codeChanges().linesRemoved()); + assertEquals(5L, result.codeChanges().filesModifiedCount()); + assertEquals("gpt-5", result.currentModel()); + } + + @Test + void sessionWorkspaceCreateFileResult_empty() { + assertNotNull(new SessionWorkspaceCreateFileResult()); + } + + @Test + void sessionWorkspaceListFilesResult_record() { + var result = new SessionWorkspaceListFilesResult(List.of("src/Main.java", "README.md")); + assertEquals(2, result.files().size()); + assertEquals("src/Main.java", result.files().get(0)); + } + + @Test + void sessionWorkspaceReadFileResult_record() { + var result = new SessionWorkspaceReadFileResult("public class Main {}"); + assertEquals("public class Main {}", result.content()); + } + + @Test + void sessionsForkResult_record() { + var result = new SessionsForkResult("forked-sess-id", null); + assertEquals("forked-sess-id", result.sessionId()); + } + + // ── Complex nested result records ────────────────────────────────────── + + @Test + void accountGetQuotaResult_nested() { + var snapshot = new AccountQuotaSnapshot(null, 100L, 40L, null, 60.0, 5.0, true, + java.time.OffsetDateTime.parse("2026-05-01T00:00:00Z")); + var result = new AccountGetQuotaResult(Map.of("chat", snapshot)); + assertEquals(1, result.quotaSnapshots().size()); + var s = result.quotaSnapshots().get("chat"); + assertEquals(100L, s.entitlementRequests()); + assertEquals(40L, s.usedRequests()); + assertEquals(60.0, s.remainingPercentage()); + assertEquals(5.0, s.overage()); + assertTrue(s.overageAllowedWithExhaustedQuota()); + assertEquals(java.time.OffsetDateTime.parse("2026-05-01T00:00:00Z"), s.resetDate()); + } + + @Test + void mcpConfigListResult_record() { + var result = new McpConfigListResult(Map.of("server1", "config1")); + assertEquals(1, result.servers().size()); + assertEquals("config1", result.servers().get("server1")); + } + + @Test + void mcpDiscoverResult_nested() { + var server = new DiscoveredMcpServer("discovered-server", DiscoveredMcpServerType.STDIO, McpServerSource.USER, + true); + var result = new McpDiscoverResult(List.of(server)); + assertEquals(1, result.servers().size()); + assertEquals("discovered-server", result.servers().get(0).name()); + assertEquals(DiscoveredMcpServerType.STDIO, result.servers().get(0).type()); + assertEquals(McpServerSource.USER, result.servers().get(0).source()); + assertTrue(result.servers().get(0).enabled()); + } + + @Test + void mcpDiscoverResult_source_enum_all_values() { + for (var src : DiscoveredMcpServerSource.values()) { + assertNotNull(src.getValue()); + assertEquals(src, DiscoveredMcpServerSource.fromValue(src.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> DiscoveredMcpServerSource.fromValue("unknown-source")); + } + + @Test + void modelsListResult_nested() { + var supports = new ModelCapabilitiesSupports(true, false); + var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); + var capabilities = new ModelCapabilities(supports, limits); + var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); + var billing = new ModelBilling(1.0, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null); + var result = new ModelsListResult(List.of(modelItem)); + + assertEquals(1, result.models().size()); + assertEquals("gpt-5", result.models().get(0).id()); + assertEquals("GPT-5", result.models().get(0).name()); + assertTrue(result.models().get(0).capabilities().supports().vision()); + assertFalse(result.models().get(0).capabilities().supports().reasoningEffort()); + assertEquals(100000L, result.models().get(0).capabilities().limits().maxPromptTokens()); + assertEquals(ModelPolicyState.ENABLED, result.models().get(0).policy().state()); + assertEquals(Double.valueOf(1.0), result.models().get(0).billing().multiplier()); + } + + @Test + void toolsListResult_nested() { + var tool = new Tool("bash", "bash", "Run shell commands", Map.of("type", "object"), "Use for shell commands"); + var result = new ToolsListResult(List.of(tool)); + assertEquals(1, result.tools().size()); + assertEquals("bash", result.tools().get(0).name()); + assertEquals("bash", result.tools().get(0).namespacedName()); + assertEquals("Run shell commands", result.tools().get(0).description()); + assertEquals("Use for shell commands", result.tools().get(0).instructions()); + } + + // ── SessionModelSwitchToParams nested records ────────────────────────── + + @Test + void sessionModelSwitchToParams_nested_records() { + var limitsVision = new ModelCapabilitiesOverrideLimitsVision(List.of("image/png", "image/jpeg"), 10L, 5000000L); + var limits = new ModelCapabilitiesOverrideLimits(100000L, 8192L, 128000L, limitsVision); + var supports = new ModelCapabilitiesOverrideSupports(true, true); + var capabilities = new ModelCapabilitiesOverride(supports, limits); + var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, capabilities); + + assertEquals("gpt-5", params.modelId()); + assertNotNull(params.modelCapabilities()); + assertTrue(params.modelCapabilities().supports().vision()); + assertTrue(params.modelCapabilities().supports().reasoningEffort()); + assertEquals(100000L, params.modelCapabilities().limits().maxPromptTokens()); + assertEquals(2, params.modelCapabilities().limits().vision().supportedMediaTypes().size()); + } + + // ── SessionUiElicitationParams nested record ─────────────────────────── + + @Test + void sessionUiElicitationParams_nested_schema() { + var schema = new UIElicitationSchema("object", Map.of("name", Map.of("type", "string")), List.of("name")); + var params = new SessionUiElicitationParams("sess-elicit", "Please fill form", schema); + assertEquals("sess-elicit", params.sessionId()); + assertEquals("object", params.requestedSchema().type()); + assertTrue(params.requestedSchema().required().contains("name")); + } + + // ── SessionUiHandlePendingElicitationParams nested enum ──────────────── + + @Test + void sessionUiHandlePendingElicitationParamsResult_action_enum() { + for (var action : UIElicitationResponseAction.values()) { + assertNotNull(action.getValue()); + assertEquals(action, UIElicitationResponseAction.fromValue(action.getValue())); + } + assertThrows(IllegalArgumentException.class, () -> UIElicitationResponseAction.fromValue("unknown")); + } +} diff --git a/java/src/test/prompts/PROMPT-smoke-test.md b/java/src/test/prompts/PROMPT-smoke-test.md new file mode 100644 index 000000000..4013002ac --- /dev/null +++ b/java/src/test/prompts/PROMPT-smoke-test.md @@ -0,0 +1,135 @@ +# Prompt: Generate and Run the copilot-sdk-java Smoke Test + +## Objective + +Create a Maven project that acts as a smoke test for `copilot-sdk-java`. The project must compile, build, and run to completion with exit code 0 as the definition of success. + +## Step 1 β€” Read the source README + +Read the file `README.md` at the top level of this repository. You will need two sections from it: + +- **"Snapshot Builds"** β€” provides the Maven GAV (groupId, artifactId, version) and the Maven Central Snapshots repository configuration to use for the dependency under test. +- **"Quick Start"** β€” provides the exact Java source code for the smoke test program. Use this code verbatim. Do not modify it, fix it, or improve it. If it does not compile or run correctly against the artifact under test, that is itself a smoke test failure and must be reported as such rather than silently corrected. + +## Step 2 β€” Create the Maven project + +Create the following file layout in a subdirectory named `smoke-test/` at the top level of this repository: + +``` +smoke-test/ + pom.xml + src/main/java/(Class name taken from the code in the "Quick Start" section in the README).java ← verbatim from README "Quick Start" +``` + +### `pom.xml` requirements + +- **groupId**: `com.github` (or any reasonable value) +- **artifactId**: `copilot-sdk-smoketest` +- **version**: `1.0-SNAPSHOT` +- **packaging**: `jar` +- **Java source/target**: (taken from the "Requirements" section in the README) (via `maven.compiler.source` and `maven.compiler.target` properties) +- **`mainClass` property**: (taken from the "Quick Start" section in the README) (the class is in the default package) + +#### Snapshot repository + +Configure the Maven Central Snapshots repository exactly as specified in the "Snapshot Builds" section of `README.md`, and add `always` inside the `` block so that every build fetches the latest snapshot without requiring `-U`: + +```xml + + central-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + true + always + + +``` + +#### Dependency + +Use the GAV from the "Snapshot Builds" section of `README.md` verbatim β€” do not substitute the release version from the "Maven" section. + +#### Plugins β€” REQUIRED configuration + +**Do not use `maven-shade-plugin`.** Use the `Class-Path` manifest approach instead: + +1. **`maven-jar-plugin`** (version **3.4.1** β€” pin explicitly to suppress Maven version warnings): + + ```xml + + org.apache.maven.plugins + maven-jar-plugin + 3.4.1 + + + + ${mainClass} + true + lib/ + false + + + + + ``` + + **Critical**: `false` is mandatory. Without it, the manifest `Class-Path:` entry uses the timestamped SNAPSHOT filename (e.g. `copilot-sdk-java-0.1.33-20260312.125508-3.jar`) while `copy-dependencies` writes the base SNAPSHOT filename (`copilot-sdk-java-0.1.33-SNAPSHOT.jar`), causing `NoClassDefFoundError` at runtime. + +2. **`maven-dependency-plugin`** (version **3.6.1**): + + ```xml + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + copy-dependencies + package + copy-dependencies + + ${project.build.directory}/lib + + + + + ``` + + This copies all runtime dependency JARs into `target/lib/`, which is where the manifest `Class-Path:` points. + +## Step 3 β€” Build + +```bash +mvn -U clean package +``` + +The `-U` flag forces a fresh snapshot metadata check regardless of local cache. The `always` already handles this for normal invocations, but `-U` is the safest choice for CI. + +Build must succeed with `BUILD SUCCESS` before proceeding. + +## Step 4 β€” Run + +```bash +java -jar ./target/copilot-sdk-smoketest-1.0-SNAPSHOT.jar +``` + +The JAR must be run from the `smoke-test/` directory so that the relative `lib/` path in the manifest resolves correctly. Do not use `-cp` or `-classpath` β€” the test specifically validates that `java -jar` works with the manifest `Class-Path:` approach. + +## Step 5 β€” Verify success + +The smoke test passes if and only if the process exits with code **0**. + +The "Quick Start" code in `README.md` already contains the exit-code logic: it captures the last assistant message and calls `System.exit(0)` if it contains `"4"` (the expected answer to "What is 2+2?"), or `System.exit(-1)` otherwise. + +Check the exit code: +```bash +echo "Exit code: $?" +``` + +Expected: `Exit code: 0` + +## Important API notes (do not apply these as fixes β€” they are here for diagnostic context only) + +If the build fails with compilation errors such as `cannot find symbol` on methods like `getContent()`, `getCurrentTokens()`, `getTokenLimit()`, or `getMessagesLength()`, this indicates a mismatch between the Quick Start code and the SDK implementation. **Do not silently fix the code.** Report the failure. The purpose of this smoke test is precisely to catch such regressions. + +For reference: the data classes in `copilot-sdk-java` are Java **records**. Record accessor methods have no `get` prefix β€” they are named `content()`, `currentTokens()`, `tokenLimit()`, and `messagesLength()`. If the README Quick Start uses `getContent()` etc., that is a bug in the README that must be surfaced, not silently corrected. diff --git a/java/src/test/resources/logging-debug.properties b/java/src/test/resources/logging-debug.properties new file mode 100644 index 000000000..096ed002e --- /dev/null +++ b/java/src/test/resources/logging-debug.properties @@ -0,0 +1,21 @@ +# Debug logging configuration for tests +# Use with: mvn test -Pdebug + +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINE +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter + +# Log4J-style format: timestamp [thread] LEVEL logger - message +# Format parameters: +# %1$tF %1$tT.%1$tL = date time.millis (2026-02-01 20:30:45.123) +# %4$-7s = level padded to 7 chars (FINE, INFO, WARNING) +# %3$s = logger name +# %5$s = message +# %6$s = throwable (if any) +java.util.logging.SimpleFormatter.format=%1$tF %1$tT.%1$tL %4$-7s [%3$s] %5$s%6$s%n + +# Set FINE level for Copilot SDK classes +com.github.copilot.sdk.level=FINE + +# Root logger level +.level=INFO diff --git a/java/src/test/resources/logging.properties b/java/src/test/resources/logging.properties new file mode 100644 index 000000000..d294b5661 --- /dev/null +++ b/java/src/test/resources/logging.properties @@ -0,0 +1,8 @@ +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=%1$tF %1$tT.%1$tL %4$-7s [%3$s] %5$s%6$s%n + +com.github.copilot.sdk.level=INFO + +.level=INFO diff --git a/java/test b/java/test new file mode 100644 index 000000000..e69de29bb