From 46f6f3e6db7e0b424a8565080f8844a45de81058 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 13:51:45 -0400 Subject: [PATCH 1/8] Add Java SDK cookbook with 7 recipes Add complete Java cookbook matching the pattern of existing .NET, Go, Node.js, and Python cookbooks. All 7 recipes included: - Ralph Loop: Autonomous AI task loops with JBang - Error Handling: try-with-resources, ExecutionException, timeouts - Multiple Sessions: Parallel sessions with CompletableFuture - Managing Local Files: AI-powered file organization - PR Visualization: Interactive PR age charts - Persisting Sessions: Save/resume with custom IDs - Accessibility Report: WCAG reports via Playwright MCP Each recipe includes both markdown documentation and a standalone JBang-runnable Java file in recipe/. --- cookbook/README.md | 2 +- cookbook/copilot-sdk/README.md | 19 +- cookbook/copilot-sdk/java/README.md | 21 ++ .../copilot-sdk/java/accessibility-report.md | 238 +++++++++++++ cookbook/copilot-sdk/java/error-handling.md | 183 ++++++++++ .../copilot-sdk/java/managing-local-files.md | 209 ++++++++++++ .../copilot-sdk/java/multiple-sessions.md | 144 ++++++++ .../copilot-sdk/java/persisting-sessions.md | 320 ++++++++++++++++++ cookbook/copilot-sdk/java/pr-visualization.md | 235 +++++++++++++ cookbook/copilot-sdk/java/ralph-loop.md | 247 ++++++++++++++ .../java/recipe/AccessibilityReport.java | 123 +++++++ .../java/recipe/ErrorHandling.java | 34 ++ .../java/recipe/ManagingLocalFiles.java | 64 ++++ .../java/recipe/MultipleSessions.java | 37 ++ .../java/recipe/PRVisualization.java | 182 ++++++++++ .../java/recipe/PersistingSessions.java | 34 ++ cookbook/copilot-sdk/java/recipe/README.md | 71 ++++ .../copilot-sdk/java/recipe/RalphLoop.java | 55 +++ 18 files changed, 2216 insertions(+), 2 deletions(-) create mode 100644 cookbook/copilot-sdk/java/README.md create mode 100644 cookbook/copilot-sdk/java/accessibility-report.md create mode 100644 cookbook/copilot-sdk/java/error-handling.md create mode 100644 cookbook/copilot-sdk/java/managing-local-files.md create mode 100644 cookbook/copilot-sdk/java/multiple-sessions.md create mode 100644 cookbook/copilot-sdk/java/persisting-sessions.md create mode 100644 cookbook/copilot-sdk/java/pr-visualization.md create mode 100644 cookbook/copilot-sdk/java/ralph-loop.md create mode 100644 cookbook/copilot-sdk/java/recipe/AccessibilityReport.java create mode 100644 cookbook/copilot-sdk/java/recipe/ErrorHandling.java create mode 100644 cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java create mode 100644 cookbook/copilot-sdk/java/recipe/MultipleSessions.java create mode 100644 cookbook/copilot-sdk/java/recipe/PRVisualization.java create mode 100644 cookbook/copilot-sdk/java/recipe/PersistingSessions.java create mode 100644 cookbook/copilot-sdk/java/recipe/README.md create mode 100644 cookbook/copilot-sdk/java/recipe/RalphLoop.java diff --git a/cookbook/README.md b/cookbook/README.md index 797ce76d..0ca90d3a 100644 --- a/cookbook/README.md +++ b/cookbook/README.md @@ -10,7 +10,7 @@ The cookbook is organized by tool or product, with recipes collected by language Ready-to-use recipes for building with the GitHub Copilot SDK across multiple languages. -- **[Copilot SDK Cookbook](copilot-sdk/)** - Recipes for .NET, Go, Node.js, and Python +- **[Copilot SDK Cookbook](copilot-sdk/)** - Recipes for .NET, Go, Java, Node.js, and Python - Error handling, session management, file operations, and more - Runnable examples for each language - Best practices and complete implementation guides diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 3e2738d1..d9a127f9 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -44,6 +44,16 @@ This cookbook collects small, focused recipes showing how to accomplish common t - [Persisting Sessions](go/persisting-sessions.md): Save and resume sessions across restarts. - [Accessibility Report](go/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. +### Java + +- [Ralph Loop](java/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. +- [Error Handling](java/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. +- [Multiple Sessions](java/multiple-sessions.md): Manage multiple independent conversations simultaneously. +- [Managing Local Files](java/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. +- [PR Visualization](java/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. +- [Persisting Sessions](java/persisting-sessions.md): Save and resume sessions across restarts. +- [Accessibility Report](java/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. + ## How to Use - Browse your language section above and open the recipe links @@ -84,6 +94,13 @@ cd go/cookbook/recipe go run .go ``` +### Java + +```bash +cd java/cookbook/recipe +jbang .java +``` + ## Contributing - Propose or add a new recipe by creating a markdown file in your language's `cookbook/` folder and a runnable example in `recipe/` @@ -91,4 +108,4 @@ go run .go ## Status -Cookbook structure is complete with 7 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. +Cookbook structure is complete with 7 recipes across all 5 supported languages. Each recipe includes both markdown documentation and runnable examples. diff --git a/cookbook/copilot-sdk/java/README.md b/cookbook/copilot-sdk/java/README.md new file mode 100644 index 00000000..fb335a19 --- /dev/null +++ b/cookbook/copilot-sdk/java/README.md @@ -0,0 +1,21 @@ +# 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. All examples can be run directly with [JBang](https://www.jbang.dev/). + +## Recipes + +- [Ralph Loop](ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. +- [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. +- [Accessibility Report](accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. + +## Contributing + +Add a new recipe by creating a markdown file in this folder and linking it above. Follow repository guidance in [CONTRIBUTING.md](../../../CONTRIBUTING.md). + +## Status + +These recipes are complete, practical examples and can be used directly or adapted for your own projects. diff --git a/cookbook/copilot-sdk/java/accessibility-report.md b/cookbook/copilot-sdk/java/accessibility-report.md new file mode 100644 index 00000000..2b6abbe6 --- /dev/null +++ b/cookbook/copilot-sdk/java/accessibility-report.md @@ -0,0 +1,238 @@ +# Generating Accessibility Reports + +Build a CLI tool that analyzes web page accessibility using the Playwright MCP server and generates detailed WCAG-compliant reports with optional test generation. + +> **Runnable example:** [recipe/AccessibilityReport.java](recipe/AccessibilityReport.java) +> +> ```bash +> jbang recipe/AccessibilityReport.java +> ``` + +## Example scenario + +You want to audit a website's accessibility compliance. This tool navigates to a URL using Playwright, captures an accessibility snapshot, and produces a structured report covering WCAG criteria like landmarks, heading hierarchy, focus management, and touch targets. It can also generate Playwright test files to automate future accessibility checks. + +## Prerequisites + +Install [JBang](https://www.jbang.dev/) and ensure `npx` is available (Node.js installed) for the Playwright MCP server: + +```bash +# macOS (using Homebrew) +brew install jbangdev/tap/jbang + +# Verify npx is available (needed for Playwright MCP) +npx --version +``` + +## Usage + +```bash +jbang AccessibilityReport.java +# Enter a URL when prompted +``` + +## Full example: AccessibilityReport.java + +```java +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; + +public class AccessibilityReport { + public static void main(String[] args) throws Exception { + System.out.println("=== Accessibility Report Generator ===\n"); + + var reader = new BufferedReader(new InputStreamReader(System.in)); + + System.out.print("Enter URL to analyze: "); + String url = reader.readLine().trim(); + if (url.isEmpty()) { + System.out.println("No URL provided. Exiting."); + return; + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + System.out.printf("%nAnalyzing: %s%n", url); + System.out.println("Please wait...\n"); + + try (var client = new CopilotClient()) { + client.start().get(); + + // Configure Playwright MCP server for browser automation + var mcpConfig = new McpServerConfig() + .setType("local") + .setCommand("npx") + .setArgs(List.of("@playwright/mcp@latest")) + .setTools(List.of("*")); + + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("claude-opus-4.6") + .setStreaming(true) + .setMcpServers(Map.of("playwright", mcpConfig)) + ).get(); + + // Stream output token-by-token + var idleLatch = new CountDownLatch(1); + + session.on(AssistantMessageDeltaEvent.class, + ev -> System.out.print(ev.getData().deltaContent())); + + session.on(SessionIdleEvent.class, + ev -> idleLatch.countDown()); + + session.on(SessionErrorEvent.class, ev -> { + System.err.printf("%nError: %s%n", ev.getData().message()); + idleLatch.countDown(); + }); + + String prompt = """ + Use the Playwright MCP server to analyze the accessibility of this webpage: %s + + Please: + 1. Navigate to the URL using playwright-browser_navigate + 2. Take an accessibility snapshot using playwright-browser_snapshot + 3. Analyze the snapshot and provide a detailed accessibility report + + Format the report with emoji indicators: + - πŸ“Š Accessibility Report header + - βœ… What's Working Well (table with Category, Status, Details) + - ⚠️ Issues Found (table with Severity, Issue, WCAG Criterion, Recommendation) + - πŸ“‹ Stats Summary (links, headings, focusable elements, landmarks) + - βš™οΈ Priority Recommendations + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis. + """.formatted(url); + + session.send(new MessageOptions().setPrompt(prompt)); + idleLatch.await(); + + System.out.println("\n\n=== Report Complete ===\n"); + + // Prompt user for test generation + System.out.print("Would you like to generate Playwright accessibility tests? (y/n): "); + String generateTests = reader.readLine().trim(); + + if (generateTests.equalsIgnoreCase("y") || generateTests.equalsIgnoreCase("yes")) { + var testLatch = new CountDownLatch(1); + + session.on(SessionIdleEvent.class, + ev -> testLatch.countDown()); + + String testPrompt = """ + Based on the accessibility report you just generated for %s, + create Playwright accessibility tests in Java. + + Include tests for: lang attribute, title, heading hierarchy, alt text, + landmarks, skip navigation, focus indicators, and touch targets. + Use Playwright's accessibility testing features with helpful comments. + Output the complete test file. + """.formatted(url); + + System.out.println("\nGenerating accessibility tests...\n"); + session.send(new MessageOptions().setPrompt(testPrompt)); + testLatch.await(); + + System.out.println("\n\n=== Tests Generated ==="); + } + + session.close(); + } + } +} +``` + +## How it works + +1. **Playwright MCP server**: Configures a local MCP server running `@playwright/mcp` to provide browser automation tools +2. **Streaming output**: Uses `streaming: true` and `AssistantMessageDeltaEvent` for real-time token-by-token output +3. **Accessibility snapshot**: Playwright's `browser_snapshot` tool captures the full accessibility tree of the page +4. **Structured report**: The prompt engineers a consistent WCAG-aligned report format with emoji severity indicators +5. **Test generation**: Optionally generates Playwright accessibility tests based on the analysis + +## Key concepts + +### MCP server configuration + +The recipe configures a local MCP server that runs alongside the session: + +```java +var mcpConfig = new McpServerConfig() + .setType("local") + .setCommand("npx") + .setArgs(List.of("@playwright/mcp@latest")) + .setTools(List.of("*")); + +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setMcpServers(Map.of("playwright", mcpConfig)) +).get(); +``` + +This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`. + +### Streaming with events + +Unlike `sendAndWait`, this recipe uses streaming for real-time output: + +```java +session.on(AssistantMessageDeltaEvent.class, + ev -> System.out.print(ev.getData().deltaContent())); + +session.on(SessionIdleEvent.class, + ev -> idleLatch.countDown()); +``` + +A `CountDownLatch` synchronizes the main thread with the async event stream β€” when the session becomes idle, the latch releases and the program continues. + +## Sample interaction + +``` +=== Accessibility Report Generator === + +Enter URL to analyze: github.com + +Analyzing: https://github.com +Please wait... + +πŸ“Š Accessibility Report: GitHub (github.com) + +βœ… What's Working Well +| Category | Status | Details | +|----------|--------|---------| +| Language | βœ… Pass | lang="en" properly set | +| Page Title | βœ… Pass | "GitHub" is recognizable | +| Heading Hierarchy | βœ… Pass | Proper H1/H2 structure | +| Images | βœ… Pass | All images have alt text | + +⚠️ Issues Found +| Severity | Issue | WCAG Criterion | Recommendation | +|----------|-------|----------------|----------------| +| 🟑 Medium | Some links lack descriptive text | 2.4.4 | Add aria-label to icon-only links | + +πŸ“‹ Stats Summary +- Total Links: 47 +- Total Headings: 8 (1Γ— H1, proper hierarchy) +- Focusable Elements: 52 +- Landmarks Found: banner βœ…, navigation βœ…, main βœ…, footer βœ… + +=== Report Complete === + +Would you like to generate Playwright accessibility tests? (y/n): y + +Generating accessibility tests... +[Generated test file output...] + +=== Tests Generated === +``` diff --git a/cookbook/copilot-sdk/java/error-handling.md b/cookbook/copilot-sdk/java/error-handling.md new file mode 100644 index 00000000..8c2dd026 --- /dev/null +++ b/cookbook/copilot-sdk/java/error-handling.md @@ -0,0 +1,183 @@ +# Error Handling Patterns + +Handle errors gracefully in your Copilot SDK applications. + +> **Runnable example:** [recipe/ErrorHandling.java](recipe/ErrorHandling.java) +> +> ```bash +> jbang recipe/ErrorHandling.java +> ``` + +## Example scenario + +You need to handle various error conditions like connection failures, timeouts, and invalid responses. + +## Basic try-with-resources + +Java's `try-with-resources` ensures the client is always cleaned up, even when exceptions occur. + +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.json.*; + +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(); + + var response = session.sendAndWait( + new MessageOptions().setPrompt("Hello!")).get(); + System.out.println(response.getData().content()); + + session.close(); + } catch (Exception ex) { + System.err.println("Error: " + ex.getMessage()); + } + } +} +``` + +## Handling specific error types + +Every `CompletableFuture.get()` call wraps failures in `ExecutionException`. Unwrap the cause to inspect the real error. + +```java +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +try (var client = new CopilotClient()) { + client.start().get(); +} catch (ExecutionException ex) { + var cause = ex.getCause(); + if (cause instanceof IOException) { + System.err.println("Copilot CLI not found or could not connect: " + cause.getMessage()); + } else { + System.err.println("Unexpected error: " + cause.getMessage()); + } +} catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + System.err.println("Interrupted while starting client."); +} +``` + +## Timeout handling + +Use the overloaded `get(timeout, unit)` on `CompletableFuture` to enforce time limits. + +```java +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get(); + +try { + var response = session.sendAndWait( + new MessageOptions().setPrompt("Complex question...")) + .get(30, TimeUnit.SECONDS); + + System.out.println(response.getData().content()); +} catch (TimeoutException ex) { + System.err.println("Request timed out after 30 seconds."); + session.abort().get(); +} +``` + +## Aborting a request + +Cancel an in-flight request by calling `session.abort()`. + +```java +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get(); + +// Start a request without waiting +session.send(new MessageOptions().setPrompt("Write a very long story...")); + +// Abort after some condition +Thread.sleep(5000); +session.abort().get(); +System.out.println("Request aborted."); +``` + +## Graceful shutdown + +Use a JVM shutdown hook to clean up when the process is interrupted. + +```java +var client = new CopilotClient(); +client.start().get(); + +Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Shutting down..."); + try { + client.close(); + } catch (Exception ex) { + System.err.println("Cleanup error: " + ex.getMessage()); + } +})); +``` + +## Try-with-resources (nested) + +When working with multiple sessions, nest `try-with-resources` blocks to guarantee each resource is closed. + +```java +try (var client = new CopilotClient()) { + client.start().get(); + + try (var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get()) { + + session.sendAndWait( + new MessageOptions().setPrompt("Hello!")).get(); + } // session is closed here + +} // client is closed here +``` + +## Handling tool errors + +When defining tools, return an error result to signal a failure back to the model instead of throwing. + +```java +import com.github.copilot.sdk.json.ToolResultObject; + +session.addTool( + ToolDefinition.builder() + .name("read_file") + .description("Read a file from disk") + .parameter("path", "string", "File path", true) + .build(), + (args) -> { + try { + var content = java.nio.file.Files.readString( + java.nio.file.Path.of(args.get("path").toString())); + return ToolResultObject.success(content); + } catch (IOException ex) { + return ToolResultObject.error("Failed to read file: " + ex.getMessage()); + } + } +); +``` + +## Best practices + +1. **Use try-with-resources**: Always wrap `CopilotClient` (and sessions, if `AutoCloseable`) in try-with-resources to guarantee cleanup. +2. **Unwrap `ExecutionException`**: Call `getCause()` to inspect the real error β€” the outer `ExecutionException` is just a `CompletableFuture` wrapper. +3. **Restore interrupt flag**: When catching `InterruptedException`, call `Thread.currentThread().interrupt()` to preserve the interrupted status. +4. **Set timeouts**: Use `get(timeout, TimeUnit)` instead of bare `get()` for any call that could block indefinitely. +5. **Return tool errors, don't throw**: Use `ToolResultObject.error()` so the model can recover gracefully. +6. **Log errors**: Capture error details for debugging β€” consider a logging framework like SLF4J for production applications. diff --git a/cookbook/copilot-sdk/java/managing-local-files.md b/cookbook/copilot-sdk/java/managing-local-files.md new file mode 100644 index 00000000..d347c7fe --- /dev/null +++ b/cookbook/copilot-sdk/java/managing-local-files.md @@ -0,0 +1,209 @@ +# Grouping Files by Metadata + +Use Copilot to intelligently organize files in a folder based on their metadata. + +> **Runnable example:** [recipe/ManagingLocalFiles.java](recipe/ManagingLocalFiles.java) +> +> ```bash +> jbang recipe/ManagingLocalFiles.java +> ``` + +## 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 recipe/ManagingLocalFiles.java /path/to/your/folder + +# Or run without arguments to use a safe default (temp directory) +jbang recipe/ManagingLocalFiles.java +``` + +**Code:** +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +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.events.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.events.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:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; +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/cookbook/copilot-sdk/java/multiple-sessions.md b/cookbook/copilot-sdk/java/multiple-sessions.md new file mode 100644 index 00000000..585ee5fd --- /dev/null +++ b/cookbook/copilot-sdk/java/multiple-sessions.md @@ -0,0 +1,144 @@ +# Working with Multiple Sessions + +Manage multiple independent conversations simultaneously. + +> **Runnable example:** [recipe/MultipleSessions.java](recipe/MultipleSessions.java) +> +> ```bash +> jbang MultipleSessions.java +> ``` + +## Example scenario + +You need to run multiple conversations in parallel, each with its own context and history. + +## Java + +```java +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.json.*; + +var client = new CopilotClient(); +client.start().get(); + +// Create multiple independent sessions +var session1 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); +var session2 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); +var session3 = client.createSession(new SessionConfig() + .setModel("claude-sonnet-4.5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + +// Each session maintains its own conversation history +session1.sendAndWait(new MessageOptions().setPrompt("You are helping with a Python project")).get(); +session2.sendAndWait(new MessageOptions().setPrompt("You are helping with a TypeScript project")).get(); +session3.sendAndWait(new MessageOptions().setPrompt("You are helping with a Go project")).get(); + +// Follow-up messages stay in their respective contexts +session1.sendAndWait(new MessageOptions().setPrompt("How do I create a virtual environment?")).get(); +session2.sendAndWait(new MessageOptions().setPrompt("How do I set up tsconfig?")).get(); +session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); + +// Clean up all sessions +session1.destroy().get(); +session2.destroy().get(); +session3.destroy().get(); +client.stop().get(); +``` + +## Custom session IDs + +Use custom IDs for easier tracking: + +```java +var session = client.createSession(new SessionConfig() + .setSessionId("user-123-chat") + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + +System.out.println(session.getSessionId()); // "user-123-chat" +``` + +## Listing sessions + +```java +var sessions = client.listSessions().get(); +System.out.println(sessions); +// [SessionInfo{sessionId="user-123-chat", ...}, ...] +``` + +## Deleting sessions + +```java +// Delete a specific session +client.deleteSession("user-123-chat").get(); +``` + +## Managing session lifecycle with CompletableFuture + +Create and message sessions in parallel using `CompletableFuture.allOf`: + +```java +import java.util.concurrent.CompletableFuture; + +// Create all sessions in parallel +var f1 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); +var f2 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); +var f3 = client.createSession(new SessionConfig() + .setModel("claude-sonnet-4.5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); + +CompletableFuture.allOf(f1, f2, f3).get(); + +var s1 = f1.get(); +var s2 = f2.get(); +var s3 = f3.get(); + +// Send messages in parallel +CompletableFuture.allOf( + s1.sendAndWait(new MessageOptions().setPrompt("Explain Java records")), + s2.sendAndWait(new MessageOptions().setPrompt("Explain sealed classes")), + s3.sendAndWait(new MessageOptions().setPrompt("Explain pattern matching")) +).get(); +``` + +## Providing a custom Executor + +Supply your own thread pool for parallel session work: + +```java +import java.util.concurrent.Executors; + +var executor = Executors.newFixedThreadPool(4); + +var client = new CopilotClient(new CopilotClientOptions() + .setExecutor(executor)); +client.start().get(); + +// Sessions now run on the custom executor +var session = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + +session.sendAndWait(new MessageOptions().setPrompt("Hello!")).get(); + +session.destroy().get(); +client.stop().get(); +executor.shutdown(); +``` + +## 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 diff --git a/cookbook/copilot-sdk/java/persisting-sessions.md b/cookbook/copilot-sdk/java/persisting-sessions.md new file mode 100644 index 00000000..5de9c0e2 --- /dev/null +++ b/cookbook/copilot-sdk/java/persisting-sessions.md @@ -0,0 +1,320 @@ +# Session Persistence and Resumption + +Save and restore conversation sessions across application restarts. + +> **Runnable example:** [recipe/PersistingSessions.java](recipe/PersistingSessions.java) +> +> ```bash +> jbang recipe/PersistingSessions.java +> ``` + +## Example scenario + +You want users to be able to continue a conversation even after closing and reopening your application. The Copilot SDK persists session state to disk automatically β€” you just need to provide a stable session ID and resume later. + +## Creating a session with a custom ID + +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.SessionConfig; + +public class CreateSessionWithId { + 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 +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.PermissionHandler; +import com.github.copilot.sdk.json.ResumeSessionConfig; + +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 +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; + +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 +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; + +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:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.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) { + 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 + +This interactive example lets you create, resume, or list sessions from the command line. + +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.*; +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(); + + switch (choice) { + case 1 -> { + System.out.print("Enter session ID: "); + String sessionId = scanner.nextLine(); + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSessionId(sessionId) + .setModel("gpt-5") + ).get(); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nCopilot: " + msg.getData().content()) + ); + + System.out.println("Created session: " + sessionId); + chatLoop(session, scanner); + session.close(); + } + + case 2 -> { + System.out.print("Enter session ID to resume: "); + String resumeId = scanner.nextLine(); + try { + var session = client.resumeSession( + resumeId, + new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + ).get(); + + session.on(AssistantMessageEvent.class, msg -> + System.out.println("\nCopilot: " + msg.getData().content()) + ); + + System.out.println("Resumed session: " + resumeId); + chatLoop(session, scanner); + session.close(); + } catch (Exception ex) { + System.err.println("Failed to resume session: " + ex.getMessage()); + } + } + + case 3 -> { + var sessions = client.listSessions().get(); + System.out.println("\nAvailable sessions:"); + for (var s : sessions) { + System.out.println(" - " + s.getSessionId()); + } + } + + default -> System.out.println("Invalid choice"); + } + } + } + + static void chatLoop(Object session, Scanner scanner) throws Exception { + System.out.println("\nStart chatting (type 'exit' to quit):"); + while (true) { + System.out.print("\nYou: "); + String input = scanner.nextLine(); + if (input.equalsIgnoreCase("exit")) break; + + // Use reflection-free approach: cast to the session type + var s = (com.github.copilot.sdk.CopilotSession) session; + s.sendAndWait(new MessageOptions().setPrompt(input)).get(); + } + } +} +``` + +## Checking if a session exists + +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.json.*; + +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 β€” use `listSessions()` or catch the exception from `resumeSession()` +3. **Clean up old sessions**: Periodically delete sessions that are no longer needed with `deleteSession()` +4. **Error handling**: Always wrap resume operations in try-catch blocks β€” sessions may have been deleted or expired +5. **Workspace awareness**: Sessions are tied to workspace paths; ensure consistency when resuming across environments diff --git a/cookbook/copilot-sdk/java/pr-visualization.md b/cookbook/copilot-sdk/java/pr-visualization.md new file mode 100644 index 00000000..564a0c43 --- /dev/null +++ b/cookbook/copilot-sdk/java/pr-visualization.md @@ -0,0 +1,235 @@ +# 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. + +> **Runnable example:** [recipe/PRVisualization.java](recipe/PRVisualization.java) +> +> ```bash +> jbang recipe/PRVisualization.java +> ``` + +## 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 recipe/PRVisualization.java + +# Specify a repo explicitly +jbang recipe/PRVisualization.java github/copilot-sdk +``` + +## Full example: PRVisualization.java + +```java +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.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** | + +## Best practices + +1. **Start with auto-detection**: Let the tool detect the repository from the git remote before prompting the user +2. **Use system messages**: Provide context about the repo and working directory so Copilot can act autonomously +3. **Approve tool execution**: Use `PermissionHandler.APPROVE_ALL` to allow Copilot to run tools like the GitHub MCP Server without manual approval +4. **Interactive follow-ups**: Let users refine the analysis conversationally instead of requiring restarts +5. **Save artifacts locally**: Direct Copilot to save generated charts to the current directory for easy access diff --git a/cookbook/copilot-sdk/java/ralph-loop.md b/cookbook/copilot-sdk/java/ralph-loop.md new file mode 100644 index 00000000..84d77478 --- /dev/null +++ b/cookbook/copilot-sdk/java/ralph-loop.md @@ -0,0 +1,247 @@ +# Ralph Loop: Autonomous AI Task Loops + +Build autonomous coding loops where an AI agent picks tasks, implements them, validates against backpressure (tests, builds), commits, and repeats β€” each iteration in a fresh context window. + +> **Runnable example:** [recipe/RalphLoop.java](recipe/RalphLoop.java) +> +> ```bash +> jbang recipe/RalphLoop.java +> ``` + +## What is a Ralph Loop? + +A [Ralph loop](https://ghuntley.com/ralph/) is an autonomous development workflow where an AI agent iterates through tasks in isolated context windows. The key insight: **state lives on disk, not in the model's context**. Each iteration starts fresh, reads the current state from files, does one task, writes results back to disk, and exits. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ loop.sh β”‚ +β”‚ while true: β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Fresh session (isolated context) β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ 1. Read PROMPT.md + AGENTS.md β”‚ β”‚ +β”‚ β”‚ 2. Study specs/* and code β”‚ β”‚ +β”‚ β”‚ 3. Pick next task from plan β”‚ β”‚ +β”‚ β”‚ 4. Implement + run tests β”‚ β”‚ +β”‚ β”‚ 5. Update plan, commit, exit β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ ↻ next iteration (fresh context) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Core principles:** + +- **Fresh context per iteration**: Each loop creates a new session β€” no context accumulation, always in the "smart zone" +- **Disk as shared state**: `IMPLEMENTATION_PLAN.md` persists between iterations and acts as the coordination mechanism +- **Backpressure steers quality**: Tests, builds, and lints reject bad work β€” the agent must fix issues before committing +- **Two modes**: PLANNING (gap analysis β†’ generate plan) and BUILDING (implement from plan) + +## Simple Version + +The minimal Ralph loop β€” the SDK equivalent of `while :; do cat PROMPT.md | copilot ; done`: + +```java +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.nio.file.*; + +public class SimpleRalphLoop { + public static void main(String[] args) throws Exception { + String promptFile = args.length > 0 ? args[0] : "PROMPT.md"; + int maxIterations = args.length > 1 ? Integer.parseInt(args[1]) : 50; + + try (var client = new CopilotClient()) { + client.start().get(); + + String prompt = Files.readString(Path.of(promptFile)); + + for (int i = 1; i <= maxIterations; i++) { + System.out.printf("%n=== Iteration %d/%d ===%n", i, maxIterations); + + // Fresh session each iteration β€” context isolation is the point + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5.1-codex-mini") + .setWorkingDirectory(System.getProperty("user.dir")) + ).get(); + + try { + session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); + } finally { + session.close(); + } + + System.out.printf("Iteration %d complete.%n", i); + } + } + } +} +``` + +This is all you need to get started. The prompt file tells the agent what to do; the agent reads project files, does work, commits, and exits. The loop restarts with a clean slate. + +## Ideal Version + +The full Ralph pattern with planning and building modes, matching the [Ralph Playbook](https://github.com/ClaytonFarr/ralph-playbook) architecture: + +```java +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.nio.file.*; +import java.util.Arrays; + +public class RalphLoop { + public static void main(String[] args) throws Exception { + // Parse CLI args: jbang RalphLoop.java [plan] [max_iterations] + boolean planMode = Arrays.asList(args).contains("plan"); + String mode = planMode ? "plan" : "build"; + int maxIterations = Arrays.stream(args) + .filter(a -> a.matches("\\d+")) + .findFirst() + .map(Integer::parseInt) + .orElse(50); + + String promptFile = planMode ? "PROMPT_plan.md" : "PROMPT_build.md"; + System.out.printf("Mode: %s | Prompt: %s%n", mode, promptFile); + + try (var client = new CopilotClient()) { + client.start().get(); + + String prompt = Files.readString(Path.of(promptFile)); + + for (int i = 1; i <= maxIterations; i++) { + System.out.printf("%n=== Iteration %d/%d ===%n", i, maxIterations); + + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5.1-codex-mini") + .setWorkingDirectory(System.getProperty("user.dir")) + ).get(); + + // Log tool usage for visibility + session.on(ToolExecutionStartEvent.class, + ev -> System.out.printf(" βš™ %s%n", ev.getData().toolName())); + + try { + session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); + } finally { + session.close(); + } + + System.out.printf("Iteration %d complete.%n", i); + } + } + } +} +``` + +### Required Project Files + +The ideal version expects this file structure in your project: + +``` +project-root/ +β”œβ”€β”€ PROMPT_plan.md # Planning mode instructions +β”œβ”€β”€ PROMPT_build.md # Building mode instructions +β”œβ”€β”€ AGENTS.md # Operational guide (build/test commands) +β”œβ”€β”€ IMPLEMENTATION_PLAN.md # Task list (generated by planning mode) +β”œβ”€β”€ specs/ # Requirement specs (one per topic) +β”‚ β”œβ”€β”€ auth.md +β”‚ └── data-pipeline.md +└── src/ # Your source code +``` + +### Example `PROMPT_plan.md` + +```markdown +0a. Study `specs/*` to learn the application specifications. +0b. Study IMPLEMENTATION_PLAN.md (if present) to understand the plan so far. +0c. Study `src/` to understand existing code and shared utilities. + +1. Compare specs against code (gap analysis). Create or update + IMPLEMENTATION_PLAN.md as a prioritized bullet-point list of tasks + yet to be implemented. Do NOT implement anything. + +IMPORTANT: Do NOT assume functionality is missing β€” search the +codebase first to confirm. Prefer updating existing utilities over +creating ad-hoc copies. +``` + +### Example `PROMPT_build.md` + +```markdown +0a. Study `specs/*` to learn the application specifications. +0b. Study IMPLEMENTATION_PLAN.md. +0c. Study `src/` for reference. + +1. Choose the most important item from IMPLEMENTATION_PLAN.md. Before + making changes, search the codebase (don't assume not implemented). +2. After implementing, run the tests. If functionality is missing, add it. +3. When you discover issues, update IMPLEMENTATION_PLAN.md immediately. +4. When tests pass, update IMPLEMENTATION_PLAN.md, then `git add -A` + then `git commit` with a descriptive message. + +5. When authoring documentation, capture the why. +6. Implement completely. No placeholders or stubs. +7. Keep IMPLEMENTATION_PLAN.md current β€” future iterations depend on it. +``` + +### Example `AGENTS.md` + +Keep this brief (~60 lines). It's loaded every iteration, so bloat wastes context. + +```markdown +## Build & Run + +mvn compile + +## Validation + +- Tests: `mvn test` +- Typecheck: `mvn compile` +- Lint: `mvn checkstyle:check` +``` + +## Best Practices + +1. **Fresh context per iteration**: Never accumulate context across iterations β€” that's the whole point +2. **Disk is your database**: `IMPLEMENTATION_PLAN.md` is shared state between isolated sessions +3. **Backpressure is essential**: Tests, builds, lints in `AGENTS.md` β€” the agent must pass them before committing +4. **Start with PLANNING mode**: Generate the plan first, then switch to BUILDING +5. **Observe and tune**: Watch early iterations, add guardrails to prompts when the agent fails in specific ways +6. **The plan is disposable**: If the agent goes off track, delete `IMPLEMENTATION_PLAN.md` and re-plan +7. **Keep `AGENTS.md` brief**: It's loaded every iteration β€” operational info only, no progress notes +8. **Use a sandbox**: The agent runs autonomously with full tool access β€” isolate it +9. **Set `workingDirectory`**: Pin the session to your project root so tool operations resolve paths correctly +10. **Auto-approve permissions**: Use `PermissionHandler.APPROVE_ALL` to allow tool calls without interrupting the loop + +## When to Use a Ralph Loop + +**Good for:** + +- Implementing features from specs with test-driven validation +- Large refactors broken into many small tasks +- Unattended, long-running development with clear requirements +- Any work where backpressure (tests/builds) can verify correctness + +**Not good for:** + +- Tasks requiring human judgment mid-loop +- One-shot operations that don't benefit from iteration +- Vague requirements without testable acceptance criteria +- Exploratory prototyping where direction isn't clear + +## See Also + +- [Error Handling](error-handling.md) β€” timeout patterns and graceful shutdown for long-running sessions +- [Persisting Sessions](persisting-sessions.md) β€” save and resume sessions across restarts diff --git a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java new file mode 100644 index 00000000..917d684b --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java @@ -0,0 +1,123 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; + +/** + * Accessibility Report Generator β€” analyzes web pages using the Playwright MCP server + * and generates WCAG-compliant accessibility reports. + * + * Usage: + * jbang AccessibilityReport.java + */ +public class AccessibilityReport { + public static void main(String[] args) throws Exception { + System.out.println("=== Accessibility Report Generator ===\n"); + + var reader = new BufferedReader(new InputStreamReader(System.in)); + + System.out.print("Enter URL to analyze: "); + String url = reader.readLine().trim(); + if (url.isEmpty()) { + System.out.println("No URL provided. Exiting."); + return; + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + System.out.printf("%nAnalyzing: %s%n", url); + System.out.println("Please wait...\n"); + + try (var client = new CopilotClient()) { + client.start().get(); + + // Configure Playwright MCP server for browser automation + var mcpConfig = new McpServerConfig() + .setType("local") + .setCommand("npx") + .setArgs(List.of("@playwright/mcp@latest")) + .setTools(List.of("*")); + + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("claude-opus-4.6") + .setStreaming(true) + .setMcpServers(Map.of("playwright", mcpConfig)) + ).get(); + + // Stream output token-by-token + var idleLatch = new CountDownLatch(1); + + session.on(AssistantMessageDeltaEvent.class, + ev -> System.out.print(ev.getData().deltaContent())); + + session.on(SessionIdleEvent.class, + ev -> idleLatch.countDown()); + + session.on(SessionErrorEvent.class, ev -> { + System.err.printf("%nError: %s%n", ev.getData().message()); + idleLatch.countDown(); + }); + + String prompt = """ + Use the Playwright MCP server to analyze the accessibility of this webpage: %s + + Please: + 1. Navigate to the URL using playwright-browser_navigate + 2. Take an accessibility snapshot using playwright-browser_snapshot + 3. Analyze the snapshot and provide a detailed accessibility report + + Format the report with emoji indicators: + - πŸ“Š Accessibility Report header + - βœ… What's Working Well (table with Category, Status, Details) + - ⚠️ Issues Found (table with Severity, Issue, WCAG Criterion, Recommendation) + - πŸ“‹ Stats Summary (links, headings, focusable elements, landmarks) + - βš™οΈ Priority Recommendations + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis. + """.formatted(url); + + session.send(new MessageOptions().setPrompt(prompt)); + idleLatch.await(); + + System.out.println("\n\n=== Report Complete ===\n"); + + // Prompt user for test generation + System.out.print("Would you like to generate Playwright accessibility tests? (y/n): "); + String generateTests = reader.readLine().trim(); + + if (generateTests.equalsIgnoreCase("y") || generateTests.equalsIgnoreCase("yes")) { + var testLatch = new CountDownLatch(1); + + session.on(SessionIdleEvent.class, + ev -> testLatch.countDown()); + + String testPrompt = """ + Based on the accessibility report you just generated for %s, + create Playwright accessibility tests in Java. + + Include tests for: lang attribute, title, heading hierarchy, alt text, + landmarks, skip navigation, focus indicators, and touch targets. + Use Playwright's accessibility testing features with helpful comments. + Output the complete test file. + """.formatted(url); + + System.out.println("\nGenerating accessibility tests...\n"); + session.send(new MessageOptions().setPrompt(testPrompt)); + testLatch.await(); + + System.out.println("\n\n=== Tests Generated ==="); + } + + session.close(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/ErrorHandling.java b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java new file mode 100644 index 00000000..4765adc4 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java @@ -0,0 +1,34 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.util.concurrent.ExecutionException; + +public class ErrorHandling { + 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 (ExecutionException ex) { + System.err.println("Error: " + ex.getCause().getMessage()); + ex.getCause().printStackTrace(); + } catch (Exception ex) { + System.err.println("Error: " + ex.getMessage()); + ex.printStackTrace(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java new file mode 100644 index 00000000..0b022eee --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java @@ -0,0 +1,64 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +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.events.ToolExecutionCompleteEvent; +import com.github.copilot.sdk.events.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(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/MultipleSessions.java b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java new file mode 100644 index 00000000..bd3b27f1 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java @@ -0,0 +1,37 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.json.*; +import java.util.concurrent.CompletableFuture; + +public class MultipleSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + var config = new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); + + // Create 3 sessions in parallel + var f1 = client.createSession(config); + var f2 = client.createSession(config); + var f3 = client.createSession(new SessionConfig() + .setModel("claude-sonnet-4.5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); + CompletableFuture.allOf(f1, f2, f3).get(); + + var s1 = f1.get(); var s2 = f2.get(); var s3 = f3.get(); + + // Send a message to each session + System.out.println("S1: " + s1.sendAndWait(new MessageOptions().setPrompt("Explain Java records")).get().getMessage()); + System.out.println("S2: " + s2.sendAndWait(new MessageOptions().setPrompt("Explain sealed classes")).get().getMessage()); + System.out.println("S3: " + s3.sendAndWait(new MessageOptions().setPrompt("Explain pattern matching")).get().getMessage()); + + // Clean up + s1.destroy().get(); s2.destroy().get(); s3.destroy().get(); + client.stop().get(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/PRVisualization.java b/cookbook/copilot-sdk/java/recipe/PRVisualization.java new file mode 100644 index 00000000..4d46d0dc --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/PRVisualization.java @@ -0,0 +1,182 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.CopilotClient; +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.events.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(); + } +} diff --git a/cookbook/copilot-sdk/java/recipe/PersistingSessions.java b/cookbook/copilot-sdk/java/recipe/PersistingSessions.java new file mode 100644 index 00000000..89663908 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/PersistingSessions.java @@ -0,0 +1,34 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; + +public class PersistingSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Create a session with a custom ID so we can resume it later + 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(); + + System.out.println("\nSession ID: " + session.getSessionId()); + System.out.println("Session closed β€” data persisted to disk."); + + // Close session but keep data on disk for later resumption + session.close(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/README.md b/cookbook/copilot-sdk/java/recipe/README.md new file mode 100644 index 00000000..232d156d --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/README.md @@ -0,0 +1,71 @@ +# Runnable Recipe Examples + +This folder contains standalone, executable Java examples for each cookbook recipe. Each file can be run directly with [JBang](https://www.jbang.dev/) β€” no project setup required. + +## Prerequisites + +- Java 17 or later +- JBang installed: + +```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/). + +## Running Examples + +Each `.java` file is a complete, runnable program. Simply use: + +```bash +jbang .java +``` + +### Available Recipes + +| Recipe | Command | Description | +| -------------------- | ------------------------------------ | ------------------------------------------ | +| Error Handling | `jbang ErrorHandling.java` | Demonstrates error handling patterns | +| Multiple Sessions | `jbang MultipleSessions.java` | Manages multiple independent conversations | +| Managing Local Files | `jbang ManagingLocalFiles.java` | Organizes files using AI grouping | +| PR Visualization | `jbang PRVisualization.java` | Generates PR age charts | +| Persisting Sessions | `jbang PersistingSessions.java` | Save and resume sessions across restarts | +| Ralph Loop | `jbang RalphLoop.java` | Autonomous AI task loop | +| Accessibility Report | `jbang AccessibilityReport.java` | WCAG accessibility report generator | + +### Examples with Arguments + +**PR Visualization with specific repo:** + +```bash +jbang PRVisualization.java github/copilot-sdk +``` + +**Managing Local Files with specific folder:** + +```bash +jbang ManagingLocalFiles.java /path/to/your/folder +``` + +**Ralph Loop in planning mode:** + +```bash +jbang RalphLoop.java plan 5 +``` + +## Why JBang? + +JBang lets you run Java files as scripts β€” no `pom.xml`, no `build.gradle`, no project scaffolding. Dependencies are declared inline with `//DEPS` comments and resolved automatically. + +## Learning Resources + +- [JBang Documentation](https://www.jbang.dev/documentation/guide/latest/) +- [GitHub Copilot SDK for Java](https://github.com/github/copilot-sdk-java) +- [Parent Cookbook](../README.md) diff --git a/cookbook/copilot-sdk/java/recipe/RalphLoop.java b/cookbook/copilot-sdk/java/recipe/RalphLoop.java new file mode 100644 index 00000000..99e04655 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/RalphLoop.java @@ -0,0 +1,55 @@ +///usr/bin/env jbang "$0" "$@" ; exit $? +//DEPS com.github:copilot-sdk-java:0.2.1-java.1 + +import com.github.copilot.sdk.*; +import com.github.copilot.sdk.events.*; +import com.github.copilot.sdk.json.*; +import java.nio.file.*; + +/** + * Simple Ralph Loop β€” reads PROMPT.md and runs it in a fresh session each iteration. + * + * Usage: + * jbang RalphLoop.java # defaults: PROMPT.md, 50 iterations + * jbang RalphLoop.java PROMPT.md 20 # custom prompt file, 20 iterations + */ +public class RalphLoop { + public static void main(String[] args) throws Exception { + String promptFile = args.length > 0 ? args[0] : "PROMPT.md"; + int maxIterations = args.length > 1 ? Integer.parseInt(args[1]) : 50; + + System.out.printf("Ralph Loop β€” prompt: %s, max iterations: %d%n", promptFile, maxIterations); + + try (var client = new CopilotClient()) { + client.start().get(); + + String prompt = Files.readString(Path.of(promptFile)); + + for (int i = 1; i <= maxIterations; i++) { + System.out.printf("%n=== Iteration %d/%d ===%n", i, maxIterations); + + // Fresh session each iteration β€” context isolation is the point + var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5.1-codex-mini") + .setWorkingDirectory(System.getProperty("user.dir")) + ).get(); + + // Log tool usage for visibility + session.on(ToolExecutionStartEvent.class, + ev -> System.out.printf(" βš™ %s%n", ev.getData().toolName())); + + try { + session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); + } finally { + session.close(); + } + + System.out.printf("Iteration %d complete.%n", i); + } + } + + System.out.println("\nAll iterations complete."); + } +} From 57b27994085ae51ecf8fae4d082828899d77e7bf Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 14:09:17 -0400 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/README.md | 2 +- .../copilot-sdk/java/accessibility-report.md | 2 +- .../copilot-sdk/java/multiple-sessions.md | 56 ++++++++++--------- .../java/recipe/AccessibilityReport.java | 7 ++- .../java/recipe/ErrorHandling.java | 10 +++- .../java/recipe/MultipleSessions.java | 6 +- cookbook/copilot-sdk/java/recipe/README.md | 4 +- 7 files changed, 50 insertions(+), 37 deletions(-) diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index d9a127f9..c740200a 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -97,7 +97,7 @@ go run .go ### Java ```bash -cd java/cookbook/recipe +cd java/recipe jbang .java ``` diff --git a/cookbook/copilot-sdk/java/accessibility-report.md b/cookbook/copilot-sdk/java/accessibility-report.md index 2b6abbe6..785b3a42 100644 --- a/cookbook/copilot-sdk/java/accessibility-report.md +++ b/cookbook/copilot-sdk/java/accessibility-report.md @@ -27,7 +27,7 @@ npx --version ## Usage ```bash -jbang AccessibilityReport.java +jbang recipe/AccessibilityReport.java # Enter a URL when prompted ``` diff --git a/cookbook/copilot-sdk/java/multiple-sessions.md b/cookbook/copilot-sdk/java/multiple-sessions.md index 585ee5fd..3ac0d785 100644 --- a/cookbook/copilot-sdk/java/multiple-sessions.md +++ b/cookbook/copilot-sdk/java/multiple-sessions.md @@ -5,7 +5,7 @@ Manage multiple independent conversations simultaneously. > **Runnable example:** [recipe/MultipleSessions.java](recipe/MultipleSessions.java) > > ```bash -> jbang MultipleSessions.java +> jbang recipe/MultipleSessions.java > ``` ## Example scenario @@ -21,35 +21,39 @@ You need to run multiple conversations in parallel, each with its own context an import com.github.copilot.sdk.*; import com.github.copilot.sdk.json.*; -var client = new CopilotClient(); -client.start().get(); +public class MultipleSessions { + public static void main(String[] args) throws Exception { + var client = new CopilotClient(); + client.start().get(); -// Create multiple independent sessions -var session1 = client.createSession(new SessionConfig() - .setModel("gpt-5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); -var session2 = client.createSession(new SessionConfig() - .setModel("gpt-5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); -var session3 = client.createSession(new SessionConfig() - .setModel("claude-sonnet-4.5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + // Create multiple independent sessions + var session1 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + var session2 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + var session3 = client.createSession(new SessionConfig() + .setModel("claude-sonnet-4.5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); -// Each session maintains its own conversation history -session1.sendAndWait(new MessageOptions().setPrompt("You are helping with a Python project")).get(); -session2.sendAndWait(new MessageOptions().setPrompt("You are helping with a TypeScript project")).get(); -session3.sendAndWait(new MessageOptions().setPrompt("You are helping with a Go project")).get(); + // Each session maintains its own conversation history + session1.sendAndWait(new MessageOptions().setPrompt("You are helping with a Python project")).get(); + session2.sendAndWait(new MessageOptions().setPrompt("You are helping with a TypeScript project")).get(); + session3.sendAndWait(new MessageOptions().setPrompt("You are helping with a Go project")).get(); -// Follow-up messages stay in their respective contexts -session1.sendAndWait(new MessageOptions().setPrompt("How do I create a virtual environment?")).get(); -session2.sendAndWait(new MessageOptions().setPrompt("How do I set up tsconfig?")).get(); -session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); + // Follow-up messages stay in their respective contexts + session1.sendAndWait(new MessageOptions().setPrompt("How do I create a virtual environment?")).get(); + session2.sendAndWait(new MessageOptions().setPrompt("How do I set up tsconfig?")).get(); + session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); -// Clean up all sessions -session1.destroy().get(); -session2.destroy().get(); -session3.destroy().get(); -client.stop().get(); + // Clean up all sessions + session1.destroy().get(); + session2.destroy().get(); + session3.destroy().get(); + client.stop().get(); + } +} ``` ## Custom session IDs diff --git a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java index 917d684b..748e5e09 100644 --- a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java +++ b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java @@ -22,7 +22,12 @@ public class AccessibilityReport { var reader = new BufferedReader(new InputStreamReader(System.in)); System.out.print("Enter URL to analyze: "); - String url = reader.readLine().trim(); + String urlLine = reader.readLine(); + if (urlLine == null) { + System.out.println("No URL provided. Exiting."); + return; + } + String url = urlLine.trim(); if (url.isEmpty()) { System.out.println("No URL provided. Exiting."); return; diff --git a/cookbook/copilot-sdk/java/recipe/ErrorHandling.java b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java index 4765adc4..56813355 100644 --- a/cookbook/copilot-sdk/java/recipe/ErrorHandling.java +++ b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java @@ -24,8 +24,14 @@ public class ErrorHandling { session.close(); } catch (ExecutionException ex) { - System.err.println("Error: " + ex.getCause().getMessage()); - ex.getCause().printStackTrace(); + Throwable cause = ex.getCause(); + Throwable error = cause != null ? cause : ex; + System.err.println("Error: " + error.getMessage()); + error.printStackTrace(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + System.err.println("Interrupted: " + ex.getMessage()); + ex.printStackTrace(); } catch (Exception ex) { System.err.println("Error: " + ex.getMessage()); ex.printStackTrace(); diff --git a/cookbook/copilot-sdk/java/recipe/MultipleSessions.java b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java index bd3b27f1..2512bdaf 100644 --- a/cookbook/copilot-sdk/java/recipe/MultipleSessions.java +++ b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java @@ -25,9 +25,9 @@ public class MultipleSessions { var s1 = f1.get(); var s2 = f2.get(); var s3 = f3.get(); // Send a message to each session - System.out.println("S1: " + s1.sendAndWait(new MessageOptions().setPrompt("Explain Java records")).get().getMessage()); - System.out.println("S2: " + s2.sendAndWait(new MessageOptions().setPrompt("Explain sealed classes")).get().getMessage()); - System.out.println("S3: " + s3.sendAndWait(new MessageOptions().setPrompt("Explain pattern matching")).get().getMessage()); + System.out.println("S1: " + s1.sendAndWait(new MessageOptions().setPrompt("Explain Java records")).get().getData().content()); + System.out.println("S2: " + s2.sendAndWait(new MessageOptions().setPrompt("Explain sealed classes")).get().getData().content()); + System.out.println("S3: " + s3.sendAndWait(new MessageOptions().setPrompt("Explain pattern matching")).get().getData().content()); // Clean up s1.destroy().get(); s2.destroy().get(); s3.destroy().get(); diff --git a/cookbook/copilot-sdk/java/recipe/README.md b/cookbook/copilot-sdk/java/recipe/README.md index 232d156d..5726a67b 100644 --- a/cookbook/copilot-sdk/java/recipe/README.md +++ b/cookbook/copilot-sdk/java/recipe/README.md @@ -54,10 +54,8 @@ jbang PRVisualization.java github/copilot-sdk jbang ManagingLocalFiles.java /path/to/your/folder ``` -**Ralph Loop in planning mode:** +**Ralph Loop with a prompt file:** -```bash -jbang RalphLoop.java plan 5 ``` ## Why JBang? From 0f06f346be6e8321a52b3373a8c78f0aa43db9e1 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 14:12:08 -0400 Subject: [PATCH 3/8] Address review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Thread.sleep with sendAndWait in PRVisualization - Fix top-level statements in multiple-sessions.md (wrap in class) - Fix .getMessage() β†’ .getData().content() in MultipleSessions.java - Guard against null readLine() in AccessibilityReport.java - Add null-safe getCause() + InterruptedException handling in ErrorHandling.java - Fix paths: jbang commands now include recipe/ prefix - Fix root README path: cd java/recipe (not java/cookbook/recipe) - Fix recipe/README.md ralph-loop CLI example --- .../copilot-sdk/java/multiple-sessions.md | 50 +++++++++---------- cookbook/copilot-sdk/java/pr-visualization.md | 8 +-- .../java/recipe/PRVisualization.java | 8 +-- cookbook/copilot-sdk/java/recipe/README.md | 4 +- 4 files changed, 32 insertions(+), 38 deletions(-) diff --git a/cookbook/copilot-sdk/java/multiple-sessions.md b/cookbook/copilot-sdk/java/multiple-sessions.md index 3ac0d785..b44a4d76 100644 --- a/cookbook/copilot-sdk/java/multiple-sessions.md +++ b/cookbook/copilot-sdk/java/multiple-sessions.md @@ -23,35 +23,35 @@ import com.github.copilot.sdk.json.*; public class MultipleSessions { public static void main(String[] args) throws Exception { - var client = new CopilotClient(); - client.start().get(); + try (var client = new CopilotClient()) { + client.start().get(); - // Create multiple independent sessions - var session1 = client.createSession(new SessionConfig() - .setModel("gpt-5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - var session2 = client.createSession(new SessionConfig() - .setModel("gpt-5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - var session3 = client.createSession(new SessionConfig() - .setModel("claude-sonnet-4.5") - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + // Create multiple independent sessions + var session1 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + var session2 = client.createSession(new SessionConfig() + .setModel("gpt-5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + var session3 = client.createSession(new SessionConfig() + .setModel("claude-sonnet-4.5") + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - // Each session maintains its own conversation history - session1.sendAndWait(new MessageOptions().setPrompt("You are helping with a Python project")).get(); - session2.sendAndWait(new MessageOptions().setPrompt("You are helping with a TypeScript project")).get(); - session3.sendAndWait(new MessageOptions().setPrompt("You are helping with a Go project")).get(); + // Each session maintains its own conversation history + session1.sendAndWait(new MessageOptions().setPrompt("You are helping with a Python project")).get(); + session2.sendAndWait(new MessageOptions().setPrompt("You are helping with a TypeScript project")).get(); + session3.sendAndWait(new MessageOptions().setPrompt("You are helping with a Go project")).get(); - // Follow-up messages stay in their respective contexts - session1.sendAndWait(new MessageOptions().setPrompt("How do I create a virtual environment?")).get(); - session2.sendAndWait(new MessageOptions().setPrompt("How do I set up tsconfig?")).get(); - session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); + // Follow-up messages stay in their respective contexts + session1.sendAndWait(new MessageOptions().setPrompt("How do I create a virtual environment?")).get(); + session2.sendAndWait(new MessageOptions().setPrompt("How do I set up tsconfig?")).get(); + session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); - // Clean up all sessions - session1.destroy().get(); - session2.destroy().get(); - session3.destroy().get(); - client.stop().get(); + // Clean up all sessions + session1.destroy().get(); + session2.destroy().get(); + session3.destroy().get(); + } } } ``` diff --git a/cookbook/copilot-sdk/java/pr-visualization.md b/cookbook/copilot-sdk/java/pr-visualization.md index 564a0c43..87f9976c 100644 --- a/cookbook/copilot-sdk/java/pr-visualization.md +++ b/cookbook/copilot-sdk/java/pr-visualization.md @@ -118,10 +118,7 @@ public class PRVisualization { 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); + session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); // Interactive loop System.out.println("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n"); @@ -145,8 +142,7 @@ public class PRVisualization { break; } - session.send(new MessageOptions().setPrompt(input)); - Thread.sleep(2000); // Give time for response + session.sendAndWait(new MessageOptions().setPrompt(input)).get(); } } diff --git a/cookbook/copilot-sdk/java/recipe/PRVisualization.java b/cookbook/copilot-sdk/java/recipe/PRVisualization.java index 4d46d0dc..0ac79527 100644 --- a/cookbook/copilot-sdk/java/recipe/PRVisualization.java +++ b/cookbook/copilot-sdk/java/recipe/PRVisualization.java @@ -93,10 +93,7 @@ public class PRVisualization { 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); + session.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); // Interactive loop System.out.println("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n"); @@ -120,8 +117,7 @@ public class PRVisualization { break; } - session.send(new MessageOptions().setPrompt(input)); - Thread.sleep(2000); // Give time for response + session.sendAndWait(new MessageOptions().setPrompt(input)).get(); } } diff --git a/cookbook/copilot-sdk/java/recipe/README.md b/cookbook/copilot-sdk/java/recipe/README.md index 5726a67b..38201fb7 100644 --- a/cookbook/copilot-sdk/java/recipe/README.md +++ b/cookbook/copilot-sdk/java/recipe/README.md @@ -54,8 +54,10 @@ jbang PRVisualization.java github/copilot-sdk jbang ManagingLocalFiles.java /path/to/your/folder ``` -**Ralph Loop with a prompt file:** +**Ralph Loop with a custom prompt file:** +```bash +jbang RalphLoop.java PROMPT_build.md 20 ``` ## Why JBang? From e03ccb978a23b300713e60b872e9895ffdde0036 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 15:12:06 -0400 Subject: [PATCH 4/8] fix: Java cookbook recipes to compile with copilot-sdk-java 0.2.1-java.1 Fix compilation errors and documentation inaccuracies in Java cookbook recipes against the actual SDK API: - MultipleSessions: Replace non-existent destroy() with close() - AccessibilityReport: Replace non-existent McpServerConfig class with Map (the actual type accepted by setMcpServers) - error-handling.md: Replace non-existent session.addTool(), ToolDefinition.builder(), and ToolResultObject with actual SDK APIs (ToolDefinition.create(), SessionConfig.setTools(), CompletableFuture return type) All 7 recipes now compile successfully with jbang build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../copilot-sdk/java/accessibility-report.md | 22 +++++----- cookbook/copilot-sdk/java/error-handling.md | 43 +++++++++++++------ .../copilot-sdk/java/multiple-sessions.md | 8 ++-- .../java/recipe/AccessibilityReport.java | 11 ++--- .../java/recipe/MultipleSessions.java | 3 +- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/cookbook/copilot-sdk/java/accessibility-report.md b/cookbook/copilot-sdk/java/accessibility-report.md index 785b3a42..83a1853c 100644 --- a/cookbook/copilot-sdk/java/accessibility-report.md +++ b/cookbook/copilot-sdk/java/accessibility-report.md @@ -67,11 +67,12 @@ public class AccessibilityReport { client.start().get(); // Configure Playwright MCP server for browser automation - var mcpConfig = new McpServerConfig() - .setType("local") - .setCommand("npx") - .setArgs(List.of("@playwright/mcp@latest")) - .setTools(List.of("*")); + Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", List.of("*") + ); var session = client.createSession( new SessionConfig() @@ -167,11 +168,12 @@ public class AccessibilityReport { The recipe configures a local MCP server that runs alongside the session: ```java -var mcpConfig = new McpServerConfig() - .setType("local") - .setCommand("npx") - .setArgs(List.of("@playwright/mcp@latest")) - .setTools(List.of("*")); +Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", List.of("*") +); var session = client.createSession( new SessionConfig() diff --git a/cookbook/copilot-sdk/java/error-handling.md b/cookbook/copilot-sdk/java/error-handling.md index 8c2dd026..a21c4f75 100644 --- a/cookbook/copilot-sdk/java/error-handling.md +++ b/cookbook/copilot-sdk/java/error-handling.md @@ -150,27 +150,42 @@ try (var client = new CopilotClient()) { ## Handling tool errors -When defining tools, return an error result to signal a failure back to the model instead of throwing. +When defining tools, return an error string to signal a failure back to the model instead of throwing. ```java -import com.github.copilot.sdk.json.ToolResultObject; +import com.github.copilot.sdk.json.ToolDefinition; +import java.util.concurrent.CompletableFuture; -session.addTool( - ToolDefinition.builder() - .name("read_file") - .description("Read a file from disk") - .parameter("path", "string", "File path", true) - .build(), - (args) -> { +var readFileTool = ToolDefinition.create( + "read_file", + "Read a file from disk", + Map.of( + "type", "object", + "properties", Map.of( + "path", Map.of("type", "string", "description", "File path") + ), + "required", List.of("path") + ), + invocation -> { try { + var path = (String) invocation.getArguments().get("path"); var content = java.nio.file.Files.readString( - java.nio.file.Path.of(args.get("path").toString())); - return ToolResultObject.success(content); - } catch (IOException ex) { - return ToolResultObject.error("Failed to read file: " + ex.getMessage()); + java.nio.file.Path.of(path)); + return CompletableFuture.completedFuture(content); + } catch (java.io.IOException ex) { + return CompletableFuture.completedFuture( + "Error: Failed to read file: " + ex.getMessage()); } } ); + +// Register tools when creating the session +var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5") + .setTools(List.of(readFileTool)) +).get(); ``` ## Best practices @@ -179,5 +194,5 @@ session.addTool( 2. **Unwrap `ExecutionException`**: Call `getCause()` to inspect the real error β€” the outer `ExecutionException` is just a `CompletableFuture` wrapper. 3. **Restore interrupt flag**: When catching `InterruptedException`, call `Thread.currentThread().interrupt()` to preserve the interrupted status. 4. **Set timeouts**: Use `get(timeout, TimeUnit)` instead of bare `get()` for any call that could block indefinitely. -5. **Return tool errors, don't throw**: Use `ToolResultObject.error()` so the model can recover gracefully. +5. **Return tool errors, don't throw**: Return an error string from the `CompletableFuture` so the model can recover gracefully. 6. **Log errors**: Capture error details for debugging β€” consider a logging framework like SLF4J for production applications. diff --git a/cookbook/copilot-sdk/java/multiple-sessions.md b/cookbook/copilot-sdk/java/multiple-sessions.md index b44a4d76..4508e565 100644 --- a/cookbook/copilot-sdk/java/multiple-sessions.md +++ b/cookbook/copilot-sdk/java/multiple-sessions.md @@ -48,9 +48,9 @@ public class MultipleSessions { session3.sendAndWait(new MessageOptions().setPrompt("How do I initialize a module?")).get(); // Clean up all sessions - session1.destroy().get(); - session2.destroy().get(); - session3.destroy().get(); + session1.close(); + session2.close(); + session3.close(); } } } @@ -136,7 +136,7 @@ var session = client.createSession(new SessionConfig() session.sendAndWait(new MessageOptions().setPrompt("Hello!")).get(); -session.destroy().get(); +session.close(); client.stop().get(); executor.shutdown(); ``` diff --git a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java index 748e5e09..b77f6419 100644 --- a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java +++ b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java @@ -43,11 +43,12 @@ public class AccessibilityReport { client.start().get(); // Configure Playwright MCP server for browser automation - var mcpConfig = new McpServerConfig() - .setType("local") - .setCommand("npx") - .setArgs(List.of("@playwright/mcp@latest")) - .setTools(List.of("*")); + Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", List.of("*") + ); var session = client.createSession( new SessionConfig() diff --git a/cookbook/copilot-sdk/java/recipe/MultipleSessions.java b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java index 2512bdaf..615dab80 100644 --- a/cookbook/copilot-sdk/java/recipe/MultipleSessions.java +++ b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java @@ -30,8 +30,7 @@ public class MultipleSessions { System.out.println("S3: " + s3.sendAndWait(new MessageOptions().setPrompt("Explain pattern matching")).get().getData().content()); // Clean up - s1.destroy().get(); s2.destroy().get(); s3.destroy().get(); - client.stop().get(); + s1.close(); s2.close(); s3.close(); } } } From 8ba7845b8e034610f83a7358d83a53bd0500410b Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 16:30:25 -0400 Subject: [PATCH 5/8] Update cookbook/copilot-sdk/java/recipe/AccessibilityReport.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/java/recipe/AccessibilityReport.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java index b77f6419..34af05d5 100644 --- a/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java +++ b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java @@ -98,7 +98,8 @@ public class AccessibilityReport { // Prompt user for test generation System.out.print("Would you like to generate Playwright accessibility tests? (y/n): "); - String generateTests = reader.readLine().trim(); + String generateTestsLine = reader.readLine(); + String generateTests = generateTestsLine == null ? "" : generateTestsLine.trim(); if (generateTests.equalsIgnoreCase("y") || generateTests.equalsIgnoreCase("yes")) { var testLatch = new CountDownLatch(1); From 6f47a3d1d21165f69084e875ecee1c81e93cdb02 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 16:30:41 -0400 Subject: [PATCH 6/8] Update cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java index 0b022eee..8a60f1f4 100644 --- a/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java +++ b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java @@ -9,7 +9,6 @@ import com.github.copilot.sdk.events.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 { From 8f570c0d897a93664c4d9dff9623ce66c7c7e08f Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 16:31:10 -0400 Subject: [PATCH 7/8] Update cookbook/copilot-sdk/java/recipe/ErrorHandling.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../copilot-sdk/java/recipe/ErrorHandling.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cookbook/copilot-sdk/java/recipe/ErrorHandling.java b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java index 56813355..ab4e7fbf 100644 --- a/cookbook/copilot-sdk/java/recipe/ErrorHandling.java +++ b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java @@ -11,18 +11,17 @@ public class ErrorHandling { try (var client = new CopilotClient()) { client.start().get(); - var session = client.createSession( + try (var session = client.createSession( new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setModel("gpt-5")).get(); + .setModel("gpt-5")).get()) { - session.on(AssistantMessageEvent.class, - msg -> System.out.println(msg.getData().content())); + session.on(AssistantMessageEvent.class, + msg -> System.out.println(msg.getData().content())); - session.sendAndWait( - new MessageOptions().setPrompt("Hello!")).get(); - - session.close(); + session.sendAndWait( + new MessageOptions().setPrompt("Hello!")).get(); + } } catch (ExecutionException ex) { Throwable cause = ex.getCause(); Throwable error = cause != null ? cause : ex; From e447f7d7c10ed9cba90bb0eb348984103f279a69 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Mon, 6 Apr 2026 16:31:24 -0400 Subject: [PATCH 8/8] Update cookbook/copilot-sdk/java/recipe/PersistingSessions.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cookbook/copilot-sdk/java/recipe/PersistingSessions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/copilot-sdk/java/recipe/PersistingSessions.java b/cookbook/copilot-sdk/java/recipe/PersistingSessions.java index 89663908..c2980e5e 100644 --- a/cookbook/copilot-sdk/java/recipe/PersistingSessions.java +++ b/cookbook/copilot-sdk/java/recipe/PersistingSessions.java @@ -25,10 +25,10 @@ public class PersistingSessions { .setPrompt("Let's discuss TypeScript generics")).get(); System.out.println("\nSession ID: " + session.getSessionId()); - System.out.println("Session closed β€” data persisted to disk."); // Close session but keep data on disk for later resumption session.close(); + System.out.println("Session closed β€” data persisted to disk."); } } }