diff --git a/agents/reepl-linkedin.agent.md b/agents/reepl-linkedin.agent.md new file mode 100644 index 00000000..df414840 --- /dev/null +++ b/agents/reepl-linkedin.agent.md @@ -0,0 +1,42 @@ +--- +name: reepl-linkedin +description: "AI-powered LinkedIn content creation, scheduling, and analytics agent. Create posts, carousels, and manage your LinkedIn presence with GitHub Copilot." +--- + +# Reepl -- LinkedIn Content Agent + +You are a LinkedIn content strategist and automation expert powered by [Reepl](https://reepl.io). You help developers, marketers, and professionals create, schedule, and analyze LinkedIn content directly from their editor. + +**What is Reepl?** Reepl is an AI-powered LinkedIn content management platform that lets you create posts, design carousels, schedule content, and track analytics. Learn more at [reepl.io](https://reepl.io) or explore the skills repository at [github.com/reepl-io/skills](https://github.com/reepl-io/skills). + +## Core Capabilities + +- **Post Creation:** Draft engaging LinkedIn posts with AI assistance, including text formatting, hashtag suggestions, and hook optimization. +- **Carousel Design:** Generate multi-slide LinkedIn carousels with structured content and visual layouts. +- **Content Scheduling:** Plan and schedule posts for optimal engagement times. +- **Analytics:** Review post performance, engagement metrics, and audience insights. +- **Voice Profiles:** Match content tone and style to a user's personal brand or voice profile. + +## Workflow + +1. **Understand the Goal:** Ask what the user wants to achieve -- thought leadership, product launch, hiring, community engagement, etc. +2. **Draft Content:** Create LinkedIn-optimized content following best practices (hooks, formatting, CTAs). +3. **Refine:** Iterate on tone, length, and structure based on feedback. +4. **Schedule or Publish:** Help the user schedule or publish the content through Reepl. + +## LinkedIn Content Best Practices + +- Start with a strong hook in the first two lines to earn the "see more" click. +- Use short paragraphs and line breaks for readability on mobile. +- Include a clear call-to-action (comment, share, visit link). +- Keep hashtags relevant and limited to 3-5 per post. +- Carousels should tell a story with a clear beginning, middle, and end. +- Optimal post length is 1,200-1,500 characters for engagement. + +## Guidelines + +- Always tailor content to the user's industry and audience. +- Maintain a professional but authentic tone unless the user specifies otherwise. +- Respect LinkedIn's content policies and community guidelines. +- Never generate misleading, spammy, or engagement-bait content. +- Prioritize value-driven content that educates, inspires, or informs. diff --git a/cookbook/cookbook.yml b/cookbook/cookbook.yml index d80454b5..43f66f3f 100644 --- a/cookbook/cookbook.yml +++ b/cookbook/cookbook.yml @@ -61,3 +61,11 @@ cookbooks: - sessions - persistence - state-management + - id: accessibility-report + name: Accessibility Report + description: Generate WCAG accessibility reports using the Playwright MCP server + tags: + - accessibility + - playwright + - mcp + - wcag diff --git a/cookbook/copilot-sdk/README.md b/cookbook/copilot-sdk/README.md index 53c91a70..3e2738d1 100644 --- a/cookbook/copilot-sdk/README.md +++ b/cookbook/copilot-sdk/README.md @@ -6,35 +6,43 @@ This cookbook collects small, focused recipes showing how to accomplish common t ### .NET (C#) +- [Ralph Loop](dotnet/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. - [Error Handling](dotnet/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. - [Multiple Sessions](dotnet/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Managing Local Files](dotnet/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. - [PR Visualization](dotnet/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. - [Persisting Sessions](dotnet/persisting-sessions.md): Save and resume sessions across restarts. +- [Accessibility Report](dotnet/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. ### Node.js / TypeScript +- [Ralph Loop](nodejs/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. - [Error Handling](nodejs/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. - [Multiple Sessions](nodejs/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Managing Local Files](nodejs/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. - [PR Visualization](nodejs/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. - [Persisting Sessions](nodejs/persisting-sessions.md): Save and resume sessions across restarts. +- [Accessibility Report](nodejs/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. ### Python +- [Ralph Loop](python/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. - [Error Handling](python/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. - [Multiple Sessions](python/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Managing Local Files](python/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. - [PR Visualization](python/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. - [Persisting Sessions](python/persisting-sessions.md): Save and resume sessions across restarts. +- [Accessibility Report](python/accessibility-report.md): Generate WCAG accessibility reports using the Playwright MCP server. ### Go +- [Ralph Loop](go/ralph-loop.md): Build autonomous AI coding loops with fresh context per iteration, planning/building modes, and backpressure. - [Error Handling](go/error-handling.md): Handle errors gracefully including connection failures, timeouts, and cleanup. - [Multiple Sessions](go/multiple-sessions.md): Manage multiple independent conversations simultaneously. - [Managing Local Files](go/managing-local-files.md): Organize files by metadata using AI-powered grouping strategies. - [PR Visualization](go/pr-visualization.md): Generate interactive PR age charts using GitHub MCP Server. - [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. ## How to Use @@ -83,4 +91,4 @@ go run .go ## Status -Cookbook structure is complete with 4 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. +Cookbook structure is complete with 7 recipes across all 4 supported languages. Each recipe includes both markdown documentation and runnable examples. diff --git a/cookbook/copilot-sdk/dotnet/accessibility-report.md b/cookbook/copilot-sdk/dotnet/accessibility-report.md new file mode 100644 index 00000000..6c2f1d8d --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/accessibility-report.md @@ -0,0 +1,287 @@ +# 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/accessibility-report.cs](recipe/accessibility-report.cs) +> +> ```bash +> dotnet run recipe/accessibility-report.cs +> ``` + +## 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 + +```bash +dotnet add package GitHub.Copilot.SDK +``` + +You also need `npx` available (Node.js installed) for the Playwright MCP server. + +## Usage + +```bash +dotnet run recipe/accessibility-report.cs +# Enter a URL when prompted +``` + +## Full example: accessibility-report.cs + +```csharp +#:package GitHub.Copilot.SDK@* + +using GitHub.Copilot.SDK; + +// Create and start client +await using var client = new CopilotClient(); +await client.StartAsync(); + +Console.WriteLine("=== Accessibility Report Generator ==="); +Console.WriteLine(); + +Console.Write("Enter URL to analyze: "); +var url = Console.ReadLine()?.Trim(); + +if (string.IsNullOrWhiteSpace(url)) +{ + Console.WriteLine("No URL provided. Exiting."); + return; +} + +// Ensure URL has a scheme +if (!url.StartsWith("http://") && !url.StartsWith("https://")) +{ + url = "https://" + url; +} + +Console.WriteLine($"\nAnalyzing: {url}"); +Console.WriteLine("Please wait...\n"); + +// Create a session with Playwright MCP server +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "claude-opus-4.6", + Streaming = true, + McpServers = new Dictionary() + { + ["playwright"] = + new McpLocalServerConfig + { + Type = "local", + Command = "npx", + Args = ["@playwright/mcp@latest"], + Tools = ["*"] + } + }, +}); + +// Wait for response using session.idle event +var done = new TaskCompletionSource(); + +session.On(evt => +{ + switch (evt) + { + case AssistantMessageDeltaEvent delta: + Console.Write(delta.Data.DeltaContent); + break; + case SessionIdleEvent: + done.TrySetResult(); + break; + case SessionErrorEvent error: + Console.WriteLine($"\nError: {error.Data.Message}"); + done.TrySetResult(); + break; + } +}); + +var prompt = $""" + Use the Playwright MCP server to analyze the accessibility of this webpage: {url} + + 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 EXACTLY like this structure with emoji indicators: + + πŸ“Š Accessibility Report: [Page Title] (domain.com) + + βœ… What's Working Well + | Category | Status | Details | + |----------|--------|---------| + | Language | βœ… Pass | lang="en-US" properly set | + | Page Title | βœ… Pass | "[Title]" is descriptive | + | Heading Hierarchy | βœ… Pass | Single H1, proper H2/H3 structure | + | Images | βœ… Pass | All X images have alt text | + + ⚠️ Issues Found + | Severity | Issue | WCAG Criterion | Recommendation | + |----------|-------|----------------|----------------| + | πŸ”΄ High | No
landmark | 1.3.1, 2.4.1 | Wrap main content in
element | + | 🟑 Medium | Focus outlines disabled | 2.4.7 | Ensure visible :focus styles exist | + + πŸ“‹ Stats Summary + - Total Links: X + - Total Headings: X + - Focusable Elements: X + - Landmarks Found: banner βœ…, navigation βœ…, main ❌, footer βœ… + + βš™οΈ Priority Recommendations + ... + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis - don't just copy the example. + """; + +await session.SendAsync(new MessageOptions { Prompt = prompt }); +await done.Task; + +Console.WriteLine("\n\n=== Report Complete ===\n"); + +// Prompt user for test generation +Console.Write("Would you like to generate Playwright accessibility tests? (y/n): "); +var generateTests = Console.ReadLine()?.Trim().ToLowerInvariant(); + +if (generateTests == "y" || generateTests == "yes") +{ + // Reset for next interaction + done = new TaskCompletionSource(); + + var detectLanguagePrompt = $""" + Analyze the current working directory to detect the primary programming language used in this project. + Respond with ONLY the detected language name and a brief explanation. + If no project is detected, suggest "TypeScript" as the default for Playwright tests. + """; + + Console.WriteLine("\nDetecting project language...\n"); + await session.SendAsync(new MessageOptions { Prompt = detectLanguagePrompt }); + await done.Task; + + Console.Write("\n\nConfirm language for tests (or enter a different one): "); + var language = Console.ReadLine()?.Trim(); + + if (string.IsNullOrWhiteSpace(language)) + { + language = "TypeScript"; + } + + // Reset for test generation + done = new TaskCompletionSource(); + + var testGenerationPrompt = $""" + Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}. + + The tests should: + 1. Verify all the accessibility checks from the report + 2. Test for the issues that were found (to ensure they get fixed) + 3. Include tests for landmarks, heading hierarchy, alt text, focus indicators, and more + 4. Use Playwright's accessibility testing features + 5. Include helpful comments explaining each test + + Output the complete test file that can be saved and run. + """; + + Console.WriteLine("\nGenerating accessibility tests...\n"); + await session.SendAsync(new MessageOptions { Prompt = testGenerationPrompt }); + await done.Task; + + Console.WriteLine("\n\n=== Tests Generated ==="); +} +``` + +## 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 detects the project language and generates Playwright accessibility tests + +## Key concepts + +### MCP server configuration + +The recipe configures a local MCP server that runs alongside the session: + +```csharp +McpServers = new Dictionary() +{ + ["playwright"] = new McpLocalServerConfig + { + Type = "local", + Command = "npx", + Args = ["@playwright/mcp@latest"], + Tools = ["*"] + } +} +``` + +This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`. + +### Streaming with events + +Unlike `SendAndWaitAsync`, this recipe uses streaming for real-time output: + +```csharp +session.On(evt => +{ + switch (evt) + { + case AssistantMessageDeltaEvent delta: + Console.Write(delta.Data.DeltaContent); // Token-by-token + break; + case SessionIdleEvent: + done.TrySetResult(); // Model finished + break; + } +}); +``` + +## 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 + +Detecting project language... +TypeScript detected (package.json found) + +Confirm language for tests (or enter a different one): + +Generating accessibility tests... +[Generated test file output...] + +=== Tests Generated === +``` diff --git a/cookbook/copilot-sdk/dotnet/ralph-loop.md b/cookbook/copilot-sdk/dotnet/ralph-loop.md new file mode 100644 index 00000000..8ff85246 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/ralph-loop.md @@ -0,0 +1,260 @@ +# 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/ralph-loop.cs](recipe/ralph-loop.cs) +> +> ```bash +> cd dotnet +> dotnet run recipe/ralph-loop.cs +> ``` + +## 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`: + +```csharp +using GitHub.Copilot.SDK; + +var client = new CopilotClient(); +await client.StartAsync(); + +try +{ + var prompt = await File.ReadAllTextAsync("PROMPT.md"); + var maxIterations = 50; + + for (var i = 1; i <= maxIterations; i++) + { + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); + + // Fresh session each iteration β€” context isolation is the point + var session = await client.CreateSessionAsync( + new SessionConfig { Model = "gpt-5.1-codex-mini" }); + try + { + var done = new TaskCompletionSource(); + session.On(evt => + { + if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); + }); + + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; + } + finally + { + await session.DisposeAsync(); + } + + Console.WriteLine($"Iteration {i} complete."); + } +} +finally +{ + await client.StopAsync(); +} +``` + +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: + +```csharp +using GitHub.Copilot.SDK; + +// Parse args: dotnet run [plan] [max_iterations] +var mode = args.Contains("plan") ? "plan" : "build"; +var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _)); +var maxIterations = maxArg != null ? int.Parse(maxArg) : 50; +var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; + +var client = new CopilotClient(); +await client.StartAsync(); + +Console.WriteLine(new string('━', 40)); +Console.WriteLine($"Mode: {mode}"); +Console.WriteLine($"Prompt: {promptFile}"); +Console.WriteLine($"Max: {maxIterations} iterations"); +Console.WriteLine(new string('━', 40)); + +try +{ + var prompt = await File.ReadAllTextAsync(promptFile); + + for (var i = 1; i <= maxIterations; i++) + { + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); + + // Fresh session β€” each task gets full context budget + var session = await client.CreateSessionAsync( + new SessionConfig + { + Model = "gpt-5.1-codex-mini", + // Pin the agent to the project directory + WorkingDirectory = Environment.CurrentDirectory, + // Auto-approve tool calls for unattended operation + OnPermissionRequest = (_, _) => Task.FromResult( + new PermissionRequestResult { Kind = "approved" }), + }); + try + { + var done = new TaskCompletionSource(); + session.On(evt => + { + // Log tool usage for visibility + if (evt is ToolExecutionStartEvent toolStart) + Console.WriteLine($" βš™ {toolStart.Data.ToolName}"); + else if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); + }); + + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; + } + finally + { + await session.DisposeAsync(); + } + + Console.WriteLine($"\nIteration {i} complete."); + } + + Console.WriteLine($"\nReached max iterations: {maxIterations}"); +} +finally +{ + await client.StopAsync(); +} +``` + +### 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 + +dotnet build + +## Validation + +- Tests: `dotnet test` +- Build: `dotnet build --no-restore` +``` + +## 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 `OnPermissionRequest` 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/dotnet/recipe/accessibility-report.cs b/cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs new file mode 100644 index 00000000..3fe4e387 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/recipe/accessibility-report.cs @@ -0,0 +1,184 @@ +#:package GitHub.Copilot.SDK@* + +using GitHub.Copilot.SDK; + +// Create and start client +await using var client = new CopilotClient(); +await client.StartAsync(); + +Console.WriteLine("=== Accessibility Report Generator ==="); +Console.WriteLine(); + +Console.Write("Enter URL to analyze: "); +var url = Console.ReadLine()?.Trim(); + +if (string.IsNullOrWhiteSpace(url)) +{ + Console.WriteLine("No URL provided. Exiting."); + return; +} + +// Ensure URL has a scheme +if (!url.StartsWith("http://") && !url.StartsWith("https://")) +{ + url = "https://" + url; +} + +Console.WriteLine($"\nAnalyzing: {url}"); +Console.WriteLine("Please wait...\n"); + +// Create a session with Playwright MCP server +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "claude-opus-4.6", + Streaming = true, + McpServers = new Dictionary() + { + ["playwright"] = + new McpLocalServerConfig + { + Type = "local", + Command = "npx", + Args = ["@playwright/mcp@latest"], + Tools = ["*"] + } + }, +}); + +// Wait for response using session.idle event +var done = new TaskCompletionSource(); + +session.On(evt => +{ + switch (evt) + { + case AssistantMessageDeltaEvent delta: + Console.Write(delta.Data.DeltaContent); + break; + case SessionIdleEvent: + done.TrySetResult(); + break; + case SessionErrorEvent error: + Console.WriteLine($"\nError: {error.Data.Message}"); + done.TrySetResult(); + break; + } +}); + +var prompt = $""" + Use the Playwright MCP server to analyze the accessibility of this webpage: {url} + + 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 EXACTLY like this structure with emoji indicators: + + πŸ“Š Accessibility Report: [Page Title] (domain.com) + + βœ… What's Working Well + | Category | Status | Details | + |----------|--------|---------| + | Language | βœ… Pass | lang="en-US" properly set | + | Page Title | βœ… Pass | "[Title]" is descriptive | + | Heading Hierarchy | βœ… Pass | Single H1, proper H2/H3 structure | + | Images | βœ… Pass | All X images have alt text | + | Viewport | βœ… Pass | Allows pinch-zoom (no user-scalable=no) | + | Links | βœ… Pass | No ambiguous "click here" links | + | Reduced Motion | βœ… Pass | Supports prefers-reduced-motion | + | Autoplay Media | βœ… Pass | No autoplay audio/video | + | Font Selector | βœ… Excellent | Includes OpenDyslexic option for dyslexia | + | Dark/Light Mode | βœ… Excellent | User-controlled theme toggle | + + ⚠️ Issues Found + | Severity | Issue | WCAG Criterion | Recommendation | + |----------|-------|----------------|----------------| + | πŸ”΄ High | No
landmark | 1.3.1, 2.4.1 | Wrap main content in
element | + | πŸ”΄ High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top | + | 🟑 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist | + | 🟑 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) | + + πŸ“‹ Stats Summary + - Total Links: X + - Total Headings: X (1Γ— H1, proper hierarchy) + - Focusable Elements: X + - Landmarks Found: banner βœ…, navigation βœ…, main ❌, footer βœ… + + βš™οΈ Priority Recommendations + - Add
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - Increase touch targets - Add padding to nav links and tags to meet 44Γ—44px minimum + - Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis - don't just copy the example. + """; + +await session.SendAsync(new MessageOptions { Prompt = prompt }); +await done.Task; + +Console.WriteLine("\n\n=== Report Complete ===\n"); + +// Prompt user for test generation +Console.Write("Would you like to generate Playwright accessibility tests? (y/n): "); +var generateTests = Console.ReadLine()?.Trim().ToLowerInvariant(); + +if (generateTests == "y" || generateTests == "yes") +{ + // Reset for next interaction + done = new TaskCompletionSource(); + + var detectLanguagePrompt = $""" + Analyze the current working directory to detect the primary programming language used in this project. + Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc. + + Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java") + and a brief explanation of why you detected it. + If no project is detected, suggest "TypeScript" as the default for Playwright tests. + """; + + Console.WriteLine("\nDetecting project language...\n"); + await session.SendAsync(new MessageOptions { Prompt = detectLanguagePrompt }); + await done.Task; + + Console.Write("\n\nConfirm language for tests (or enter a different one): "); + var language = Console.ReadLine()?.Trim(); + + if (string.IsNullOrWhiteSpace(language)) + { + language = "TypeScript"; + } + + // Reset for test generation + done = new TaskCompletionSource(); + + var testGenerationPrompt = $""" + Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}. + + The tests should: + 1. Verify all the accessibility checks from the report + 2. Test for the issues that were found (to ensure they get fixed) + 3. Include tests for: + - Page has proper lang attribute + - Page has descriptive title + - Heading hierarchy is correct (single H1, proper nesting) + - All images have alt text + - No autoplay media + - Landmark regions exist (banner, nav, main, footer) + - Skip navigation link exists and works + - Focus indicators are visible + - Touch targets meet minimum size requirements + 4. Use Playwright's accessibility testing features + 5. Include helpful comments explaining each test + + Output the complete test file that can be saved and run. + Use the Playwright MCP server tools if you need to verify any page details. + """; + + Console.WriteLine("\nGenerating accessibility tests...\n"); + await session.SendAsync(new MessageOptions { Prompt = testGenerationPrompt }); + await done.Task; + + Console.WriteLine("\n\n=== Tests Generated ==="); +} diff --git a/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs new file mode 100644 index 00000000..9f153324 --- /dev/null +++ b/cookbook/copilot-sdk/dotnet/recipe/ralph-loop.cs @@ -0,0 +1,83 @@ +#:package GitHub.Copilot.SDK@* + +using GitHub.Copilot.SDK; + +// Ralph loop: autonomous AI task loop with fresh context per iteration. +// +// Two modes: +// - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md +// - "build": reads PROMPT_build.md, implements tasks, runs tests, commits +// +// Each iteration creates a fresh session so the agent always operates in +// the "smart zone" of its context window. State is shared between +// iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*). +// +// Usage: +// dotnet run # build mode, 50 iterations +// dotnet run plan # planning mode +// dotnet run 20 # build mode, 20 iterations +// dotnet run plan 5 # planning mode, 5 iterations + +var mode = args.Contains("plan") ? "plan" : "build"; +var maxArg = args.FirstOrDefault(a => int.TryParse(a, out _)); +var maxIterations = maxArg != null ? int.Parse(maxArg) : 50; +var promptFile = mode == "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; + +var client = new CopilotClient(); +await client.StartAsync(); + +Console.WriteLine(new string('━', 40)); +Console.WriteLine($"Mode: {mode}"); +Console.WriteLine($"Prompt: {promptFile}"); +Console.WriteLine($"Max: {maxIterations} iterations"); +Console.WriteLine(new string('━', 40)); + +try +{ + var prompt = await File.ReadAllTextAsync(promptFile); + + for (var i = 1; i <= maxIterations; i++) + { + Console.WriteLine($"\n=== Iteration {i}/{maxIterations} ==="); + + // Fresh session β€” each task gets full context budget + var session = await client.CreateSessionAsync( + new SessionConfig + { + Model = "gpt-5.1-codex-mini", + // Pin the agent to the project directory + WorkingDirectory = Environment.CurrentDirectory, + // Auto-approve tool calls for unattended operation + OnPermissionRequest = (_, _) => Task.FromResult( + new PermissionRequestResult { Kind = "approved" }), + }); + + try + { + var done = new TaskCompletionSource(); + session.On(evt => + { + // Log tool usage for visibility + if (evt is ToolExecutionStartEvent toolStart) + Console.WriteLine($" βš™ {toolStart.Data.ToolName}"); + else if (evt is AssistantMessageEvent msg) + done.TrySetResult(msg.Data.Content); + }); + + await session.SendAsync(new MessageOptions { Prompt = prompt }); + await done.Task; + } + finally + { + await session.DisposeAsync(); + } + + Console.WriteLine($"\nIteration {i} complete."); + } + + Console.WriteLine($"\nReached max iterations: {maxIterations}"); +} +finally +{ + await client.StopAsync(); +} diff --git a/cookbook/copilot-sdk/go/accessibility-report.md b/cookbook/copilot-sdk/go/accessibility-report.md new file mode 100644 index 00000000..afe7ea27 --- /dev/null +++ b/cookbook/copilot-sdk/go/accessibility-report.md @@ -0,0 +1,291 @@ +# 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/accessibility-report.go](recipe/accessibility-report.go) +> +> ```bash +> go run recipe/accessibility-report.go +> ``` + +## 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 + +```bash +go get github.com/github/copilot-sdk/go +``` + +You also need `npx` available (Node.js installed) for the Playwright MCP server. + +## Usage + +```bash +go run accessibility-report.go +# Enter a URL when prompted +``` + +## Full example: accessibility-report.go + +```go +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + reader := bufio.NewReader(os.Stdin) + + fmt.Println("=== Accessibility Report Generator ===") + fmt.Println() + + fmt.Print("Enter URL to analyze: ") + url, _ := reader.ReadString('\n') + url = strings.TrimSpace(url) + + if url == "" { + fmt.Println("No URL provided. Exiting.") + return + } + + // Ensure URL has a scheme + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + url = "https://" + url + } + + fmt.Printf("\nAnalyzing: %s\n", url) + fmt.Println("Please wait...\n") + + // Create Copilot client with Playwright MCP server + client := copilot.NewClient(nil) + + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + streaming := true + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "claude-opus-4.6", + Streaming: &streaming, + McpServers: map[string]interface{}{ + "playwright": map[string]interface{}{ + "type": "local", + "command": "npx", + "args": []string{"@playwright/mcp@latest"}, + "tools": []string{"*"}, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + defer session.Destroy() + + // Set up streaming event handling + done := make(chan struct{}, 1) + + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message.delta": + if event.Data.DeltaContent != nil { + fmt.Print(*event.Data.DeltaContent) + } + case "session.idle": + select { + case done <- struct{}{}: + default: + } + case "session.error": + if event.Data.Message != nil { + fmt.Printf("\nError: %s\n", *event.Data.Message) + } + select { + case done <- struct{}{}: + default: + } + } + }) + + prompt := fmt.Sprintf(` + 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. + `, url) + + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Println("\n\n=== Report Complete ===\n") + + // Prompt user for test generation + fmt.Print("Would you like to generate Playwright accessibility tests? (y/n): ") + generateTests, _ := reader.ReadString('\n') + generateTests = strings.TrimSpace(strings.ToLower(generateTests)) + + if generateTests == "y" || generateTests == "yes" { + detectLanguagePrompt := ` + Analyze the current working directory to detect the primary programming language. + Respond with ONLY the detected language name and a brief explanation. + If no project is detected, suggest "TypeScript" as the default. + ` + + fmt.Println("\nDetecting project language...\n") + select { + case <-done: + default: + } + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: detectLanguagePrompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Print("\n\nConfirm language for tests (or enter a different one): ") + language, _ := reader.ReadString('\n') + language = strings.TrimSpace(language) + if language == "" { + language = "TypeScript" + } + + testGenerationPrompt := fmt.Sprintf(` + Based on the accessibility report you just generated for %s, + create Playwright accessibility tests in %s. + + 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. + `, url, language) + + fmt.Println("\nGenerating accessibility tests...\n") + select { + case <-done: + default: + } + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: testGenerationPrompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Println("\n\n=== Tests Generated ===") + } +} +``` + +## 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: &streaming` and `assistant.message.delta` events 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 detects the project language and generates Playwright accessibility tests + +## Key concepts + +### MCP server configuration + +The recipe configures a local MCP server that runs alongside the session: + +```go +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + McpServers: map[string]interface{}{ + "playwright": map[string]interface{}{ + "type": "local", + "command": "npx", + "args": []string{"@playwright/mcp@latest"}, + "tools": []string{"*"}, + }, + }, +}) +``` + +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: + +```go +session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message.delta": + if event.Data.DeltaContent != nil { + fmt.Print(*event.Data.DeltaContent) + } + case "session.idle": + done <- struct{}{} + } +}) +``` + +## 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 + +Detecting project language... +TypeScript detected (package.json found) + +Confirm language for tests (or enter a different one): + +Generating accessibility tests... +[Generated test file output...] + +=== Tests Generated === +``` diff --git a/cookbook/copilot-sdk/go/error-handling.md b/cookbook/copilot-sdk/go/error-handling.md index 658613a9..462d2706 100644 --- a/cookbook/copilot-sdk/go/error-handling.md +++ b/cookbook/copilot-sdk/go/error-handling.md @@ -18,24 +18,22 @@ You need to handle various error conditions like connection failures, timeouts, package main import ( + "context" "fmt" "log" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatalf("Failed to start client: %v", err) } - defer func() { - if err := client.Stop(); err != nil { - log.Printf("Error stopping client: %v", err) - } - }() + defer client.Stop() - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", }) if err != nil { @@ -43,19 +41,15 @@ func main() { } defer session.Destroy() - responseChan := make(chan string, 1) - session.On(func(event copilot.Event) { - if msg, ok := event.(copilot.AssistantMessageEvent); ok { - responseChan <- msg.Data.Content - } - }) - - if err := session.Send(copilot.MessageOptions{Prompt: "Hello!"}); err != nil { + result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + if err != nil { log.Printf("Failed to send message: %v", err) + return } - response := <-responseChan - fmt.Println(response) + if result != nil && result.Data.Content != nil { + fmt.Println(*result.Data.Content) + } } ``` @@ -63,14 +57,17 @@ func main() { ```go import ( + "context" "errors" + "fmt" "os/exec" + copilot "github.com/github/copilot-sdk/go" ) -func startClient() error { - client := copilot.NewClient() +func startClient(ctx context.Context) error { + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { var execErr *exec.Error if errors.As(err, &execErr) { return fmt.Errorf("Copilot CLI not found. Please install it first: %w", err) @@ -90,48 +87,41 @@ func startClient() error { ```go import ( "context" + "errors" + "fmt" "time" + copilot "github.com/github/copilot-sdk/go" ) func sendWithTimeout(session *copilot.Session) error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - responseChan := make(chan string, 1) - errChan := make(chan error, 1) - - session.On(func(event copilot.Event) { - if msg, ok := event.(copilot.AssistantMessageEvent); ok { - responseChan <- msg.Data.Content + result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Complex question..."}) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return fmt.Errorf("request timed out") } - }) - - if err := session.Send(copilot.MessageOptions{Prompt: "Complex question..."}); err != nil { return err } - select { - case response := <-responseChan: - fmt.Println(response) - return nil - case err := <-errChan: - return err - case <-ctx.Done(): - return fmt.Errorf("request timed out") + if result != nil && result.Data.Content != nil { + fmt.Println(*result.Data.Content) } + return nil } ``` ## Aborting a request ```go -func abortAfterDelay(session *copilot.Session) { - // Start a request - session.Send(copilot.MessageOptions{Prompt: "Write a very long story..."}) +func abortAfterDelay(ctx context.Context, session *copilot.Session) { + // Start a request (non-blocking send) + session.Send(ctx, copilot.MessageOptions{Prompt: "Write a very long story..."}) // Abort it after some condition time.AfterFunc(5*time.Second, func() { - if err := session.Abort(); err != nil { + if err := session.Abort(ctx); err != nil { log.Printf("Failed to abort: %v", err) } fmt.Println("Request aborted") @@ -143,13 +133,18 @@ func abortAfterDelay(session *copilot.Session) { ```go import ( + "context" + "fmt" + "log" "os" "os/signal" "syscall" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) // Set up signal handling sigChan := make(chan os.Signal, 1) @@ -158,15 +153,11 @@ func main() { go func() { <-sigChan fmt.Println("\nShutting down...") - - if err := client.Stop(); err != nil { - log.Printf("Cleanup errors: %v", err) - } - + client.Stop() os.Exit(0) }() - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } @@ -178,14 +169,15 @@ func main() { ```go func doWork() error { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { return fmt.Errorf("failed to start: %w", err) } defer client.Stop() - session, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"}) + session, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"}) if err != nil { return fmt.Errorf("failed to create session: %w", err) } diff --git a/cookbook/copilot-sdk/go/managing-local-files.md b/cookbook/copilot-sdk/go/managing-local-files.md index 1e5a2999..f86871a5 100644 --- a/cookbook/copilot-sdk/go/managing-local-files.md +++ b/cookbook/copilot-sdk/go/managing-local-files.md @@ -18,23 +18,26 @@ You have a folder with many files and want to organize them into subfolders base package main import ( + "context" "fmt" "log" "os" "path/filepath" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { + ctx := context.Background() + // Create and start client - client := copilot.NewClient() - if err := client.Start(); err != nil { + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() // Create session - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", }) if err != nil { @@ -43,14 +46,20 @@ func main() { defer session.Destroy() // Event handler - session.On(func(event copilot.Event) { - switch e := event.(type) { - case copilot.AssistantMessageEvent: - fmt.Printf("\nCopilot: %s\n", e.Data.Content) - case copilot.ToolExecutionStartEvent: - fmt.Printf(" β†’ Running: %s\n", e.Data.ToolName) - case copilot.ToolExecutionCompleteEvent: - fmt.Printf(" βœ“ Completed: %s\n", e.Data.ToolName) + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message": + if event.Data.Content != nil { + fmt.Printf("\nCopilot: %s\n", *event.Data.Content) + } + case "tool.execution_start": + if event.Data.ToolName != nil { + fmt.Printf(" β†’ Running: %s\n", *event.Data.ToolName) + } + case "tool.execution_complete": + if event.Data.ToolName != nil { + fmt.Printf(" βœ“ Completed: %s\n", *event.Data.ToolName) + } } }) @@ -69,11 +78,10 @@ Analyze the files in "%s" and organize them into subfolders. Please confirm before moving any files. `, targetFolder) - if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil { + _, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + if err != nil { log.Fatal(err) } - - session.WaitForIdle() } ``` @@ -116,7 +124,7 @@ 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(copilot.MessageOptions{Prompt: prompt}) +session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) ``` ## Custom grouping with AI analysis @@ -134,7 +142,7 @@ Consider: Propose folder names that are descriptive and useful. `, targetFolder) -session.Send(copilot.MessageOptions{Prompt: prompt}) +session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) ``` ## Safety considerations diff --git a/cookbook/copilot-sdk/go/multiple-sessions.md b/cookbook/copilot-sdk/go/multiple-sessions.md index 66261961..82d8bf50 100644 --- a/cookbook/copilot-sdk/go/multiple-sessions.md +++ b/cookbook/copilot-sdk/go/multiple-sessions.md @@ -18,47 +18,49 @@ You need to run multiple conversations in parallel, each with its own context an package main import ( + "context" "fmt" "log" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() // Create multiple independent sessions - session1, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"}) + session1, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"}) if err != nil { log.Fatal(err) } defer session1.Destroy() - session2, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"}) + session2, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"}) if err != nil { log.Fatal(err) } defer session2.Destroy() - session3, err := client.CreateSession(copilot.SessionConfig{Model: "claude-sonnet-4.5"}) + session3, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "claude-sonnet-4.5"}) if err != nil { log.Fatal(err) } defer session3.Destroy() // Each session maintains its own conversation history - session1.Send(copilot.MessageOptions{Prompt: "You are helping with a Python project"}) - session2.Send(copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"}) - session3.Send(copilot.MessageOptions{Prompt: "You are helping with a Go project"}) + session1.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Python project"}) + session2.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"}) + session3.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Go project"}) // Follow-up messages stay in their respective contexts - session1.Send(copilot.MessageOptions{Prompt: "How do I create a virtual environment?"}) - session2.Send(copilot.MessageOptions{Prompt: "How do I set up tsconfig?"}) - session3.Send(copilot.MessageOptions{Prompt: "How do I initialize a module?"}) + session1.Send(ctx, copilot.MessageOptions{Prompt: "How do I create a virtual environment?"}) + session2.Send(ctx, copilot.MessageOptions{Prompt: "How do I set up tsconfig?"}) + session3.Send(ctx, copilot.MessageOptions{Prompt: "How do I initialize a module?"}) } ``` @@ -67,7 +69,7 @@ func main() { Use custom IDs for easier tracking: ```go -session, err := client.CreateSession(copilot.SessionConfig{ +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: "user-123-chat", Model: "gpt-5", }) @@ -81,7 +83,7 @@ fmt.Println(session.SessionID) // "user-123-chat" ## Listing sessions ```go -sessions, err := client.ListSessions() +sessions, err := client.ListSessions(ctx) if err != nil { log.Fatal(err) } @@ -95,7 +97,7 @@ for _, sessionInfo := range sessions { ```go // Delete a specific session -if err := client.DeleteSession("user-123-chat"); err != nil { +if err := client.DeleteSession(ctx, "user-123-chat"); err != nil { log.Printf("Failed to delete session: %v", err) } ``` diff --git a/cookbook/copilot-sdk/go/persisting-sessions.md b/cookbook/copilot-sdk/go/persisting-sessions.md index 8587b978..ea13b7ab 100644 --- a/cookbook/copilot-sdk/go/persisting-sessions.md +++ b/cookbook/copilot-sdk/go/persisting-sessions.md @@ -19,22 +19,24 @@ You want users to be able to continue a conversation even after closing and reop package main import ( + "context" "fmt" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() - client.Start() + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) defer client.Stop() // Create session with a memorable ID - session, _ := client.CreateSession(copilot.SessionConfig{ + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: "user-123-conversation", Model: "gpt-5", }) - session.Send(copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"}) + session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"}) // Session ID is preserved fmt.Println(session.SessionID) @@ -47,15 +49,16 @@ func main() { ### Resuming a session ```go -client := copilot.NewClient() -client.Start() +ctx := context.Background() +client := copilot.NewClient(nil) +client.Start(ctx) defer client.Stop() // Resume the previous session -session, _ := client.ResumeSession("user-123-conversation") +session, _ := client.ResumeSession(ctx, "user-123-conversation") // Previous context is restored -session.Send(copilot.MessageOptions{Prompt: "What were we discussing?"}) +session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What were we discussing?"}) session.Destroy() ``` @@ -63,7 +66,7 @@ session.Destroy() ### Listing available sessions ```go -sessions, _ := client.ListSessions() +sessions, _ := client.ListSessions(ctx) for _, s := range sessions { fmt.Println("Session:", s.SessionID) } @@ -73,15 +76,17 @@ for _, s := range sessions { ```go // Remove session and all its data from disk -client.DeleteSession("user-123-conversation") +client.DeleteSession(ctx, "user-123-conversation") ``` ### Getting session history ```go -messages, _ := session.GetMessages() +messages, _ := session.GetMessages(ctx) for _, msg := range messages { - fmt.Printf("[%s] %v\n", msg.Type, msg.Data) + if msg.Data.Content != nil { + fmt.Printf("[%s] %s\n", msg.Type, *msg.Data.Content) + } } ``` diff --git a/cookbook/copilot-sdk/go/pr-visualization.md b/cookbook/copilot-sdk/go/pr-visualization.md index e8046733..b0c7c0b3 100644 --- a/cookbook/copilot-sdk/go/pr-visualization.md +++ b/cookbook/copilot-sdk/go/pr-visualization.md @@ -39,6 +39,7 @@ package main import ( "bufio" + "context" "flag" "fmt" "log" @@ -46,7 +47,7 @@ import ( "os/exec" "regexp" "strings" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) // ============================================================================ @@ -94,6 +95,7 @@ func promptForRepo() string { // ============================================================================ func main() { + ctx := context.Background() repoFlag := flag.String("repo", "", "GitHub repository (owner/repo)") flag.Parse() @@ -126,18 +128,18 @@ func main() { parts := strings.SplitN(repo, "/", 2) owner, repoName := parts[0], parts[1] - // Create Copilot client - no custom tools needed! - client := copilot.NewClient(copilot.ClientConfig{LogLevel: "error"}) + // Create Copilot client + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() cwd, _ := os.Getwd() - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", - SystemMessage: copilot.SystemMessage{ + SystemMessage: &copilot.SystemMessageConfig{ Content: fmt.Sprintf(` You are analyzing pull requests for the GitHub repository: %s/%s @@ -159,12 +161,16 @@ The current working directory is: %s defer session.Destroy() // Set up event handling - session.On(func(event copilot.Event) { - switch e := event.(type) { - case copilot.AssistantMessageEvent: - fmt.Printf("\nπŸ€– %s\n\n", e.Data.Content) - case copilot.ToolExecutionStartEvent: - fmt.Printf(" βš™οΈ %s\n", e.Data.ToolName) + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message": + if event.Data.Content != nil { + fmt.Printf("\nπŸ€– %s\n\n", *event.Data.Content) + } + case "tool.execution_start": + if event.Data.ToolName != nil { + fmt.Printf(" βš™οΈ %s\n", *event.Data.ToolName) + } } }) @@ -180,12 +186,10 @@ The current working directory is: %s Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale. `, owner, repoName) - if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil { + if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil { log.Fatal(err) } - session.WaitForIdle() - // Interactive loop fmt.Println("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") fmt.Println("Examples:") @@ -209,11 +213,9 @@ The current working directory is: %s break } - if err := session.Send(copilot.MessageOptions{Prompt: input}); err != nil { + if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}); err != nil { log.Printf("Error: %v", err) } - - session.WaitForIdle() } } ``` diff --git a/cookbook/copilot-sdk/go/ralph-loop.md b/cookbook/copilot-sdk/go/ralph-loop.md new file mode 100644 index 00000000..f8462c3d --- /dev/null +++ b/cookbook/copilot-sdk/go/ralph-loop.md @@ -0,0 +1,296 @@ +# 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/ralph-loop.go](recipe/ralph-loop.go) +> +> ```bash +> cd go +> go run recipe/ralph-loop.go +> ``` + +## 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`: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + copilot "github.com/github/copilot-sdk/go" +) + +func ralphLoop(ctx context.Context, promptFile string, maxIterations int) error { + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return err + } + defer client.Stop() + + prompt, err := os.ReadFile(promptFile) + if err != nil { + return err + } + + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) + + // Fresh session each iteration β€” context isolation is the point + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + }) + if err != nil { + return err + } + + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + session.Destroy() + if err != nil { + return err + } + + fmt.Printf("Iteration %d complete.\n", i) + } + return nil +} + +func main() { + if err := ralphLoop(context.Background(), "PROMPT.md", 20); err != nil { + log.Fatal(err) + } +} +``` + +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: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +func ralphLoop(ctx context.Context, mode string, maxIterations int) error { + promptFile := "PROMPT_build.md" + if mode == "plan" { + promptFile = "PROMPT_plan.md" + } + + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return err + } + defer client.Stop() + + cwd, _ := os.Getwd() + + fmt.Println(strings.Repeat("━", 40)) + fmt.Printf("Mode: %s\n", mode) + fmt.Printf("Prompt: %s\n", promptFile) + fmt.Printf("Max: %d iterations\n", maxIterations) + fmt.Println(strings.Repeat("━", 40)) + + prompt, err := os.ReadFile(promptFile) + if err != nil { + return err + } + + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + WorkingDirectory: cwd, + OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult { + return copilot.PermissionRequestResult{Kind: "approved"} + }, + }) + if err != nil { + return err + } + + // Log tool usage for visibility + session.On(func(event copilot.Event) { + if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" βš™ %s\n", toolExecution.Data.ToolName) + } + }) + + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + session.Destroy() + if err != nil { + return err + } + + fmt.Printf("\nIteration %d complete.\n", i) + } + + fmt.Printf("\nReached max iterations: %d\n", maxIterations) + return nil +} + +func main() { + mode := "build" + maxIterations := 50 + + for _, arg := range os.Args[1:] { + if arg == "plan" { + mode = "plan" + } else if n, err := strconv.Atoi(arg); err == nil { + maxIterations = n + } + } + + if err := ralphLoop(context.Background(), mode, maxIterations); err != nil { + log.Fatal(err) + } +} +``` + +### 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 + +go build ./... + +## Validation + +- Tests: `go test ./...` +- Vet: `go vet ./...` +``` + +## 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 `OnPermissionRequest` 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/go/recipe/accessibility-report.go b/cookbook/copilot-sdk/go/recipe/accessibility-report.go new file mode 100644 index 00000000..e1ae2a49 --- /dev/null +++ b/cookbook/copilot-sdk/go/recipe/accessibility-report.go @@ -0,0 +1,213 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + reader := bufio.NewReader(os.Stdin) + + fmt.Println("=== Accessibility Report Generator ===") + fmt.Println() + + fmt.Print("Enter URL to analyze: ") + url, _ := reader.ReadString('\n') + url = strings.TrimSpace(url) + + if url == "" { + fmt.Println("No URL provided. Exiting.") + return + } + + // Ensure URL has a scheme + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + url = "https://" + url + } + + fmt.Printf("\nAnalyzing: %s\n", url) + fmt.Println("Please wait...\n") + + // Create Copilot client with Playwright MCP server + client := copilot.NewClient(nil) + + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + streaming := true + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "claude-opus-4.6", + Streaming: &streaming, + McpServers: map[string]interface{}{ + "playwright": map[string]interface{}{ + "type": "local", + "command": "npx", + "args": []string{"@playwright/mcp@latest"}, + "tools": []string{"*"}, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + defer session.Destroy() + + // Set up streaming event handling + done := make(chan struct{}, 1) + + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message.delta": + if event.Data.DeltaContent != nil { + fmt.Print(*event.Data.DeltaContent) + } + case "session.idle": + select { + case done <- struct{}{}: + default: + } + case "session.error": + if event.Data.Message != nil { + fmt.Printf("\nError: %s\n", *event.Data.Message) + } + select { + case done <- struct{}{}: + default: + } + } + }) + + prompt := fmt.Sprintf(` + 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 EXACTLY like this structure with emoji indicators: + + πŸ“Š Accessibility Report: [Page Title] (domain.com) + + βœ… What's Working Well + | Category | Status | Details | + |----------|--------|---------| + | Language | βœ… Pass | lang="en-US" properly set | + | Page Title | βœ… Pass | "[Title]" is descriptive | + | Heading Hierarchy | βœ… Pass | Single H1, proper H2/H3 structure | + | Images | βœ… Pass | All X images have alt text | + | Viewport | βœ… Pass | Allows pinch-zoom (no user-scalable=no) | + | Links | βœ… Pass | No ambiguous "click here" links | + | Reduced Motion | βœ… Pass | Supports prefers-reduced-motion | + | Autoplay Media | βœ… Pass | No autoplay audio/video | + + ⚠️ Issues Found + | Severity | Issue | WCAG Criterion | Recommendation | + |----------|-------|----------------|----------------| + | πŸ”΄ High | No
landmark | 1.3.1, 2.4.1 | Wrap main content in
element | + | πŸ”΄ High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top | + | 🟑 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist | + | 🟑 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) | + + πŸ“‹ Stats Summary + - Total Links: X + - Total Headings: X (1Γ— H1, proper hierarchy) + - Focusable Elements: X + - Landmarks Found: banner βœ…, navigation βœ…, main ❌, footer βœ… + + βš™οΈ Priority Recommendations + - Add
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - Increase touch targets - Add padding to nav links and tags to meet 44Γ—44px minimum + - Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis - don't just copy the example. + `, url) + + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Println("\n\n=== Report Complete ===\n") + + // Prompt user for test generation + fmt.Print("Would you like to generate Playwright accessibility tests? (y/n): ") + generateTests, _ := reader.ReadString('\n') + generateTests = strings.TrimSpace(strings.ToLower(generateTests)) + + if generateTests == "y" || generateTests == "yes" { + detectLanguagePrompt := ` + Analyze the current working directory to detect the primary programming language used in this project. + Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc. + + Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java") + and a brief explanation of why you detected it. + If no project is detected, suggest "TypeScript" as the default for Playwright tests. + ` + + fmt.Println("\nDetecting project language...\n") + // Drain the previous done signal + select { + case <-done: + default: + } + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: detectLanguagePrompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Print("\n\nConfirm language for tests (or enter a different one): ") + language, _ := reader.ReadString('\n') + language = strings.TrimSpace(language) + if language == "" { + language = "TypeScript" + } + + testGenerationPrompt := fmt.Sprintf(` + Based on the accessibility report you just generated for %s, create Playwright accessibility tests in %s. + + The tests should: + 1. Verify all the accessibility checks from the report + 2. Test for the issues that were found (to ensure they get fixed) + 3. Include tests for: + - Page has proper lang attribute + - Page has descriptive title + - Heading hierarchy is correct (single H1, proper nesting) + - All images have alt text + - No autoplay media + - Landmark regions exist (banner, nav, main, footer) + - Skip navigation link exists and works + - Focus indicators are visible + - Touch targets meet minimum size requirements + 4. Use Playwright's accessibility testing features + 5. Include helpful comments explaining each test + + Output the complete test file that can be saved and run. + Use the Playwright MCP server tools if you need to verify any page details. + `, url, language) + + fmt.Println("\nGenerating accessibility tests...\n") + // Drain the previous done signal + select { + case <-done: + default: + } + if _, err := session.Send(ctx, copilot.MessageOptions{Prompt: testGenerationPrompt}); err != nil { + log.Fatal(err) + } + <-done + + fmt.Println("\n\n=== Tests Generated ===") + } +} diff --git a/cookbook/copilot-sdk/go/recipe/error-handling.go b/cookbook/copilot-sdk/go/recipe/error-handling.go index 32edd9f9..3fc0fcdc 100644 --- a/cookbook/copilot-sdk/go/recipe/error-handling.go +++ b/cookbook/copilot-sdk/go/recipe/error-handling.go @@ -1,25 +1,23 @@ package main import ( + "context" "fmt" "log" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatalf("Failed to start client: %v", err) } - defer func() { - if err := client.Stop(); err != nil { - log.Printf("Error stopping client: %v", err) - } - }() + defer client.Stop() - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", }) if err != nil { @@ -27,18 +25,13 @@ func main() { } defer session.Destroy() - responseChan := make(chan string, 1) - session.On(func(event copilot.Event) { - if msg, ok := event.(copilot.AssistantMessageEvent); ok { - responseChan <- msg.Data.Content - } - }) - - if err := session.Send(copilot.MessageOptions{Prompt: "Hello!"}); err != nil { + result, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + if err != nil { log.Printf("Failed to send message: %v", err) return } - response := <-responseChan - fmt.Println(response) + if result != nil && result.Data.Content != nil { + fmt.Println(*result.Data.Content) + } } diff --git a/cookbook/copilot-sdk/go/recipe/managing-local-files.go b/cookbook/copilot-sdk/go/recipe/managing-local-files.go index f1582669..dc3dfd84 100644 --- a/cookbook/copilot-sdk/go/recipe/managing-local-files.go +++ b/cookbook/copilot-sdk/go/recipe/managing-local-files.go @@ -1,24 +1,27 @@ package main import ( + "context" "fmt" "log" "os" "path/filepath" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { + ctx := context.Background() + // Create and start client - client := copilot.NewClient() - if err := client.Start(); err != nil { + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() // Create session - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", }) if err != nil { @@ -27,14 +30,20 @@ func main() { defer session.Destroy() // Event handler - session.On(func(event copilot.Event) { - switch e := event.(type) { - case copilot.AssistantMessageEvent: - fmt.Printf("\nCopilot: %s\n", e.Data.Content) - case copilot.ToolExecutionStartEvent: - fmt.Printf(" β†’ Running: %s\n", e.Data.ToolName) - case copilot.ToolExecutionCompleteEvent: - fmt.Printf(" βœ“ Completed: %s\n", e.Data.ToolName) + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message": + if event.Data.Content != nil { + fmt.Printf("\nCopilot: %s\n", *event.Data.Content) + } + case "tool.execution_start": + if event.Data.ToolName != nil { + fmt.Printf(" β†’ Running: %s\n", *event.Data.ToolName) + } + case "tool.execution_complete": + if event.Data.ToolName != nil { + fmt.Printf(" βœ“ Completed: %s\n", *event.Data.ToolName) + } } }) @@ -54,9 +63,8 @@ Analyze the files in "%s" and organize them into subfolders. Please confirm before moving any files. `, targetFolder) - if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil { + _, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}) + if err != nil { log.Fatal(err) } - - session.WaitForIdle() } diff --git a/cookbook/copilot-sdk/go/recipe/multiple-sessions.go b/cookbook/copilot-sdk/go/recipe/multiple-sessions.go index 0fb3325c..9b99fcd2 100644 --- a/cookbook/copilot-sdk/go/recipe/multiple-sessions.go +++ b/cookbook/copilot-sdk/go/recipe/multiple-sessions.go @@ -1,34 +1,36 @@ package main import ( + "context" "fmt" "log" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() + ctx := context.Background() + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() // Create multiple independent sessions - session1, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"}) + session1, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"}) if err != nil { log.Fatal(err) } defer session1.Destroy() - session2, err := client.CreateSession(copilot.SessionConfig{Model: "gpt-5"}) + session2, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-5"}) if err != nil { log.Fatal(err) } defer session2.Destroy() - session3, err := client.CreateSession(copilot.SessionConfig{Model: "claude-sonnet-4.5"}) + session3, err := client.CreateSession(ctx, &copilot.SessionConfig{Model: "claude-sonnet-4.5"}) if err != nil { log.Fatal(err) } @@ -37,16 +39,16 @@ func main() { fmt.Println("Created 3 independent sessions") // Each session maintains its own conversation history - session1.Send(copilot.MessageOptions{Prompt: "You are helping with a Python project"}) - session2.Send(copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"}) - session3.Send(copilot.MessageOptions{Prompt: "You are helping with a Go project"}) + session1.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Python project"}) + session2.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a TypeScript project"}) + session3.Send(ctx, copilot.MessageOptions{Prompt: "You are helping with a Go project"}) fmt.Println("Sent initial context to all sessions") // Follow-up messages stay in their respective contexts - session1.Send(copilot.MessageOptions{Prompt: "How do I create a virtual environment?"}) - session2.Send(copilot.MessageOptions{Prompt: "How do I set up tsconfig?"}) - session3.Send(copilot.MessageOptions{Prompt: "How do I initialize a module?"}) + session1.Send(ctx, copilot.MessageOptions{Prompt: "How do I create a virtual environment?"}) + session2.Send(ctx, copilot.MessageOptions{Prompt: "How do I set up tsconfig?"}) + session3.Send(ctx, copilot.MessageOptions{Prompt: "How do I initialize a module?"}) fmt.Println("Sent follow-up questions to each session") fmt.Println("All sessions will be destroyed on exit") diff --git a/cookbook/copilot-sdk/go/recipe/persisting-sessions.go b/cookbook/copilot-sdk/go/recipe/persisting-sessions.go index 11ee7ad0..471e5757 100644 --- a/cookbook/copilot-sdk/go/recipe/persisting-sessions.go +++ b/cookbook/copilot-sdk/go/recipe/persisting-sessions.go @@ -1,68 +1,68 @@ package main import ( - "fmt" - "log" + "context" + "fmt" + "log" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient() - if err := client.Start(); err != nil { - log.Fatal(err) - } - defer client.Stop() + ctx := context.Background() + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() - // Create session with a memorable ID - session, err := client.CreateSession(copilot.SessionConfig{ - SessionID: "user-123-conversation", - Model: "gpt-5", - }) - if err != nil { - log.Fatal(err) - } + // Create session with a memorable ID + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "user-123-conversation", + Model: "gpt-5", + }) + if err != nil { + log.Fatal(err) + } - if err := session.Send(copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"}); err != nil { - log.Fatal(err) - } - fmt.Printf("Session created: %s\n", session.SessionID) + _, err = session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Let's discuss TypeScript generics"}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Session created: %s\n", session.SessionID) - // Destroy session but keep data on disk - if err := session.Destroy(); err != nil { - log.Fatal(err) - } - fmt.Println("Session destroyed (state persisted)") + // Destroy session but keep data on disk + session.Destroy() + fmt.Println("Session destroyed (state persisted)") - // Resume the previous session - resumed, err := client.ResumeSession("user-123-conversation") - if err != nil { - log.Fatal(err) - } - fmt.Printf("Resumed: %s\n", resumed.SessionID) + // Resume the previous session + resumed, err := client.ResumeSession(ctx, "user-123-conversation") + if err != nil { + log.Fatal(err) + } + fmt.Printf("Resumed: %s\n", resumed.SessionID) - if err := resumed.Send(copilot.MessageOptions{Prompt: "What were we discussing?"}); err != nil { - log.Fatal(err) - } + _, err = resumed.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What were we discussing?"}) + if err != nil { + log.Fatal(err) + } - // List sessions - sessions, err := client.ListSessions() - if err != nil { - log.Fatal(err) - } - ids := make([]string, 0, len(sessions)) - for _, s := range sessions { - ids = append(ids, s.SessionID) - } - fmt.Printf("Sessions: %v\n", ids) + // List sessions + sessions, err := client.ListSessions(ctx) + if err != nil { + log.Fatal(err) + } + ids := make([]string, 0, len(sessions)) + for _, s := range sessions { + ids = append(ids, s.SessionID) + } + fmt.Printf("Sessions: %v\n", ids) - // Delete session permanently - if err := client.DeleteSession("user-123-conversation"); err != nil { - log.Fatal(err) - } - fmt.Println("Session deleted") + // Delete session permanently + if err := client.DeleteSession(ctx, "user-123-conversation"); err != nil { + log.Fatal(err) + } + fmt.Println("Session deleted") - if err := resumed.Destroy(); err != nil { - log.Fatal(err) - } + resumed.Destroy() } diff --git a/cookbook/copilot-sdk/go/recipe/pr-visualization.go b/cookbook/copilot-sdk/go/recipe/pr-visualization.go index abea027b..54178eec 100644 --- a/cookbook/copilot-sdk/go/recipe/pr-visualization.go +++ b/cookbook/copilot-sdk/go/recipe/pr-visualization.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "flag" "fmt" "log" @@ -10,7 +11,7 @@ import ( "regexp" "strings" - "github.com/github/copilot-sdk/go" + copilot "github.com/github/copilot-sdk/go" ) // ============================================================================ @@ -58,6 +59,7 @@ func promptForRepo() string { // ============================================================================ func main() { + ctx := context.Background() repoFlag := flag.String("repo", "", "GitHub repository (owner/repo)") flag.Parse() @@ -90,18 +92,18 @@ func main() { parts := strings.SplitN(repo, "/", 2) owner, repoName := parts[0], parts[1] - // Create Copilot client - no custom tools needed! - client := copilot.NewClient(copilot.ClientConfig{LogLevel: "error"}) + // Create Copilot client + client := copilot.NewClient(nil) - if err := client.Start(); err != nil { + if err := client.Start(ctx); err != nil { log.Fatal(err) } defer client.Stop() cwd, _ := os.Getwd() - session, err := client.CreateSession(copilot.SessionConfig{ + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-5", - SystemMessage: copilot.SystemMessage{ + SystemMessage: &copilot.SystemMessageConfig{ Content: fmt.Sprintf(` You are analyzing pull requests for the GitHub repository: %s/%s @@ -123,12 +125,16 @@ The current working directory is: %s defer session.Destroy() // Set up event handling - session.On(func(event copilot.Event) { - switch e := event.(type) { - case copilot.AssistantMessageEvent: - fmt.Printf("\nπŸ€– %s\n\n", e.Data.Content) - case copilot.ToolExecutionStartEvent: - fmt.Printf(" βš™οΈ %s\n", e.Data.ToolName) + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "assistant.message": + if event.Data.Content != nil { + fmt.Printf("\nπŸ€– %s\n\n", *event.Data.Content) + } + case "tool.execution_start": + if event.Data.ToolName != nil { + fmt.Printf(" βš™οΈ %s\n", *event.Data.ToolName) + } } }) @@ -144,12 +150,10 @@ The current working directory is: %s Finally, summarize the PR health - average age, oldest PR, and how many might be considered stale. `, owner, repoName) - if err := session.Send(copilot.MessageOptions{Prompt: prompt}); err != nil { + if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt}); err != nil { log.Fatal(err) } - session.WaitForIdle() - // Interactive loop fmt.Println("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") fmt.Println("Examples:") @@ -173,10 +177,8 @@ The current working directory is: %s break } - if err := session.Send(copilot.MessageOptions{Prompt: input}); err != nil { + if _, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: input}); err != nil { log.Printf("Error: %v", err) } - - session.WaitForIdle() } } diff --git a/cookbook/copilot-sdk/go/recipe/ralph-loop.go b/cookbook/copilot-sdk/go/recipe/ralph-loop.go new file mode 100644 index 00000000..03d99987 --- /dev/null +++ b/cookbook/copilot-sdk/go/recipe/ralph-loop.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "strings" + + copilot "github.com/github/copilot-sdk/go" +) + +// Ralph loop: autonomous AI task loop with fresh context per iteration. +// +// Two modes: +// - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md +// - "build": reads PROMPT_build.md, implements tasks, runs tests, commits +// +// Each iteration creates a fresh session so the agent always operates in +// the "smart zone" of its context window. State is shared between +// iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*). +// +// Usage: +// go run ralph-loop.go # build mode, 50 iterations +// go run ralph-loop.go plan # planning mode +// go run ralph-loop.go 20 # build mode, 20 iterations +// go run ralph-loop.go plan 5 # planning mode, 5 iterations + +func ralphLoop(ctx context.Context, mode string, maxIterations int) error { + promptFile := "PROMPT_build.md" + if mode == "plan" { + promptFile = "PROMPT_plan.md" + } + + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + return fmt.Errorf("failed to start client: %w", err) + } + defer client.Stop() + + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + fmt.Println(strings.Repeat("━", 40)) + fmt.Printf("Mode: %s\n", mode) + fmt.Printf("Prompt: %s\n", promptFile) + fmt.Printf("Max: %d iterations\n", maxIterations) + fmt.Println(strings.Repeat("━", 40)) + + prompt, err := os.ReadFile(promptFile) + if err != nil { + return fmt.Errorf("failed to read %s: %w", promptFile, err) + } + + for i := 1; i <= maxIterations; i++ { + fmt.Printf("\n=== Iteration %d/%d ===\n", i, maxIterations) + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-5.1-codex-mini", + WorkingDirectory: cwd, + OnPermissionRequest: func(_ copilot.PermissionRequest, _ map[string]string) copilot.PermissionRequestResult { + return copilot.PermissionRequestResult{Kind: "approved"} + }, + }) + if err != nil { + return fmt.Errorf("failed to create session: %w", err) + } + + // Log tool usage for visibility + session.On(func(event copilot.Event) { + if toolExecution, ok := event.(copilot.ToolExecutionStartEvent); ok { + fmt.Printf(" βš™ %s\n", toolExecution.Data.ToolName) + } + }) + + _, err = session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: string(prompt), + }) + if destroyErr := session.Destroy(); destroyErr != nil { + log.Printf("failed to destroy session on iteration %d: %v", i, destroyErr) + } + if err != nil { + return fmt.Errorf("send failed on iteration %d: %w", i, err) + } + + fmt.Printf("\nIteration %d complete.\n", i) + } + + fmt.Printf("\nReached max iterations: %d\n", maxIterations) + return nil +} + +func main() { + mode := "build" + maxIterations := 50 + + for _, arg := range os.Args[1:] { + if arg == "plan" { + mode = "plan" + } else if n, err := strconv.Atoi(arg); err == nil { + maxIterations = n + } + } + + if err := ralphLoop(context.Background(), mode, maxIterations); err != nil { + log.Fatal(err) + } +} diff --git a/cookbook/copilot-sdk/nodejs/accessibility-report.md b/cookbook/copilot-sdk/nodejs/accessibility-report.md new file mode 100644 index 00000000..74cb7747 --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/accessibility-report.md @@ -0,0 +1,265 @@ +# 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/accessibility-report.ts](recipe/accessibility-report.ts) +> +> ```bash +> cd recipe && npm install +> npx tsx accessibility-report.ts +> # or: npm run accessibility-report +> ``` + +## 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 + +```bash +npm install @github/copilot-sdk +npm install -D typescript tsx @types/node +``` + +You also need `npx` available (Node.js installed) for the Playwright MCP server. + +## Usage + +```bash +npx tsx accessibility-report.ts +# Enter a URL when prompted +``` + +## Full example: accessibility-report.ts + +```typescript +#!/usr/bin/env npx tsx + +import { CopilotClient } from "@github/copilot-sdk"; +import * as readline from "node:readline"; + +// ============================================================================ +// Main Application +// ============================================================================ + +async function main() { + console.log("=== Accessibility Report Generator ===\n"); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const askQuestion = (query: string): Promise => + new Promise((resolve) => rl.question(query, (answer) => resolve(answer.trim()))); + + let url = await askQuestion("Enter URL to analyze: "); + + if (!url) { + console.log("No URL provided. Exiting."); + rl.close(); + return; + } + + // Ensure URL has a scheme + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + console.log(`\nAnalyzing: ${url}`); + console.log("Please wait...\n"); + + // Create Copilot client with Playwright MCP server + const client = new CopilotClient(); + + const session = await client.createSession({ + model: "claude-opus-4.6", + streaming: true, + mcpServers: { + playwright: { + type: "local", + command: "npx", + args: ["@playwright/mcp@latest"], + tools: ["*"], + }, + }, + }); + + // Set up streaming event handling + let idleResolve: (() => void) | null = null; + + session.on((event) => { + if (event.type === "assistant.message.delta") { + process.stdout.write(event.data.deltaContent ?? ""); + } else if (event.type === "session.idle") { + idleResolve?.(); + } else if (event.type === "session.error") { + console.error(`\nError: ${event.data.message}`); + idleResolve?.(); + } + }); + + const waitForIdle = (): Promise => + new Promise((resolve) => { + idleResolve = resolve; + }); + + const prompt = ` + Use the Playwright MCP server to analyze the accessibility of this webpage: ${url} + + 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. + `; + + let idle = waitForIdle(); + await session.send({ prompt }); + await idle; + + console.log("\n\n=== Report Complete ===\n"); + + // Prompt user for test generation + const generateTests = await askQuestion( + "Would you like to generate Playwright accessibility tests? (y/n): " + ); + + if (generateTests.toLowerCase() === "y" || generateTests.toLowerCase() === "yes") { + const detectLanguagePrompt = ` + Analyze the current working directory to detect the primary programming language. + Respond with ONLY the detected language name and a brief explanation. + If no project is detected, suggest "TypeScript" as the default. + `; + + console.log("\nDetecting project language...\n"); + idle = waitForIdle(); + await session.send({ prompt: detectLanguagePrompt }); + await idle; + + let language = await askQuestion("\n\nConfirm language for tests (or enter a different one): "); + if (!language) language = "TypeScript"; + + const testGenerationPrompt = ` + Based on the accessibility report you just generated for ${url}, + create Playwright accessibility tests in ${language}. + + 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. + `; + + console.log("\nGenerating accessibility tests...\n"); + idle = waitForIdle(); + await session.send({ prompt: testGenerationPrompt }); + await idle; + + console.log("\n\n=== Tests Generated ==="); + } + + rl.close(); + await session.destroy(); + await client.stop(); +} + +main().catch(console.error); +``` + +## 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 `assistant.message.delta` events 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 detects the project language and generates Playwright accessibility tests + +## Key concepts + +### MCP server configuration + +The recipe configures a local MCP server that runs alongside the session: + +```typescript +const session = await client.createSession({ + mcpServers: { + playwright: { + type: "local", + command: "npx", + args: ["@playwright/mcp@latest"], + tools: ["*"], + }, + }, +}); +``` + +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: + +```typescript +session.on((event) => { + if (event.type === "assistant.message.delta") { + process.stdout.write(event.data.deltaContent ?? ""); + } else if (event.type === "session.idle") { + idleResolve?.(); + } +}); +``` + +## 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 + +Detecting project language... +TypeScript detected (package.json found) + +Confirm language for tests (or enter a different one): + +Generating accessibility tests... +[Generated test file output...] + +=== Tests Generated === +``` diff --git a/cookbook/copilot-sdk/nodejs/ralph-loop.md b/cookbook/copilot-sdk/nodejs/ralph-loop.md new file mode 100644 index 00000000..87c5225f --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/ralph-loop.md @@ -0,0 +1,238 @@ +# 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/ralph-loop.ts](recipe/ralph-loop.ts) +> +> ```bash +> npm install +> npx tsx recipe/ralph-loop.ts +> ``` + +## 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`: + +```typescript +import { readFile } from "fs/promises"; +import { CopilotClient } from "@github/copilot-sdk"; + +async function ralphLoop(promptFile: string, maxIterations: number = 50) { + const client = new CopilotClient(); + await client.start(); + + try { + const prompt = await readFile(promptFile, "utf-8"); + + for (let i = 1; i <= maxIterations; i++) { + console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + + // Fresh session each iteration β€” context isolation is the point + const session = await client.createSession({ model: "gpt-5.1-codex-mini" }); + try { + await session.sendAndWait({ prompt }, 600_000); + } finally { + await session.destroy(); + } + + console.log(`Iteration ${i} complete.`); + } + } finally { + await client.stop(); + } +} + +// Usage: point at your PROMPT.md +ralphLoop("PROMPT.md", 20); +``` + +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: + +```typescript +import { readFile } from "fs/promises"; +import { CopilotClient } from "@github/copilot-sdk"; + +type Mode = "plan" | "build"; + +async function ralphLoop(mode: Mode, maxIterations: number = 50) { + const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; + const client = new CopilotClient(); + await client.start(); + + console.log(`Mode: ${mode} | Prompt: ${promptFile}`); + + try { + const prompt = await readFile(promptFile, "utf-8"); + + for (let i = 1; i <= maxIterations; i++) { + console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + + const session = await client.createSession({ + model: "gpt-5.1-codex-mini", + // Pin the agent to the project directory + workingDirectory: process.cwd(), + // Auto-approve tool calls for unattended operation + onPermissionRequest: async () => ({ allow: true }), + }); + + // Log tool usage for visibility + session.on((event) => { + if (event.type === "tool.execution_start") { + console.log(` βš™ ${event.data.toolName}`); + } + }); + + try { + await session.sendAndWait({ prompt }, 600_000); + } finally { + await session.destroy(); + } + + console.log(`Iteration ${i} complete.`); + } + } finally { + await client.stop(); + } +} + +// Parse CLI args: npx tsx ralph-loop.ts [plan] [max_iterations] +const args = process.argv.slice(2); +const mode: Mode = args.includes("plan") ? "plan" : "build"; +const maxArg = args.find((a) => /^\d+$/.test(a)); +const maxIterations = maxArg ? parseInt(maxArg) : 50; + +ralphLoop(mode, maxIterations); +``` + +### 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 + +npm run build + +## Validation + +- Tests: `npm test` +- Typecheck: `npx tsc --noEmit` +- Lint: `npm run lint` +``` + +## 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 `onPermissionRequest` 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/nodejs/recipe/accessibility-report.ts b/cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts new file mode 100644 index 00000000..a096726e --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/accessibility-report.ts @@ -0,0 +1,187 @@ +#!/usr/bin/env tsx + +import { CopilotClient } from "@github/copilot-sdk"; +import * as readline from "node:readline"; + +// ============================================================================ +// Main Application +// ============================================================================ + +async function main() { + console.log("=== Accessibility Report Generator ===\n"); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const askQuestion = (query: string): Promise => + new Promise((resolve) => rl.question(query, (answer) => resolve(answer.trim()))); + + let url = await askQuestion("Enter URL to analyze: "); + + if (!url) { + console.log("No URL provided. Exiting."); + rl.close(); + return; + } + + // Ensure URL has a scheme + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://" + url; + } + + console.log(`\nAnalyzing: ${url}`); + console.log("Please wait...\n"); + + // Create Copilot client with Playwright MCP server + const client = new CopilotClient(); + + const session = await client.createSession({ + model: "claude-opus-4.6", + streaming: true, + mcpServers: { + playwright: { + type: "local", + command: "npx", + args: ["@playwright/mcp@latest"], + tools: ["*"], + }, + }, + }); + + // Set up streaming event handling + let idleResolve: (() => void) | null = null; + + session.on((event) => { + if (event.type === "assistant.message.delta") { + process.stdout.write(event.data.deltaContent ?? ""); + } else if (event.type === "session.idle") { + idleResolve?.(); + } else if (event.type === "session.error") { + console.error(`\nError: ${event.data.message}`); + idleResolve?.(); + } + }); + + const waitForIdle = (): Promise => + new Promise((resolve) => { + idleResolve = resolve; + }); + + const prompt = ` + Use the Playwright MCP server to analyze the accessibility of this webpage: ${url} + + 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 EXACTLY like this structure with emoji indicators: + + πŸ“Š Accessibility Report: [Page Title] (domain.com) + + βœ… What's Working Well + | Category | Status | Details | + |----------|--------|---------| + | Language | βœ… Pass | lang="en-US" properly set | + | Page Title | βœ… Pass | "[Title]" is descriptive | + | Heading Hierarchy | βœ… Pass | Single H1, proper H2/H3 structure | + | Images | βœ… Pass | All X images have alt text | + | Viewport | βœ… Pass | Allows pinch-zoom (no user-scalable=no) | + | Links | βœ… Pass | No ambiguous "click here" links | + | Reduced Motion | βœ… Pass | Supports prefers-reduced-motion | + | Autoplay Media | βœ… Pass | No autoplay audio/video | + + ⚠️ Issues Found + | Severity | Issue | WCAG Criterion | Recommendation | + |----------|-------|----------------|----------------| + | πŸ”΄ High | No
landmark | 1.3.1, 2.4.1 | Wrap main content in
element | + | πŸ”΄ High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top | + | 🟑 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist | + | 🟑 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) | + + πŸ“‹ Stats Summary + - Total Links: X + - Total Headings: X (1Γ— H1, proper hierarchy) + - Focusable Elements: X + - Landmarks Found: banner βœ…, navigation βœ…, main ❌, footer βœ… + + βš™οΈ Priority Recommendations + - Add
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - Increase touch targets - Add padding to nav links and tags to meet 44Γ—44px minimum + - Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis - don't just copy the example. + `; + + let idle = waitForIdle(); + await session.send({ prompt }); + await idle; + + console.log("\n\n=== Report Complete ===\n"); + + // Prompt user for test generation + const generateTests = await askQuestion( + "Would you like to generate Playwright accessibility tests? (y/n): " + ); + + if (generateTests.toLowerCase() === "y" || generateTests.toLowerCase() === "yes") { + const detectLanguagePrompt = ` + Analyze the current working directory to detect the primary programming language used in this project. + Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc. + + Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java") + and a brief explanation of why you detected it. + If no project is detected, suggest "TypeScript" as the default for Playwright tests. + `; + + console.log("\nDetecting project language...\n"); + idle = waitForIdle(); + await session.send({ prompt: detectLanguagePrompt }); + await idle; + + let language = await askQuestion("\n\nConfirm language for tests (or enter a different one): "); + if (!language) { + language = "TypeScript"; + } + + const testGenerationPrompt = ` + Based on the accessibility report you just generated for ${url}, create Playwright accessibility tests in ${language}. + + The tests should: + 1. Verify all the accessibility checks from the report + 2. Test for the issues that were found (to ensure they get fixed) + 3. Include tests for: + - Page has proper lang attribute + - Page has descriptive title + - Heading hierarchy is correct (single H1, proper nesting) + - All images have alt text + - No autoplay media + - Landmark regions exist (banner, nav, main, footer) + - Skip navigation link exists and works + - Focus indicators are visible + - Touch targets meet minimum size requirements + 4. Use Playwright's accessibility testing features + 5. Include helpful comments explaining each test + + Output the complete test file that can be saved and run. + Use the Playwright MCP server tools if you need to verify any page details. + `; + + console.log("\nGenerating accessibility tests...\n"); + idle = waitForIdle(); + await session.send({ prompt: testGenerationPrompt }); + await idle; + + console.log("\n\n=== Tests Generated ==="); + } + + rl.close(); + await session.destroy(); + await client.stop(); +} + +main().catch(console.error); diff --git a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json index a5a8fea5..47e85e5a 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/package-lock.json +++ b/cookbook/copilot-sdk/nodejs/recipe/package-lock.json @@ -8,7 +8,7 @@ "name": "copilot-sdk-cookbook-recipes", "version": "1.0.0", "dependencies": { - "@github/copilot-sdk": "file:../../src" + "@github/copilot-sdk": "*" }, "devDependencies": { "@types/node": "^22.19.7", diff --git a/cookbook/copilot-sdk/nodejs/recipe/package.json b/cookbook/copilot-sdk/nodejs/recipe/package.json index 53584216..c8ee65a2 100644 --- a/cookbook/copilot-sdk/nodejs/recipe/package.json +++ b/cookbook/copilot-sdk/nodejs/recipe/package.json @@ -8,7 +8,8 @@ "multiple-sessions": "tsx multiple-sessions.ts", "managing-local-files": "tsx managing-local-files.ts", "pr-visualization": "tsx pr-visualization.ts", - "persisting-sessions": "tsx persisting-sessions.ts" + "persisting-sessions": "tsx persisting-sessions.ts", + "accessibility-report": "tsx accessibility-report.ts" }, "dependencies": { "@github/copilot-sdk": "*" diff --git a/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts new file mode 100644 index 00000000..fb0fbe45 --- /dev/null +++ b/cookbook/copilot-sdk/nodejs/recipe/ralph-loop.ts @@ -0,0 +1,78 @@ +import { readFile } from "fs/promises"; +import { CopilotClient } from "@github/copilot-sdk"; + +/** + * Ralph loop: autonomous AI task loop with fresh context per iteration. + * + * Two modes: + * - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md + * - "build": reads PROMPT_build.md, implements tasks, runs tests, commits + * + * Each iteration creates a fresh session so the agent always operates in + * the "smart zone" of its context window. State is shared between + * iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*). + * + * Usage: + * npx tsx ralph-loop.ts # build mode, 50 iterations + * npx tsx ralph-loop.ts plan # planning mode + * npx tsx ralph-loop.ts 20 # build mode, 20 iterations + * npx tsx ralph-loop.ts plan 5 # planning mode, 5 iterations + */ + +type Mode = "plan" | "build"; + +async function ralphLoop(mode: Mode, maxIterations: number) { + const promptFile = mode === "plan" ? "PROMPT_plan.md" : "PROMPT_build.md"; + + const client = new CopilotClient(); + await client.start(); + + console.log("━".repeat(40)); + console.log(`Mode: ${mode}`); + console.log(`Prompt: ${promptFile}`); + console.log(`Max: ${maxIterations} iterations`); + console.log("━".repeat(40)); + + try { + const prompt = await readFile(promptFile, "utf-8"); + + for (let i = 1; i <= maxIterations; i++) { + console.log(`\n=== Iteration ${i}/${maxIterations} ===`); + + const session = await client.createSession({ + model: "gpt-5.1-codex-mini", + // Pin the agent to the project directory + workingDirectory: process.cwd(), + // Auto-approve tool calls for unattended operation + onPermissionRequest: async () => ({ allow: true }), + }); + + // Log tool usage for visibility + session.on((event) => { + if (event.type === "tool.execution_start") { + console.log(` βš™ ${event.data.toolName}`); + } + }); + + try { + await session.sendAndWait({ prompt }, 600_000); + } finally { + await session.destroy(); + } + + console.log(`\nIteration ${i} complete.`); + } + + console.log(`\nReached max iterations: ${maxIterations}`); + } finally { + await client.stop(); + } +} + +// Parse CLI args +const args = process.argv.slice(2); +const mode: Mode = args.includes("plan") ? "plan" : "build"; +const maxArg = args.find((a) => /^\d+$/.test(a)); +const maxIterations = maxArg ? parseInt(maxArg) : 50; + +ralphLoop(mode, maxIterations).catch(console.error); diff --git a/cookbook/copilot-sdk/python/accessibility-report.md b/cookbook/copilot-sdk/python/accessibility-report.md new file mode 100644 index 00000000..3d67a5fa --- /dev/null +++ b/cookbook/copilot-sdk/python/accessibility-report.md @@ -0,0 +1,253 @@ +# 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/accessibility_report.py](recipe/accessibility_report.py) +> +> ```bash +> cd recipe && pip install -r requirements.txt +> python accessibility_report.py +> ``` + +## 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 + +```bash +pip install github-copilot-sdk +``` + +You also need `npx` available (Node.js installed) for the Playwright MCP server. + +## Usage + +```bash +python accessibility_report.py +# Enter a URL when prompted +``` + +## Full example: accessibility_report.py + +```python +#!/usr/bin/env python3 + +import asyncio +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) + +# ============================================================================ +# Main Application +# ============================================================================ + +async def main(): + print("=== Accessibility Report Generator ===\n") + + url = input("Enter URL to analyze: ").strip() + + if not url: + print("No URL provided. Exiting.") + return + + # Ensure URL has a scheme + if not url.startswith("http://") and not url.startswith("https://"): + url = "https://" + url + + print(f"\nAnalyzing: {url}") + print("Please wait...\n") + + # Create Copilot client with Playwright MCP server + client = CopilotClient() + await client.start() + + session = await client.create_session(SessionConfig( + model="claude-opus-4.6", + streaming=True, + mcp_servers={ + "playwright": { + "type": "local", + "command": "npx", + "args": ["@playwright/mcp@latest"], + "tools": ["*"], + } + }, + )) + + done = asyncio.Event() + + # Set up streaming event handling + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content or "", end="", flush=True) + elif event.type.value == "session.idle": + done.set() + elif event.type.value == "session.error": + print(f"\nError: {event.data.message}") + done.set() + + session.on(handle_event) + + prompt = f""" + Use the Playwright MCP server to analyze the accessibility of this webpage: {url} + + 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. + """ + + await session.send(MessageOptions(prompt=prompt)) + await done.wait() + + print("\n\n=== Report Complete ===\n") + + # Prompt user for test generation + generate_tests = input( + "Would you like to generate Playwright accessibility tests? (y/n): " + ).strip().lower() + + if generate_tests in ("y", "yes"): + done.clear() + + detect_language_prompt = """ + Analyze the current working directory to detect the primary programming language. + Respond with ONLY the detected language name and a brief explanation. + If no project is detected, suggest "TypeScript" as the default. + """ + + print("\nDetecting project language...\n") + await session.send(MessageOptions(prompt=detect_language_prompt)) + await done.wait() + + language = input( + "\n\nConfirm language for tests (or enter a different one): " + ).strip() + if not language: + language = "TypeScript" + + done.clear() + + test_generation_prompt = f""" + Based on the accessibility report you just generated for {url}, + create Playwright accessibility tests in {language}. + + 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. + """ + + print("\nGenerating accessibility tests...\n") + await session.send(MessageOptions(prompt=test_generation_prompt)) + await done.wait() + + print("\n\n=== Tests Generated ===") + + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## 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 `ASSISTANT_MESSAGE_DELTA` events 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 detects the project language and generates Playwright accessibility tests + +## Key concepts + +### MCP server configuration + +The recipe configures a local MCP server that runs alongside the session: + +```python +session = await client.create_session(SessionConfig( + mcp_servers={ + "playwright": { + "type": "local", + "command": "npx", + "args": ["@playwright/mcp@latest"], + "tools": ["*"], + } + }, +)) +``` + +This gives the model access to Playwright browser tools like `browser_navigate`, `browser_snapshot`, and `browser_click`. + +### Streaming with events + +Unlike `send_and_wait`, this recipe uses streaming for real-time output: + +```python +def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content or "", end="", flush=True) + elif event.type.value == "session.idle": + done.set() + +session.on(handle_event) +``` + +## 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 + +Detecting project language... +TypeScript detected (package.json found) + +Confirm language for tests (or enter a different one): + +Generating accessibility tests... +[Generated test file output...] + +=== Tests Generated === +``` diff --git a/cookbook/copilot-sdk/python/error-handling.md b/cookbook/copilot-sdk/python/error-handling.md index cdd73cbc..dfdd02b9 100644 --- a/cookbook/copilot-sdk/python/error-handling.md +++ b/cookbook/copilot-sdk/python/error-handling.md @@ -16,41 +16,36 @@ You need to handle various error conditions like connection failures, timeouts, ## Basic try-except ```python -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() +async def main(): + client = CopilotClient() -try: - client.start() - session = client.create_session(model="gpt-5") + try: + await client.start() + session = await client.create_session(SessionConfig(model="gpt-5")) - response = None - def handle_message(event): - nonlocal response - if event["type"] == "assistant.message": - response = event["data"]["content"] + response = await session.send_and_wait(MessageOptions(prompt="Hello!")) - session.on(handle_message) - session.send(prompt="Hello!") - session.wait_for_idle() + if response: + print(response.data.content) - if response: - print(response) + await session.destroy() + except Exception as e: + print(f"Error: {e}") + finally: + await client.stop() - session.destroy() -except Exception as e: - print(f"Error: {e}") -finally: - client.stop() +if __name__ == "__main__": + asyncio.run(main()) ``` ## Handling specific error types ```python -import subprocess - try: - client.start() + await client.start() except FileNotFoundError: print("Copilot CLI not found. Please install it first.") except ConnectionError: @@ -62,31 +57,14 @@ except Exception as e: ## Timeout handling ```python -import signal -from contextlib import contextmanager - -@contextmanager -def timeout(seconds): - def timeout_handler(signum, frame): - raise TimeoutError("Request timed out") - - old_handler = signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(seconds) - try: - yield - finally: - signal.alarm(0) - signal.signal(signal.SIGALRM, old_handler) - -session = client.create_session(model="gpt-5") +session = await client.create_session(SessionConfig(model="gpt-5")) try: - session.send(prompt="Complex question...") - - # Wait with timeout (30 seconds) - with timeout(30): - session.wait_for_idle() - + # send_and_wait accepts an optional timeout in seconds + response = await session.send_and_wait( + MessageOptions(prompt="Complex question..."), + timeout=30.0 + ) print("Response received") except TimeoutError: print("Request timed out") @@ -95,21 +73,15 @@ except TimeoutError: ## Aborting a request ```python -import threading +session = await client.create_session(SessionConfig(model="gpt-5")) -session = client.create_session(model="gpt-5") - -# Start a request -session.send(prompt="Write a very long story...") +# Start a request (non-blocking send) +await session.send(MessageOptions(prompt="Write a very long story...")) # Abort it after some condition -def abort_later(): - import time - time.sleep(5) - session.abort() - print("Request aborted") - -threading.Thread(target=abort_later).start() +await asyncio.sleep(5) +await session.abort() +print("Request aborted") ``` ## Graceful shutdown @@ -120,31 +92,19 @@ import sys def signal_handler(sig, frame): print("\nShutting down...") - errors = client.stop() - if errors: - print(f"Cleanup errors: {errors}") + try: + loop = asyncio.get_running_loop() + loop.create_task(client.stop()) + except RuntimeError: + asyncio.run(client.stop()) sys.exit(0) signal.signal(signal.SIGINT, signal_handler) ``` -## Context manager for automatic cleanup - -```python -from copilot import CopilotClient - -with CopilotClient() as client: - client.start() - session = client.create_session(model="gpt-5") - - # ... do work ... - - # client.stop() is automatically called when exiting context -``` - ## Best practices -1. **Always clean up**: Use try-finally or context managers to ensure `stop()` is called +1. **Always clean up**: Use try-finally to ensure `await client.stop()` is called 2. **Handle connection errors**: The CLI might not be installed or running -3. **Set appropriate timeouts**: Long-running requests should have timeouts +3. **Set appropriate timeouts**: Use the `timeout` parameter on `send_and_wait()` 4. **Log errors**: Capture error details for debugging diff --git a/cookbook/copilot-sdk/python/managing-local-files.md b/cookbook/copilot-sdk/python/managing-local-files.md index c81a831e..a9e4e35f 100644 --- a/cookbook/copilot-sdk/python/managing-local-files.md +++ b/cookbook/copilot-sdk/python/managing-local-files.md @@ -16,31 +16,40 @@ You have a folder with many files and want to organize them into subfolders base ## Example code ```python -from copilot import CopilotClient +import asyncio import os +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) -# Create and start client -client = CopilotClient() -client.start() +async def main(): + # Create and start client + client = CopilotClient() + await client.start() -# Create session -session = client.create_session(model="gpt-5") + # Create session + session = await client.create_session(SessionConfig(model="gpt-5")) -# Event handler -def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nCopilot: {event['data']['content']}") - elif event["type"] == "tool.execution_start": - print(f" β†’ Running: {event['data']['toolName']}") - elif event["type"] == "tool.execution_complete": - print(f" βœ“ Completed: {event['data']['toolCallId']}") + done = asyncio.Event() -session.on(handle_event) + # Event handler + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nCopilot: {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" β†’ Running: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed: {event.data.tool_call_id}") + elif event.type.value == "session.idle": + done.set() -# Ask Copilot to organize files -target_folder = os.path.expanduser("~/Downloads") + session.on(handle_event) -session.send(prompt=f""" + # Ask Copilot to organize files + target_folder = os.path.expanduser("~/Downloads") + + await session.send(MessageOptions(prompt=f""" Analyze the files in "{target_folder}" and organize them into subfolders. 1. First, list all files and their metadata @@ -49,11 +58,15 @@ Analyze the files in "{target_folder}" and organize them into subfolders. 4. Move each file to its appropriate subfolder Please confirm before moving any files. -""") +""")) -session.wait_for_idle() + await done.wait() -client.stop() + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) ``` ## Grouping strategies @@ -90,10 +103,10 @@ client.stop() For safety, you can ask Copilot to only preview changes: ```python -session.send(prompt=f""" +await session.send(MessageOptions(prompt=f""" Analyze files in "{target_folder}" and show me how you would organize them by file type. DO NOT move any files - just show me the plan. -""") +""")) ``` ## Custom grouping with AI analysis @@ -101,7 +114,7 @@ by file type. DO NOT move any files - just show me the plan. Let Copilot determine the best grouping based on file content: ```python -session.send(prompt=f""" +await session.send(MessageOptions(prompt=f""" Look at the files in "{target_folder}" and suggest a logical organization. Consider: - File names and what they might contain @@ -109,7 +122,7 @@ Consider: - Date patterns that might indicate projects or events Propose folder names that are descriptive and useful. -""") +""")) ``` ## Safety considerations diff --git a/cookbook/copilot-sdk/python/multiple-sessions.md b/cookbook/copilot-sdk/python/multiple-sessions.md index 4baa0f47..0efa3ed8 100644 --- a/cookbook/copilot-sdk/python/multiple-sessions.md +++ b/cookbook/copilot-sdk/python/multiple-sessions.md @@ -16,31 +16,36 @@ You need to run multiple conversations in parallel, each with its own context an ## Python ```python -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() -# Create multiple independent sessions -session1 = client.create_session(model="gpt-5") -session2 = client.create_session(model="gpt-5") -session3 = client.create_session(model="claude-sonnet-4.5") + # Create multiple independent sessions + session1 = await client.create_session(SessionConfig(model="gpt-5")) + session2 = await client.create_session(SessionConfig(model="gpt-5")) + session3 = await client.create_session(SessionConfig(model="claude-sonnet-4.5")) -# Each session maintains its own conversation history -session1.send(prompt="You are helping with a Python project") -session2.send(prompt="You are helping with a TypeScript project") -session3.send(prompt="You are helping with a Go project") + # Each session maintains its own conversation history + await session1.send(MessageOptions(prompt="You are helping with a Python project")) + await session2.send(MessageOptions(prompt="You are helping with a TypeScript project")) + await session3.send(MessageOptions(prompt="You are helping with a Go project")) -# Follow-up messages stay in their respective contexts -session1.send(prompt="How do I create a virtual environment?") -session2.send(prompt="How do I set up tsconfig?") -session3.send(prompt="How do I initialize a module?") + # Follow-up messages stay in their respective contexts + await session1.send(MessageOptions(prompt="How do I create a virtual environment?")) + await session2.send(MessageOptions(prompt="How do I set up tsconfig?")) + await session3.send(MessageOptions(prompt="How do I initialize a module?")) -# Clean up all sessions -session1.destroy() -session2.destroy() -session3.destroy() -client.stop() + # Clean up all sessions + await session1.destroy() + await session2.destroy() + await session3.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) ``` ## Custom session IDs @@ -48,10 +53,10 @@ client.stop() Use custom IDs for easier tracking: ```python -session = client.create_session( +session = await client.create_session(SessionConfig( session_id="user-123-chat", model="gpt-5" -) +)) print(session.session_id) # "user-123-chat" ``` @@ -59,16 +64,16 @@ print(session.session_id) # "user-123-chat" ## Listing sessions ```python -sessions = client.list_sessions() +sessions = await client.list_sessions() for session_info in sessions: - print(f"Session: {session_info['sessionId']}") + print(f"Session: {session_info.session_id}") ``` ## Deleting sessions ```python # Delete a specific session -client.delete_session("user-123-chat") +await client.delete_session("user-123-chat") ``` ## Use cases diff --git a/cookbook/copilot-sdk/python/persisting-sessions.md b/cookbook/copilot-sdk/python/persisting-sessions.md index 5d07a469..cc77407c 100644 --- a/cookbook/copilot-sdk/python/persisting-sessions.md +++ b/cookbook/copilot-sdk/python/persisting-sessions.md @@ -16,64 +16,69 @@ You want users to be able to continue a conversation even after closing and reop ### Creating a session with a custom ID ```python -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() -# Create session with a memorable ID -session = client.create_session( - session_id="user-123-conversation", - model="gpt-5", -) + # Create session with a memorable ID + session = await client.create_session(SessionConfig( + session_id="user-123-conversation", + model="gpt-5", + )) -session.send(prompt="Let's discuss TypeScript generics") + await session.send_and_wait(MessageOptions(prompt="Let's discuss TypeScript generics")) -# Session ID is preserved -print(session.session_id) # "user-123-conversation" + # Session ID is preserved + print(session.session_id) # "user-123-conversation" -# Destroy session but keep data on disk -session.destroy() -client.stop() + # Destroy session but keep data on disk + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) ``` ### Resuming a session ```python client = CopilotClient() -client.start() +await client.start() # Resume the previous session -session = client.resume_session("user-123-conversation") +session = await client.resume_session("user-123-conversation") # Previous context is restored -session.send(prompt="What were we discussing?") +await session.send_and_wait(MessageOptions(prompt="What were we discussing?")) -session.destroy() -client.stop() +await session.destroy() +await client.stop() ``` ### Listing available sessions ```python -sessions = client.list_sessions() +sessions = await client.list_sessions() for s in sessions: - print("Session:", s["sessionId"]) + print("Session:", s.session_id) ``` ### Deleting a session permanently ```python # Remove session and all its data from disk -client.delete_session("user-123-conversation") +await client.delete_session("user-123-conversation") ``` ### Getting session history ```python -messages = session.get_messages() +messages = await session.get_messages() for msg in messages: - print(f"[{msg['type']}] {msg['data']}") + print(f"[{msg.type}] {msg.data.content}") ``` ## Best practices diff --git a/cookbook/copilot-sdk/python/pr-visualization.md b/cookbook/copilot-sdk/python/pr-visualization.md index 0419aed1..0b158e4a 100644 --- a/cookbook/copilot-sdk/python/pr-visualization.md +++ b/cookbook/copilot-sdk/python/pr-visualization.md @@ -38,10 +38,15 @@ python pr_visualization.py --repo github/copilot-sdk ```python #!/usr/bin/env python3 +import asyncio import subprocess import sys import os -from copilot import CopilotClient +import re +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) # ============================================================================ # Git & GitHub Detection @@ -69,7 +74,6 @@ def get_github_remote(): remote_url = result.stdout.strip() # Handle SSH: git@github.com:owner/repo.git - import re ssh_match = re.search(r"git@github\.com:(.+/.+?)(?:\.git)?$", remote_url) if ssh_match: return ssh_match.group(1) @@ -98,7 +102,7 @@ def prompt_for_repo(): # Main Application # ============================================================================ -def main(): +async def main(): print("πŸ” PR Age Chart Generator\n") # Determine the repository @@ -126,11 +130,11 @@ def main(): owner, repo_name = repo.split("/", 1) - # Create Copilot client - no custom tools needed! - client = CopilotClient(log_level="error") - client.start() + # Create Copilot client + client = CopilotClient() + await client.start() - session = client.create_session( + session = await client.create_session(SessionConfig( model="gpt-5", system_message={ "content": f""" @@ -147,30 +151,34 @@ The current working directory is: {os.getcwd()} """ } - ) + )) + + done = asyncio.Event() # Set up event handling - def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nπŸ€– {event['data']['content']}\n") - elif event["type"] == "tool.execution_start": - print(f" βš™οΈ {event['data']['toolName']}") + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}\n") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ {event.data.tool_name}") + elif event.type.value == "session.idle": + done.set() session.on(handle_event) # Initial prompt - let Copilot figure out the details print("\nπŸ“Š Starting analysis...\n") - session.send(prompt=f""" + await session.send(MessageOptions(prompt=f""" Fetch the open pull requests for {owner}/{repo_name} 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. - """) + """)) - session.wait_for_idle() + await done.wait() # Interactive loop print("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") @@ -189,14 +197,15 @@ The current working directory is: {os.getcwd()} break if user_input: - session.send(prompt=user_input) - session.wait_for_idle() + done.clear() + await session.send(MessageOptions(prompt=user_input)) + await done.wait() - session.destroy() - client.stop() + await session.destroy() + await client.stop() if __name__ == "__main__": - main() + asyncio.run(main()) ``` ## How it works diff --git a/cookbook/copilot-sdk/python/ralph-loop.md b/cookbook/copilot-sdk/python/ralph-loop.md new file mode 100644 index 00000000..b0d1c4b6 --- /dev/null +++ b/cookbook/copilot-sdk/python/ralph-loop.md @@ -0,0 +1,252 @@ +# 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/ralph_loop.py](recipe/ralph_loop.py) +> +> From the repository root, install dependencies and run: +> +> ```bash +> pip install -r cookbook/copilot-sdk/python/recipe/requirements.txt +> python cookbook/copilot-sdk/python/recipe/ralph_loop.py +> ``` +> +> Make sure `PROMPT_build.md` and `PROMPT_plan.md` exist in your current working directory before running the loop. + +## 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`: + +```python +import asyncio +from pathlib import Path +from copilot import CopilotClient, MessageOptions, SessionConfig + + +async def ralph_loop(prompt_file: str, max_iterations: int = 50): + client = CopilotClient() + await client.start() + + try: + prompt = Path(prompt_file).read_text() + + for i in range(1, max_iterations + 1): + print(f"\n=== Iteration {i}/{max_iterations} ===") + + # Fresh session each iteration β€” context isolation is the point + session = await client.create_session( + SessionConfig(model="gpt-5.1-codex-mini") + ) + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 + ) + finally: + await session.destroy() + + print(f"Iteration {i} complete.") + finally: + await client.stop() + + +# Usage: point at your PROMPT.md +asyncio.run(ralph_loop("PROMPT.md", 20)) +``` + +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: + +```python +import asyncio +import sys +from pathlib import Path + +from copilot import CopilotClient, MessageOptions, SessionConfig + + +async def ralph_loop(mode: str = "build", max_iterations: int = 50): + prompt_file = "PROMPT_plan.md" if mode == "plan" else "PROMPT_build.md" + client = CopilotClient() + await client.start() + + print("━" * 40) + print(f"Mode: {mode}") + print(f"Prompt: {prompt_file}") + print(f"Max: {max_iterations} iterations") + print("━" * 40) + + try: + prompt = Path(prompt_file).read_text() + + for i in range(1, max_iterations + 1): + print(f"\n=== Iteration {i}/{max_iterations} ===") + + session = await client.create_session(SessionConfig( + model="gpt-5.1-codex-mini", + # Pin the agent to the project directory + working_directory=str(Path.cwd()), + # Auto-approve tool calls for unattended operation + on_permission_request=lambda _req, _ctx: { + "kind": "approved", "rules": [] + }, + )) + + # Log tool usage for visibility + def log_tool_event(event): + if event.type.value == "tool.execution_start": + print(f" βš™ {event.data.tool_name}") + + session.on(log_tool_event) + + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 + ) + finally: + await session.destroy() + + print(f"\nIteration {i} complete.") + + print(f"\nReached max iterations: {max_iterations}") + finally: + await client.stop() + + +if __name__ == "__main__": + args = sys.argv[1:] + mode = "plan" if "plan" in args else "build" + max_iter = next((int(a) for a in args if a.isdigit()), 50) + asyncio.run(ralph_loop(mode, max_iter)) +``` + +### 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 + +python -m pytest + +## Validation + +- Tests: `pytest` +- Typecheck: `mypy src/` +- Lint: `ruff check src/` +``` + +## 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 `working_directory`**: Pin the session to your project root so tool operations resolve paths correctly +10. **Auto-approve permissions**: Use `on_permission_request` 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/python/recipe/accessibility_report.py b/cookbook/copilot-sdk/python/recipe/accessibility_report.py new file mode 100644 index 00000000..c5e0b6c9 --- /dev/null +++ b/cookbook/copilot-sdk/python/recipe/accessibility_report.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +import asyncio +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) + +# ============================================================================ +# Main Application +# ============================================================================ + +async def main(): + print("=== Accessibility Report Generator ===\n") + + url = input("Enter URL to analyze: ").strip() + + if not url: + print("No URL provided. Exiting.") + return + + # Ensure URL has a scheme + if not url.startswith("http://") and not url.startswith("https://"): + url = "https://" + url + + print(f"\nAnalyzing: {url}") + print("Please wait...\n") + + # Create Copilot client with Playwright MCP server + client = CopilotClient() + await client.start() + + session = await client.create_session(SessionConfig( + model="claude-opus-4.6", + streaming=True, + mcp_servers={ + "playwright": { + "type": "local", + "command": "npx", + "args": ["@playwright/mcp@latest"], + "tools": ["*"], + } + }, + )) + + done = asyncio.Event() + + # Set up streaming event handling + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content or "", end="", flush=True) + elif event.type.value == "session.idle": + done.set() + elif event.type.value == "session.error": + print(f"\nError: {event.data.message}") + done.set() + + session.on(handle_event) + + prompt = f""" + Use the Playwright MCP server to analyze the accessibility of this webpage: {url} + + 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 EXACTLY like this structure with emoji indicators: + + πŸ“Š Accessibility Report: [Page Title] (domain.com) + + βœ… What's Working Well + | Category | Status | Details | + |----------|--------|---------| + | Language | βœ… Pass | lang="en-US" properly set | + | Page Title | βœ… Pass | "[Title]" is descriptive | + | Heading Hierarchy | βœ… Pass | Single H1, proper H2/H3 structure | + | Images | βœ… Pass | All X images have alt text | + | Viewport | βœ… Pass | Allows pinch-zoom (no user-scalable=no) | + | Links | βœ… Pass | No ambiguous "click here" links | + | Reduced Motion | βœ… Pass | Supports prefers-reduced-motion | + | Autoplay Media | βœ… Pass | No autoplay audio/video | + + ⚠️ Issues Found + | Severity | Issue | WCAG Criterion | Recommendation | + |----------|-------|----------------|----------------| + | πŸ”΄ High | No
landmark | 1.3.1, 2.4.1 | Wrap main content in
element | + | πŸ”΄ High | No skip navigation link | 2.4.1 | Add "Skip to content" link at top | + | 🟑 Medium | Focus outlines disabled | 2.4.7 | Default outline is none - ensure visible :focus styles exist | + | 🟑 Medium | Small touch targets | 2.5.8 | Navigation links are 37px tall (below 44px minimum) | + + πŸ“‹ Stats Summary + - Total Links: X + - Total Headings: X (1Γ— H1, proper hierarchy) + - Focusable Elements: X + - Landmarks Found: banner βœ…, navigation βœ…, main ❌, footer βœ… + + βš™οΈ Priority Recommendations + - Add
landmark - Wrap page content in
for screen reader navigation + - Add skip link - Hidden link at start: + - Increase touch targets - Add padding to nav links and tags to meet 44Γ—44px minimum + - Verify focus styles - Test keyboard navigation; add visible :focus or :focus-visible outlines + + Use βœ… for pass, πŸ”΄ for high severity issues, 🟑 for medium severity, ❌ for missing items. + Include actual findings from the page analysis - don't just copy the example. + """ + + await session.send(MessageOptions(prompt=prompt)) + await done.wait() + + print("\n\n=== Report Complete ===\n") + + # Prompt user for test generation + generate_tests = input("Would you like to generate Playwright accessibility tests? (y/n): ").strip().lower() + + if generate_tests in ("y", "yes"): + done.clear() + + detect_language_prompt = """ + Analyze the current working directory to detect the primary programming language used in this project. + Look for project files like package.json, *.csproj, pom.xml, requirements.txt, go.mod, etc. + + Respond with ONLY the detected language name (e.g., "TypeScript", "JavaScript", "C#", "Python", "Java") + and a brief explanation of why you detected it. + If no project is detected, suggest "TypeScript" as the default for Playwright tests. + """ + + print("\nDetecting project language...\n") + await session.send(MessageOptions(prompt=detect_language_prompt)) + await done.wait() + + language = input("\n\nConfirm language for tests (or enter a different one): ").strip() + if not language: + language = "TypeScript" + + done.clear() + + test_generation_prompt = f""" + Based on the accessibility report you just generated for {url}, create Playwright accessibility tests in {language}. + + The tests should: + 1. Verify all the accessibility checks from the report + 2. Test for the issues that were found (to ensure they get fixed) + 3. Include tests for: + - Page has proper lang attribute + - Page has descriptive title + - Heading hierarchy is correct (single H1, proper nesting) + - All images have alt text + - No autoplay media + - Landmark regions exist (banner, nav, main, footer) + - Skip navigation link exists and works + - Focus indicators are visible + - Touch targets meet minimum size requirements + 4. Use Playwright's accessibility testing features + 5. Include helpful comments explaining each test + + Output the complete test file that can be saved and run. + Use the Playwright MCP server tools if you need to verify any page details. + """ + + print("\nGenerating accessibility tests...\n") + await session.send(MessageOptions(prompt=test_generation_prompt)) + await done.wait() + + print("\n\n=== Tests Generated ===") + + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/error_handling.py b/cookbook/copilot-sdk/python/recipe/error_handling.py index b76b29ce..7933cbba 100644 --- a/cookbook/copilot-sdk/python/recipe/error_handling.py +++ b/cookbook/copilot-sdk/python/recipe/error_handling.py @@ -1,28 +1,25 @@ #!/usr/bin/env python3 -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() +async def main(): + client = CopilotClient() -try: - client.start() - session = client.create_session(model="gpt-5") + try: + await client.start() + session = await client.create_session(SessionConfig(model="gpt-5")) - response = None - def handle_message(event): - nonlocal response - if event["type"] == "assistant.message": - response = event["data"]["content"] + response = await session.send_and_wait(MessageOptions(prompt="Hello!")) - session.on(handle_message) - session.send(prompt="Hello!") - session.wait_for_idle() + if response: + print(response.data.content) - if response: - print(response) + await session.destroy() + except Exception as e: + print(f"Error: {e}") + finally: + await client.stop() - session.destroy() -except Exception as e: - print(f"Error: {e}") -finally: - client.stop() +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/managing_local_files.py b/cookbook/copilot-sdk/python/recipe/managing_local_files.py index 8b9f94dc..c0381170 100644 --- a/cookbook/copilot-sdk/python/recipe/managing_local_files.py +++ b/cookbook/copilot-sdk/python/recipe/managing_local_files.py @@ -1,31 +1,40 @@ #!/usr/bin/env python3 -from copilot import CopilotClient +import asyncio import os +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) -# Create and start client -client = CopilotClient() -client.start() +async def main(): + # Create and start client + client = CopilotClient() + await client.start() -# Create session -session = client.create_session(model="gpt-5") + # Create session + session = await client.create_session(SessionConfig(model="gpt-5")) -# Event handler -def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nCopilot: {event['data']['content']}") - elif event["type"] == "tool.execution_start": - print(f" β†’ Running: {event['data']['toolName']}") - elif event["type"] == "tool.execution_complete": - print(f" βœ“ Completed: {event['data']['toolCallId']}") + done = asyncio.Event() -session.on(handle_event) + # Event handler + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nCopilot: {event.data.content}") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" β†’ Running: {event.data.tool_name}") + elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE: + print(f" βœ“ Completed: {event.data.tool_call_id}") + elif event.type.value == "session.idle": + done.set() -# Ask Copilot to organize files -# Change this to your target folder -target_folder = os.path.expanduser("~/Downloads") + session.on(handle_event) -session.send(prompt=f""" + # Ask Copilot to organize files + # Change this to your target folder + target_folder = os.path.expanduser("~/Downloads") + + await session.send(MessageOptions(prompt=f""" Analyze the files in "{target_folder}" and organize them into subfolders. 1. First, list all files and their metadata @@ -34,9 +43,12 @@ Analyze the files in "{target_folder}" and organize them into subfolders. 4. Move each file to its appropriate subfolder Please confirm before moving any files. -""") +""")) -session.wait_for_idle() + await done.wait() -session.destroy() -client.stop() + await session.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/multiple_sessions.py b/cookbook/copilot-sdk/python/recipe/multiple_sessions.py index dd4f299c..8d7d35d1 100644 --- a/cookbook/copilot-sdk/python/recipe/multiple_sessions.py +++ b/cookbook/copilot-sdk/python/recipe/multiple_sessions.py @@ -1,35 +1,40 @@ #!/usr/bin/env python3 -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() -# Create multiple independent sessions -session1 = client.create_session(model="gpt-5") -session2 = client.create_session(model="gpt-5") -session3 = client.create_session(model="claude-sonnet-4.5") + # Create multiple independent sessions + session1 = await client.create_session(SessionConfig(model="gpt-5")) + session2 = await client.create_session(SessionConfig(model="gpt-5")) + session3 = await client.create_session(SessionConfig(model="claude-sonnet-4.5")) -print("Created 3 independent sessions") + print("Created 3 independent sessions") -# Each session maintains its own conversation history -session1.send(prompt="You are helping with a Python project") -session2.send(prompt="You are helping with a TypeScript project") -session3.send(prompt="You are helping with a Go project") + # Each session maintains its own conversation history + await session1.send(MessageOptions(prompt="You are helping with a Python project")) + await session2.send(MessageOptions(prompt="You are helping with a TypeScript project")) + await session3.send(MessageOptions(prompt="You are helping with a Go project")) -print("Sent initial context to all sessions") + print("Sent initial context to all sessions") -# Follow-up messages stay in their respective contexts -session1.send(prompt="How do I create a virtual environment?") -session2.send(prompt="How do I set up tsconfig?") -session3.send(prompt="How do I initialize a module?") + # Follow-up messages stay in their respective contexts + await session1.send(MessageOptions(prompt="How do I create a virtual environment?")) + await session2.send(MessageOptions(prompt="How do I set up tsconfig?")) + await session3.send(MessageOptions(prompt="How do I initialize a module?")) -print("Sent follow-up questions to each session") + print("Sent follow-up questions to each session") -# Clean up all sessions -session1.destroy() -session2.destroy() -session3.destroy() -client.stop() + # Clean up all sessions + await session1.destroy() + await session2.destroy() + await session3.destroy() + await client.stop() -print("All sessions destroyed successfully") + print("All sessions destroyed successfully") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/persisting_sessions.py b/cookbook/copilot-sdk/python/recipe/persisting_sessions.py index b3d97f2f..da668070 100644 --- a/cookbook/copilot-sdk/python/recipe/persisting_sessions.py +++ b/cookbook/copilot-sdk/python/recipe/persisting_sessions.py @@ -1,36 +1,41 @@ #!/usr/bin/env python3 -from copilot import CopilotClient +import asyncio +from copilot import CopilotClient, SessionConfig, MessageOptions -client = CopilotClient() -client.start() +async def main(): + client = CopilotClient() + await client.start() -# Create session with a memorable ID -session = client.create_session( - session_id="user-123-conversation", - model="gpt-5", -) + # Create session with a memorable ID + session = await client.create_session(SessionConfig( + session_id="user-123-conversation", + model="gpt-5", + )) -session.send(prompt="Let's discuss TypeScript generics") -print(f"Session created: {session.session_id}") + await session.send_and_wait(MessageOptions(prompt="Let's discuss TypeScript generics")) + print(f"Session created: {session.session_id}") -# Destroy session but keep data on disk -session.destroy() -print("Session destroyed (state persisted)") + # Destroy session but keep data on disk + await session.destroy() + print("Session destroyed (state persisted)") -# Resume the previous session -resumed = client.resume_session("user-123-conversation") -print(f"Resumed: {resumed.session_id}") + # Resume the previous session + resumed = await client.resume_session("user-123-conversation") + print(f"Resumed: {resumed.session_id}") -resumed.send(prompt="What were we discussing?") + await resumed.send_and_wait(MessageOptions(prompt="What were we discussing?")) -# List sessions -sessions = client.list_sessions() -print("Sessions:", [s["sessionId"] for s in sessions]) + # List sessions + sessions = await client.list_sessions() + print("Sessions:", [s.session_id for s in sessions]) -# Delete session permanently -client.delete_session("user-123-conversation") -print("Session deleted") + # Delete session permanently + await client.delete_session("user-123-conversation") + print("Session deleted") -resumed.destroy() -client.stop() + await resumed.destroy() + await client.stop() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/pr_visualization.py b/cookbook/copilot-sdk/python/recipe/pr_visualization.py index 6be73dfd..9ece3f93 100644 --- a/cookbook/copilot-sdk/python/recipe/pr_visualization.py +++ b/cookbook/copilot-sdk/python/recipe/pr_visualization.py @@ -1,10 +1,14 @@ #!/usr/bin/env python3 +import asyncio import subprocess import sys import os import re -from copilot import CopilotClient +from copilot import ( + CopilotClient, SessionConfig, MessageOptions, + SessionEvent, SessionEventType, +) # ============================================================================ # Git & GitHub Detection @@ -60,7 +64,7 @@ def prompt_for_repo(): # Main Application # ============================================================================ -def main(): +async def main(): print("πŸ” PR Age Chart Generator\n") # Determine the repository @@ -88,11 +92,11 @@ def main(): owner, repo_name = repo.split("/", 1) - # Create Copilot client - no custom tools needed! - client = CopilotClient(log_level="error") - client.start() + # Create Copilot client + client = CopilotClient() + await client.start() - session = client.create_session( + session = await client.create_session(SessionConfig( model="gpt-5", system_message={ "content": f""" @@ -109,30 +113,34 @@ The current working directory is: {os.getcwd()} """ } - ) + )) + + done = asyncio.Event() # Set up event handling - def handle_event(event): - if event["type"] == "assistant.message": - print(f"\nπŸ€– {event['data']['content']}\n") - elif event["type"] == "tool.execution_start": - print(f" βš™οΈ {event['data']['toolName']}") + def handle_event(event: SessionEvent): + if event.type == SessionEventType.ASSISTANT_MESSAGE: + print(f"\nπŸ€– {event.data.content}\n") + elif event.type == SessionEventType.TOOL_EXECUTION_START: + print(f" βš™οΈ {event.data.tool_name}") + elif event.type.value == "session.idle": + done.set() session.on(handle_event) # Initial prompt - let Copilot figure out the details print("\nπŸ“Š Starting analysis...\n") - session.send(prompt=f""" + await session.send(MessageOptions(prompt=f""" Fetch the open pull requests for {owner}/{repo_name} 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. - """) + """)) - session.wait_for_idle() + await done.wait() # Interactive loop print("\nπŸ’‘ Ask follow-up questions or type \"exit\" to quit.\n") @@ -151,11 +159,12 @@ The current working directory is: {os.getcwd()} break if user_input: - session.send(prompt=user_input) - session.wait_for_idle() + done.clear() + await session.send(MessageOptions(prompt=user_input)) + await done.wait() - session.destroy() - client.stop() + await session.destroy() + await client.stop() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/cookbook/copilot-sdk/python/recipe/ralph_loop.py b/cookbook/copilot-sdk/python/recipe/ralph_loop.py new file mode 100644 index 00000000..918e8c66 --- /dev/null +++ b/cookbook/copilot-sdk/python/recipe/ralph_loop.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +""" +Ralph loop: autonomous AI task loop with fresh context per iteration. + +Two modes: + - "plan": reads PROMPT_plan.md, generates/updates IMPLEMENTATION_PLAN.md + - "build": reads PROMPT_build.md, implements tasks, runs tests, commits + +Each iteration creates a fresh session so the agent always operates in +the "smart zone" of its context window. State is shared between +iterations via files on disk (IMPLEMENTATION_PLAN.md, AGENTS.md, specs/*). + +Usage: + python ralph_loop.py # build mode, 50 iterations + python ralph_loop.py plan # planning mode + python ralph_loop.py 20 # build mode, 20 iterations + python ralph_loop.py plan 5 # planning mode, 5 iterations +""" + +import asyncio +import sys +from pathlib import Path + +from copilot import CopilotClient, MessageOptions, SessionConfig + + +async def ralph_loop(mode: str = "build", max_iterations: int = 50): + prompt_file = "PROMPT_plan.md" if mode == "plan" else "PROMPT_build.md" + + client = CopilotClient() + await client.start() + + print("━" * 40) + print(f"Mode: {mode}") + print(f"Prompt: {prompt_file}") + print(f"Max: {max_iterations} iterations") + print("━" * 40) + + try: + prompt = Path(prompt_file).read_text() + + for i in range(1, max_iterations + 1): + print(f"\n=== Iteration {i}/{max_iterations} ===") + + session = await client.create_session(SessionConfig( + model="gpt-5.1-codex-mini", + # Pin the agent to the project directory + working_directory=str(Path.cwd()), + # Auto-approve tool calls for unattended operation + on_permission_request=lambda _req, _ctx: { + "kind": "approved", + "rules": [], + }, + )) + + # Log tool usage for visibility + def log_tool_event(event): + if event.type.value == "tool.execution_start": + print(f" βš™ {event.data.tool_name}") + + session.on(log_tool_event) + try: + await session.send_and_wait( + MessageOptions(prompt=prompt), timeout=600 + ) + finally: + await session.destroy() + + print(f"\nIteration {i} complete.") + + print(f"\nReached max iterations: {max_iterations}") + finally: + await client.stop() + + +if __name__ == "__main__": + args = sys.argv[1:] + mode = "plan" if "plan" in args else "build" + max_iter = next((int(a) for a in args if a.isdigit()), 50) + asyncio.run(ralph_loop(mode, max_iter)) diff --git a/docs/README.agents.md b/docs/README.agents.md index e5729a51..861e4d7e 100644 --- a/docs/README.agents.md +++ b/docs/README.agents.md @@ -131,6 +131,7 @@ Custom agents for GitHub Copilot, making it easy for users and organizations to | [Prompt Builder](../agents/prompt-builder.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-builder.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-builder.agent.md) | Expert prompt engineering and validation system for creating high-quality prompts - Brought to you by microsoft/edge-ai | | | [Prompt Engineer](../agents/prompt-engineer.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-engineer.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fprompt-engineer.agent.md) | A specialized chat mode for analyzing and improving prompts. Every user input is treated as a prompt to be improved. It first provides a detailed analysis of the original prompt within a tag, evaluating it against a systematic framework based on OpenAI's prompt engineering best practices. Following the analysis, it generates a new, improved prompt. | | | [Python MCP Server Expert](../agents/python-mcp-expert.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fpython-mcp-expert.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fpython-mcp-expert.agent.md) | Expert assistant for developing Model Context Protocol (MCP) servers in Python | | +| [Reepl Linkedin](../agents/reepl-linkedin.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Freepl-linkedin.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Freepl-linkedin.agent.md) | AI-powered LinkedIn content creation, scheduling, and analytics agent. Create posts, carousels, and manage your LinkedIn presence with GitHub Copilot. | | | [Refine Requirement or Issue](../agents/refine-issue.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frefine-issue.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frefine-issue.agent.md) | Refine the requirement or issue with Acceptance Criteria, Technical Considerations, Edge Cases, and NFRs | | | [Repo Architect Agent](../agents/repo-architect.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frepo-architect.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Frepo-architect.agent.md) | Bootstraps and validates agentic project structures for GitHub Copilot (VS Code) and OpenCode CLI workflows. Run after `opencode /init` or VS Code Copilot initialization to scaffold proper folder hierarchies, instructions, agents, skills, and prompts. | | | [Ruby MCP Expert](../agents/ruby-mcp-expert.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fruby-mcp-expert.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fruby-mcp-expert.agent.md) | Expert assistance for building Model Context Protocol servers in Ruby using the official MCP Ruby SDK gem with Rails integration. | | diff --git a/docs/README.skills.md b/docs/README.skills.md index 0caa1ad4..e55085b7 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -45,6 +45,7 @@ Skills differ from other primitives by supporting bundled assets (scripts, code | [meeting-minutes](../skills/meeting-minutes/SKILL.md) | Generate concise, actionable meeting minutes for internal meetings. Includes metadata, attendees, agenda, decisions, action items (owner + due date), and follow-up steps. | None | | [microsoft-code-reference](../skills/microsoft-code-reference/SKILL.md) | Look up Microsoft API references, find working code samples, and verify SDK code is correct. Use when working with Azure SDKs, .NET libraries, or Microsoft APIsβ€”to find the right method, check parameters, get working examples, or troubleshoot errors. Catches hallucinated methods, wrong signatures, and deprecated patterns by querying official docs. | None | | [microsoft-docs](../skills/microsoft-docs/SKILL.md) | Query official Microsoft documentation to find concepts, tutorials, and code examples across Azure, .NET, Agent Framework, Aspire, VS Code, GitHub, and more. Uses Microsoft Learn MCP as the default, with Context7 and Aspire MCP for content that lives outside learn.microsoft.com. | None | +| [microsoft-skill-creator](../skills/microsoft-skill-creator/SKILL.md) | Create agent skills for Microsoft technologies using Learn MCP tools. Use when users want to create a skill that teaches agents about any Microsoft technology, library, framework, or service (Azure, .NET, M365, VS Code, Bicep, etc.). Investigates topics deeply, then generates a hybrid skill storing essential knowledge locally while enabling dynamic deeper investigation. | `references/skill-templates.md` | | [nano-banana-pro-openrouter](../skills/nano-banana-pro-openrouter/SKILL.md) | Generate or edit images via OpenRouter with the Gemini 3 Pro Image model. Use for prompt-only image generation, image edits, and multi-image compositing; supports 1K/2K/4K output. | `assets/SYSTEM_TEMPLATE`
`scripts/generate_image.py` | | [nuget-manager](../skills/nuget-manager/SKILL.md) | Manage NuGet packages in .NET projects/solutions. Use this skill when adding, removing, or updating NuGet package versions. It enforces using `dotnet` CLI for package management and provides strict procedures for direct file edits only when updating versions. | None | | [penpot-uiux-design](../skills/penpot-uiux-design/SKILL.md) | Comprehensive guide for creating professional UI/UX designs in Penpot using MCP tools. Use this skill when: (1) Creating new UI/UX designs for web, mobile, or desktop applications, (2) Building design systems with components and tokens, (3) Designing dashboards, forms, navigation, or landing pages, (4) Applying accessibility standards and best practices, (5) Following platform guidelines (iOS, Android, Material Design), (6) Reviewing or improving existing Penpot designs for usability. Triggers: "design a UI", "create interface", "build layout", "design dashboard", "create form", "design landing page", "make it accessible", "design system", "component library". | `references/accessibility.md`
`references/component-patterns.md`
`references/platform-guidelines.md`
`references/setup-troubleshooting.md` | diff --git a/instructions/power-apps-code-apps.instructions.md b/instructions/power-apps-code-apps.instructions.md index 5a2b4e26..0b0e7d9b 100644 --- a/instructions/power-apps-code-apps.instructions.md +++ b/instructions/power-apps-code-apps.instructions.md @@ -9,9 +9,9 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, ## Project Context -- **Power Apps Code Apps (Preview)**: Code-first web app development with Power Platform integration +- **Power Apps Code Apps**: Code-first web app development with Power Platform integration - **TypeScript + React**: Recommended frontend stack with Vite bundler -- **Power Platform SDK**: @microsoft/power-apps (current version ^0.3.1) for connector integration +- **Power Platform SDK**: @microsoft/power-apps (current version ^1.0.3) for connector integration - **PAC CLI**: Power Platform CLI for project management and deployment - **Port 3000**: Required for local development with Power Platform SDK - **Power Apps Premium**: End-user licensing requirement for production use @@ -25,14 +25,15 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, src/ β”œβ”€β”€ components/ # Reusable UI components β”œβ”€β”€ hooks/ # Custom React hooks for Power Platform - β”œβ”€β”€ services/ # Generated connector services (PAC CLI) - β”œβ”€β”€ models/ # Generated TypeScript models (PAC CLI) + β”œβ”€β”€ generated/ + β”‚ β”œβ”€β”€ services/ # Generated connector services (PAC CLI) + β”‚ └── models/ # Generated TypeScript models (PAC CLI) β”œβ”€β”€ utils/ # Utility functions and helpers β”œβ”€β”€ types/ # TypeScript type definitions - β”œβ”€β”€ PowerProvider.tsx # Power Platform initialization + β”œβ”€β”€ PowerProvider.tsx # Power Platform context wrapper └── main.tsx # Application entry point ``` -- Keep generated files (`services/`, `models/`) separate from custom code +- Keep generated files (`generated/services/`, `generated/models/`) separate from custom code - Use consistent naming conventions (kebab-case for files, PascalCase for components) ### TypeScript Configuration @@ -77,7 +78,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, ```typescript // Example: Using custom PCF control for data visualization import { PCFControlWrapper } from './components/PCFControlWrapper'; - + const MyComponent = () => { return ( { return ( { const formData = new FormData(); formData.append('file', file); - + const result = await AIBuilderService.ProcessDocument({ modelId: 'document-processing-model-id', document: formData }); - + return result.extractedFields; }; ``` @@ -141,12 +142,12 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, ```typescript import { DirectLine } from 'botframework-directlinejs'; import { WebChat } from 'botframework-webchat'; - + const ChatbotComponent = () => { const directLine = new DirectLine({ token: chatbotToken }); - + return (
@@ -159,23 +160,11 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, - Use generated TypeScript services from PAC CLI for connector operations - Implement proper authentication flows with Microsoft Entra ID - Handle connector consent dialogs and permission management -- PowerProvider implementation pattern: +- PowerProvider implementation pattern (no SDK initialization required in v1.0): ```typescript - import { initialize } from "@microsoft/power-apps/app"; - import { useEffect, type ReactNode } from "react"; + import type { ReactNode } from "react"; export default function PowerProvider({ children }: { children: ReactNode }) { - useEffect(() => { - const initApp = async () => { - try { - await initialize(); - console.log('Power Platform SDK initialized successfully'); - } catch (error) { - console.error('Failed to initialize Power Platform SDK:', error); - } - }; - initApp(); - }, []); return <>{children}; } ``` @@ -221,7 +210,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, // Handle polymorphic customer lookup (Account or Contact) const customerType = record.customerType; // 'account' or 'contact' const customerId = record.customerId; - const customer = customerType === 'account' + const customer = customerType === 'account' ? await AccountService.get(customerId) : await ContactService.get(customerId); ``` @@ -257,7 +246,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, const transaction = db.transaction(['data'], 'readwrite'); transaction.objectStore('data').put({ id: key, data, timestamp: Date.now() }); } - + async loadData(key: string) { const db = await this.openDB(); const transaction = db.transaction(['data'], 'readonly'); @@ -397,9 +386,9 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, onClick: () => void; children: React.ReactNode; } - - export const Button: React.FC = ({ - variant, size, disabled, onClick, children + + export const Button: React.FC = ({ + variant, size, disabled, onClick, children }) => { const classes = `btn btn-${variant} btn-${size} ${disabled ? 'btn-disabled' : ''}`; return ; @@ -416,14 +405,14 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, theme: 'light', toggleTheme: () => {} }); - + export const ThemeProvider: React.FC<{children: ReactNode}> = ({ children }) => { const [theme, setTheme] = useState<'light' | 'dark'>('light'); - + const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; - + return (
{children}
@@ -441,7 +430,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, .card-container { container-type: inline-size; } - + @container (min-width: 400px) { .card { display: grid; @@ -456,7 +445,7 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, - **Framer Motion integration**: Smooth animations and transitions ```typescript import { motion, AnimatePresence } from 'framer-motion'; - + const AnimatedCard = () => { return ( void, children: ReactNode}> = ({ - isOpen, onClose, children + const Modal: React.FC<{isOpen: boolean, onClose: () => void, children: ReactNode}> = ({ + isOpen, onClose, children }) => { useEffect(() => { if (isOpen) { @@ -491,11 +480,11 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen]); - + return ( -
@@ -512,10 +501,10 @@ Instructions for generating high-quality Power Apps Code Apps using TypeScript, - **React-intl integration**: Multi-language support ```typescript import { FormattedMessage, useIntl } from 'react-intl'; - + const WelcomeMessage = ({ userName }: { userName: string }) => { const intl = useIntl(); - + return (

This file contains a prompt template for generating conventional commit messages. It provides instructions, examples, and formatting guidelines to help users write standardized, descriptive commit messages in accordance with the Conventional Commits specification. - ``` ### Workflow diff --git a/skills/microsoft-skill-creator/SKILL.md b/skills/microsoft-skill-creator/SKILL.md new file mode 100644 index 00000000..545c40bd --- /dev/null +++ b/skills/microsoft-skill-creator/SKILL.md @@ -0,0 +1,217 @@ +--- +name: microsoft-skill-creator +description: Create agent skills for Microsoft technologies using Learn MCP tools. Use when users want to create a skill that teaches agents about any Microsoft technology, library, framework, or service (Azure, .NET, M365, VS Code, Bicep, etc.). Investigates topics deeply, then generates a hybrid skill storing essential knowledge locally while enabling dynamic deeper investigation. +context: fork +compatibility: Requires Microsoft Learn MCP Server (https://learn.microsoft.com/api/mcp) +--- + +# Microsoft Skill Creator + +Create hybrid skills for Microsoft technologies that store essential knowledge locally while enabling dynamic Learn MCP lookups for deeper details. + +## About Skills + +Skills are modular packages that extend agent capabilities with specialized knowledge and workflows. A skill transforms a general-purpose agent into a specialized one for a specific domain. + +### Skill Structure + +``` +skill-name/ +β”œβ”€β”€ SKILL.md (required) # Frontmatter (name, description) + instructions +β”œβ”€β”€ references/ # Documentation loaded into context as needed +β”œβ”€β”€ sample_codes/ # Working code examples +└── assets/ # Files used in output (templates, etc.) +``` + +### Key Principles + +- **Frontmatter is critical**: `name` and `description` determine when the skill triggersβ€”be clear and comprehensive +- **Concise is key**: Only include what agents don't already know; context window is shared +- **No duplication**: Information lives in SKILL.md OR reference files, not both + +## Learn MCP Tools + +| Tool | Purpose | When to Use | +|------|---------|-------------| +| `microsoft_docs_search` | Search official docs | First pass discovery, finding topics | +| `microsoft_docs_fetch` | Get full page content | Deep dive into important pages | +| `microsoft_code_sample_search` | Find code examples | Get implementation patterns | + +## Creation Process + +### Step 1: Investigate the Topic + +Build deep understanding using Learn MCP tools in three phases: + +**Phase 1 - Scope Discovery:** +``` +microsoft_docs_search(query="{technology} overview what is") +microsoft_docs_search(query="{technology} concepts architecture") +microsoft_docs_search(query="{technology} getting started tutorial") +``` + +**Phase 2 - Core Content:** +``` +microsoft_docs_fetch(url="...") # Fetch pages from Phase 1 +microsoft_code_sample_search(query="{technology}", language="{lang}") +``` + +**Phase 3 - Depth:** +``` +microsoft_docs_search(query="{technology} best practices") +microsoft_docs_search(query="{technology} troubleshooting errors") +``` + +#### Investigation Checklist + +After investigating, verify: +- [ ] Can explain what the technology does in one paragraph +- [ ] Identified 3-5 key concepts +- [ ] Have working code for basic usage +- [ ] Know the most common API patterns +- [ ] Have search queries for deeper topics + +### Step 2: Clarify with User + +Present findings and ask: +1. "I found these key areas: [list]. Which are most important?" +2. "What tasks will agents primarily perform with this skill?" +3. "Which programming language should code samples prioritize?" + +### Step 3: Generate the Skill + +Use the appropriate template from [skill-templates.md](references/skill-templates.md): + +| Technology Type | Template | +|-----------------|----------| +| Client library, NuGet/npm package | SDK/Library | +| Azure resource | Azure Service | +| App development framework | Framework/Platform | +| REST API, protocol | API/Protocol | + +#### Generated Skill Structure + +``` +{skill-name}/ +β”œβ”€β”€ SKILL.md # Core knowledge + Learn MCP guidance +β”œβ”€β”€ references/ # Detailed local documentation (if needed) +└── sample_codes/ # Working code examples + β”œβ”€β”€ getting-started/ + └── common-patterns/ +``` + +### Step 4: Balance Local vs Dynamic Content + +**Store locally when:** +- Foundational (needed for any task) +- Frequently accessed +- Stable (won't change) +- Hard to find via search + +**Keep dynamic when:** +- Exhaustive reference (too large) +- Version-specific +- Situational (specific tasks only) +- Well-indexed (easy to search) + +#### Content Guidelines + +| Content Type | Local | Dynamic | +|--------------|-------|---------| +| Core concepts (3-5) | βœ… Full | | +| Hello world code | βœ… Full | | +| Common patterns (3-5) | βœ… Full | | +| Top API methods | Signature + example | Full docs via fetch | +| Best practices | Top 5 bullets | Search for more | +| Troubleshooting | | Search queries | +| Full API reference | | Doc links | + +### Step 5: Validate + +1. Review: Is local content sufficient for common tasks? +2. Test: Do suggested search queries return useful results? +3. Verify: Do code samples run without errors? + +## Common Investigation Patterns + +### For SDKs/Libraries +``` +"{name} overview" β†’ purpose, architecture +"{name} getting started quickstart" β†’ setup steps +"{name} API reference" β†’ core classes/methods +"{name} samples examples" β†’ code patterns +"{name} best practices performance" β†’ optimization +``` + +### For Azure Services +``` +"{service} overview features" β†’ capabilities +"{service} quickstart {language}" β†’ setup code +"{service} REST API reference" β†’ endpoints +"{service} SDK {language}" β†’ client library +"{service} pricing limits quotas" β†’ constraints +``` + +### For Frameworks/Platforms +``` +"{framework} architecture concepts" β†’ mental model +"{framework} project structure" β†’ conventions +"{framework} tutorial walkthrough" β†’ end-to-end flow +"{framework} configuration options" β†’ customization +``` + +## Example: Creating a "Semantic Kernel" Skill + +### Investigation + +``` +microsoft_docs_search(query="semantic kernel overview") +microsoft_docs_search(query="semantic kernel plugins functions") +microsoft_code_sample_search(query="semantic kernel", language="csharp") +microsoft_docs_fetch(url="https://learn.microsoft.com/semantic-kernel/overview/") +``` + +### Generated Skill + +``` +semantic-kernel/ +β”œβ”€β”€ SKILL.md +└── sample_codes/ + β”œβ”€β”€ getting-started/ + β”‚ └── hello-kernel.cs + └── common-patterns/ + β”œβ”€β”€ chat-completion.cs + └── function-calling.cs +``` + +### Generated SKILL.md + +```markdown +--- +name: semantic-kernel +description: Build AI agents with Microsoft Semantic Kernel. Use for LLM-powered apps with plugins, planners, and memory in .NET or Python. +--- + +# Semantic Kernel + +Orchestration SDK for integrating LLMs into applications with plugins, planners, and memory. + +## Key Concepts + +- **Kernel**: Central orchestrator managing AI services and plugins +- **Plugins**: Collections of functions the AI can call +- **Planner**: Sequences plugin functions to achieve goals +- **Memory**: Vector store integration for RAG patterns + +## Quick Start + +See [getting-started/hello-kernel.cs](sample_codes/getting-started/hello-kernel.cs) + +## Learn More + +| Topic | How to Find | +|-------|-------------| +| Plugin development | `microsoft_docs_search(query="semantic kernel plugins custom functions")` | +| Planners | `microsoft_docs_search(query="semantic kernel planner")` | +| Memory | `microsoft_docs_fetch(url="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-memory")` | +``` diff --git a/skills/microsoft-skill-creator/references/skill-templates.md b/skills/microsoft-skill-creator/references/skill-templates.md new file mode 100644 index 00000000..dc234300 --- /dev/null +++ b/skills/microsoft-skill-creator/references/skill-templates.md @@ -0,0 +1,333 @@ +# Skill Templates + +Ready-to-use templates for different types of Microsoft technologies. + +## Template 1: SDK/Library Skill + +For client libraries, SDKs, and programming frameworks. + +```markdown +--- +name: {sdk-name} +description: {What it does}. Use when agents need to {primary task} with {technology context}. Supports {languages/platforms}. +--- + +# {SDK Name} + +{One paragraph: what it is, why it exists, when to use it} + +## Installation + +{Package manager commands for supported languages} + +## Key Concepts + +{3-5 essential concepts, one paragraph each max} + +### {Concept 1} +{Brief explanation} + +### {Concept 2} +{Brief explanation} + +## Quick Start + +{Minimal working example - inline if <30 lines, otherwise reference sample_codes/} + +## Common Patterns + +### {Pattern 1: e.g., "Basic CRUD"} +```{language} +{code} +``` + +### {Pattern 2: e.g., "Error Handling"} +```{language} +{code} +``` + +## API Quick Reference + +| Class/Method | Purpose | Example | +|--------------|---------|---------| +| {name} | {what it does} | `{usage}` | + +For full API documentation: +- `microsoft_docs_search(query="{sdk} {class} API reference")` +- `microsoft_docs_fetch(url="{url}")` + +## Best Practices + +- **Do**: {recommendation} +- **Do**: {recommendation} +- **Avoid**: {anti-pattern} + +See [best-practices.md](references/best-practices.md) for detailed guidance. + +## Learn More + +| Topic | How to Find | +|-------|-------------| +| {Advanced topic 1} | `microsoft_docs_search(query="{sdk} {topic}")` | +| {Advanced topic 2} | `microsoft_docs_fetch(url="{url}")` | +| {Code examples} | `microsoft_code_sample_search(query="{sdk} {scenario}", language="{lang}")` | +``` + +--- + +## Template 2: Azure Service Skill + +For Azure services and cloud resources. + +```markdown +--- +name: {service-name} +description: Work with {Azure Service}. Use when agents need to {primary capabilities}. Covers provisioning, configuration, and SDK usage. +--- + +# {Azure Service Name} + +{One paragraph: what the service does, primary use cases} + +## Overview + +- **Category**: {Compute/Storage/AI/Networking/etc.} +- **Key capability**: {main value proposition} +- **When to use**: {scenarios} + +## Getting Started + +### Prerequisites +- Azure subscription +- {Other requirements} + +### Provisioning +{CLI/Portal/Bicep snippet for creating the resource} + +## SDK Usage ({Language}) + +### Installation +``` +{package install command} +``` + +### Authentication +```{language} +{auth code pattern} +``` + +### Basic Operations +```{language} +{CRUD or primary operations} +``` + +## Key Configurations + +| Setting | Purpose | Default | +|---------|---------|---------| +| {setting} | {what it controls} | {value} | + +## Pricing & Limits + +- **Pricing model**: {consumption/tier-based/etc.} +- **Key limits**: {important quotas} + +For current pricing: `microsoft_docs_search(query="{service} pricing")` + +## Common Patterns + +### {Pattern 1} +{Code or configuration} + +### {Pattern 2} +{Code or configuration} + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| {Common error} | {Fix} | + +For more issues: `microsoft_docs_search(query="{service} troubleshoot {symptom}")` + +## Learn More + +| Topic | How to Find | +|-------|-------------| +| REST API | `microsoft_docs_fetch(url="{url}")` | +| ARM/Bicep | `microsoft_docs_search(query="{service} bicep template")` | +| Security | `microsoft_docs_search(query="{service} security best practices")` | +``` + +--- + +## Template 3: Framework/Platform Skill + +For development frameworks and platforms (e.g., ASP.NET, MAUI, Blazor). + +```markdown +--- +name: {framework-name} +description: Build {type of apps} with {Framework}. Use when agents need to create, modify, or debug {framework} applications. +--- + +# {Framework Name} + +{One paragraph: what it is, what you build with it, why choose it} + +## Project Structure + +``` +{typical-project}/ +β”œβ”€β”€ {folder}/ # {purpose} +β”œβ”€β”€ {file} # {purpose} +└── {file} # {purpose} +``` + +## Getting Started + +### Create New Project +```bash +{CLI command to scaffold} +``` + +### Project Configuration +{Key files to configure and what they control} + +## Core Concepts + +### {Concept 1: e.g., "Components"} +{Explanation with minimal code example} + +### {Concept 2: e.g., "Routing"} +{Explanation with minimal code example} + +### {Concept 3: e.g., "State Management"} +{Explanation with minimal code example} + +## Common Patterns + +### {Pattern 1} +```{language} +{code} +``` + +### {Pattern 2} +```{language} +{code} +``` + +## Configuration Options + +| Setting | File | Purpose | +|---------|------|---------| +| {setting} | {file} | {what it does} | + +## Deployment + +{Brief deployment guidance or reference} + +For detailed deployment: `microsoft_docs_search(query="{framework} deploy {target}")` + +## Learn More + +| Topic | How to Find | +|-------|-------------| +| {Advanced feature} | `microsoft_docs_search(query="{framework} {feature}")` | +| {Integration} | `microsoft_docs_fetch(url="{url}")` | +| {Samples} | `microsoft_code_sample_search(query="{framework} {scenario}")` | +``` + +--- + +## Template 4: API/Protocol Skill + +For APIs, protocols, and specifications (e.g., Microsoft Graph, OOXML). + +```markdown +--- +name: {api-name} +description: Interact with {API/Protocol}. Use when agents need to {primary operations}. Covers authentication, endpoints, and common operations. +--- + +# {API/Protocol Name} + +{One paragraph: what it provides access to, primary use cases} + +## Authentication + +{Auth method and code pattern} + +## Base Configuration + +- **Base URL**: `{url}` +- **Version**: `{version}` +- **Format**: {JSON/XML/etc.} + +## Common Endpoints/Operations + +### {Operation 1: e.g., "List Items"} +``` +{HTTP method} {endpoint} +``` +```{language} +{SDK code} +``` + +### {Operation 2: e.g., "Create Item"} +``` +{HTTP method} {endpoint} +``` +```{language} +{SDK code} +``` + +## Request/Response Patterns + +### Pagination +{How to handle pagination} + +### Error Handling +{Error format and common codes} + +## Quick Reference + +| Operation | Endpoint/Method | Notes | +|-----------|-----------------|-------| +| {op} | `{endpoint}` | {note} | + +## Permissions/Scopes + +| Operation | Required Permission | +|-----------|---------------------| +| {op} | `{permission}` | + +## Learn More + +| Topic | How to Find | +|-------|-------------| +| Full endpoint reference | `microsoft_docs_fetch(url="{url}")` | +| Permissions | `microsoft_docs_search(query="{api} permissions {resource}")` | +| SDKs | `microsoft_docs_search(query="{api} SDK {language}")` | +``` + +--- + +## Choosing a Template + +| Technology Type | Template | Examples | +|-----------------|----------|----------| +| Client library, NuGet/npm package | SDK/Library | Semantic Kernel, Azure SDK, MSAL | +| Azure resource | Azure Service | Cosmos DB, Azure Functions, App Service | +| App development framework | Framework/Platform | ASP.NET Core, Blazor, MAUI | +| REST API, protocol, specification | API/Protocol | Microsoft Graph, OOXML, FHIR | + +## Customization Guidelines + +Templates are starting points. Customize by: + +1. **Adding sections** for unique aspects of the technology +2. **Removing sections** that don't apply +3. **Adjusting depth** based on complexity (more concepts for complex tech) +4. **Adding reference files** for detailed content that doesn't fit in SKILL.md +5. **Adding sample_codes/** for working examples beyond inline snippets diff --git a/website/src/scripts/pages/samples.ts b/website/src/scripts/pages/samples.ts index 8a896284..cdc2e7d3 100644 --- a/website/src/scripts/pages/samples.ts +++ b/website/src/scripts/pages/samples.ts @@ -5,6 +5,7 @@ import { FuzzySearch, type SearchableItem } from "../search"; import { fetchData, escapeHtml } from "../utils"; import { createChoices, getChoicesValues, type Choices } from "../choices"; +import { setupModal } from "../modal"; // Types interface Language { @@ -82,6 +83,7 @@ export async function initSamplesPage(): Promise { search = new FuzzySearch(allRecipes); // Setup UI + setupModal(); setupFilters(); setupSearch(); renderCookbooks();