diff --git a/cookbook/README.md b/cookbook/README.md index 797ce76dd..0ca90d3a3 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 3e2738d18..c740200a1 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/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 000000000..fb335a198 --- /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 000000000..83a1853c5 --- /dev/null +++ b/cookbook/copilot-sdk/java/accessibility-report.md @@ -0,0 +1,240 @@ +# 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 recipe/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 + Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", 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 +Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", 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 000000000..a21c4f755 --- /dev/null +++ b/cookbook/copilot-sdk/java/error-handling.md @@ -0,0 +1,198 @@ +# 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 string to signal a failure back to the model instead of throwing. + +```java +import com.github.copilot.sdk.json.ToolDefinition; +import java.util.concurrent.CompletableFuture; + +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(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 + +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**: 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/managing-local-files.md b/cookbook/copilot-sdk/java/managing-local-files.md new file mode 100644 index 000000000..d347c7fea --- /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 000000000..4508e565b --- /dev/null +++ b/cookbook/copilot-sdk/java/multiple-sessions.md @@ -0,0 +1,148 @@ +# Working with Multiple Sessions + +Manage multiple independent conversations simultaneously. + +> **Runnable example:** [recipe/MultipleSessions.java](recipe/MultipleSessions.java) +> +> ```bash +> jbang recipe/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.*; + +public class MultipleSessions { + public static void main(String[] args) throws Exception { + try (var client = new CopilotClient()) { + client.start().get(); + + // Create multiple independent sessions + var session1 = client.createSession(new SessionConfig() + .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.close(); + session2.close(); + session3.close(); + } + } +} +``` + +## 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.close(); +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 000000000..5de9c0e2a --- /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 000000000..87f9976c2 --- /dev/null +++ b/cookbook/copilot-sdk/java/pr-visualization.md @@ -0,0 +1,231 @@ +# 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.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); + + // 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.sendAndWait(new MessageOptions().setPrompt(input)).get(); + } + } + + 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 000000000..84d77478b --- /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 000000000..34af05d50 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/AccessibilityReport.java @@ -0,0 +1,130 @@ +///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 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; + } + 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 + Map mcpConfig = Map.of( + "type", "local", + "command", "npx", + "args", List.of("@playwright/mcp@latest"), + "tools", 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 generateTestsLine = reader.readLine(); + String generateTests = generateTestsLine == null ? "" : generateTestsLine.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 000000000..ab4e7fbf0 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/ErrorHandling.java @@ -0,0 +1,39 @@ +///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(); + + try (var session = client.createSession( + new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("gpt-5")).get()) { + + session.on(AssistantMessageEvent.class, + msg -> System.out.println(msg.getData().content())); + + session.sendAndWait( + new MessageOptions().setPrompt("Hello!")).get(); + } + } catch (ExecutionException ex) { + 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/ManagingLocalFiles.java b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java new file mode 100644 index 000000000..8a60f1f44 --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/ManagingLocalFiles.java @@ -0,0 +1,63 @@ +///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.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 000000000..615dab80b --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/MultipleSessions.java @@ -0,0 +1,36 @@ +///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().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.close(); s2.close(); s3.close(); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/PRVisualization.java b/cookbook/copilot-sdk/java/recipe/PRVisualization.java new file mode 100644 index 000000000..0ac79527d --- /dev/null +++ b/cookbook/copilot-sdk/java/recipe/PRVisualization.java @@ -0,0 +1,178 @@ +///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.sendAndWait(new MessageOptions().setPrompt(prompt)).get(); + + // 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.sendAndWait(new MessageOptions().setPrompt(input)).get(); + } + } + + 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 000000000..c2980e5e2 --- /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()); + + // Close session but keep data on disk for later resumption + session.close(); + System.out.println("Session closed β€” data persisted to disk."); + } + } +} diff --git a/cookbook/copilot-sdk/java/recipe/README.md b/cookbook/copilot-sdk/java/recipe/README.md new file mode 100644 index 000000000..38201fb7f --- /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 with a custom prompt file:** + +```bash +jbang RalphLoop.java PROMPT_build.md 20 +``` + +## 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 000000000..99e046557 --- /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."); + } +}