From 890b1a79e69579c744c3133dce80191515129674 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 22:15:44 -0800 Subject: [PATCH 01/61] docs: add steering and queueing guide (#714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add steering and queueing guide Add a comprehensive guide covering the two message delivery modes available through the SDK: - Steering (immediate mode): inject messages into the current agent turn for real-time course correction - Queueing (enqueue mode): buffer messages for sequential processing after the current turn completes Includes code examples for all four SDK languages (Node.js, Python, Go, .NET), a sequence diagram, API reference, UI building patterns, and best practices. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct Python permission handler in steering guide Fix all 3 Python snippets to use the correct signature: - 2-arg lambda (req, inv) matching PermissionHandlerFn typedef - Return PermissionRequestResult dataclass instead of plain dict - Add 'from copilot.types import PermissionRequestResult' import The previous pattern (lambda req: {"kind": "approved"}) would fail at runtime: TypeError from wrong arg count, and AttributeError from dict lacking .kind attribute access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct Python permission handler in skills and custom-agents guides Same fix as the steering guide — use correct 2-arg signature and PermissionRequestResult dataclass return type. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/custom-agents.md | 3 +- docs/guides/skills.md | 3 +- docs/guides/steering-and-queueing.md | 506 +++++++++++++++++++++++++++ 3 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 docs/guides/steering-and-queueing.md diff --git a/docs/guides/custom-agents.md b/docs/guides/custom-agents.md index 16f7a37a0c..82790d3417 100644 --- a/docs/guides/custom-agents.md +++ b/docs/guides/custom-agents.md @@ -65,6 +65,7 @@ const session = await client.createSession({ ```python from copilot import CopilotClient +from copilot.types import PermissionRequestResult client = CopilotClient() await client.start() @@ -87,7 +88,7 @@ session = await client.create_session({ "prompt": "You are a code editor. Make minimal, surgical changes to files as requested.", }, ], - "on_permission_request": lambda req: {"kind": "approved"}, + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), }) ``` diff --git a/docs/guides/skills.md b/docs/guides/skills.md index 5e085e3b29..b9b07ae882 100644 --- a/docs/guides/skills.md +++ b/docs/guides/skills.md @@ -43,6 +43,7 @@ await session.sendAndWait({ prompt: "Review this code for security issues" }); ```python from copilot import CopilotClient +from copilot.types import PermissionRequestResult async def main(): client = CopilotClient() @@ -54,7 +55,7 @@ async def main(): "./skills/code-review", "./skills/documentation", ], - "on_permission_request": lambda req: {"kind": "approved"}, + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), }) # Copilot now has access to skills in those directories diff --git a/docs/guides/steering-and-queueing.md b/docs/guides/steering-and-queueing.md new file mode 100644 index 0000000000..da66caa64f --- /dev/null +++ b/docs/guides/steering-and-queueing.md @@ -0,0 +1,506 @@ +# Steering & Queueing + +Two interaction patterns let users send messages while the agent is already working: **steering** redirects the agent mid-turn, and **queueing** buffers messages for sequential processing after the current turn completes. + +## Overview + +When a session is actively processing a turn, incoming messages can be delivered in one of two modes via the `mode` field on `MessageOptions`: + +| Mode | Behavior | Use case | +|------|----------|----------| +| `"immediate"` (steering) | Injected into the **current** LLM turn | "Actually, don't create that file — use a different approach" | +| `"enqueue"` (queueing) | Queued and processed **after** the current turn finishes | "After this, also fix the tests" | + +```mermaid +sequenceDiagram + participant U as User + participant S as Session + participant LLM as Agent + + U->>S: send({ prompt: "Refactor auth" }) + S->>LLM: Turn starts + + Note over U,LLM: Agent is busy... + + U->>S: send({ prompt: "Use JWT instead", mode: "immediate" }) + S-->>LLM: Injected into current turn (steering) + + U->>S: send({ prompt: "Then update the docs", mode: "enqueue" }) + S-->>S: Queued for next turn + + LLM->>S: Turn completes (incorporates steering) + S->>LLM: Processes queued message + LLM->>S: Turn completes +``` + +## Steering (Immediate Mode) + +Steering sends a message that is injected directly into the agent's current turn. The agent sees the message in real time and adjusts its response accordingly — useful for course-correcting without aborting the turn. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +// Start a long-running task +const msgId = await session.send({ + prompt: "Refactor the authentication module to use sessions", +}); + +// While the agent is working, steer it +await session.send({ + prompt: "Actually, use JWT tokens instead of sessions", + mode: "immediate", +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import PermissionRequestResult + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), + }) + + # Start a long-running task + msg_id = await session.send({ + "prompt": "Refactor the authentication module to use sessions", + }) + + # While the agent is working, steer it + await session.send({ + "prompt": "Actually, use JWT tokens instead of sessions", + "mode": "immediate", + }) + + await client.stop() +``` + +
+ +
+Go + +```go +package main + +import ( + "context" + "log" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + if err != nil { + log.Fatal(err) + } + + // Start a long-running task + _, err = session.Send(ctx, copilot.MessageOptions{ + Prompt: "Refactor the authentication module to use sessions", + }) + if err != nil { + log.Fatal(err) + } + + // While the agent is working, steer it + _, err = session.Send(ctx, copilot.MessageOptions{ + Prompt: "Actually, use JWT tokens instead of sessions", + Mode: "immediate", + }) + if err != nil { + log.Fatal(err) + } +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), +}); + +// Start a long-running task +var msgId = await session.SendAsync(new MessageOptions +{ + Prompt = "Refactor the authentication module to use sessions" +}); + +// While the agent is working, steer it +await session.SendAsync(new MessageOptions +{ + Prompt = "Actually, use JWT tokens instead of sessions", + Mode = "immediate" +}); +``` + +
+ +### How Steering Works Internally + +1. The message is added to the runtime's `ImmediatePromptProcessor` queue +2. Before the next LLM request within the current turn, the processor injects the message into the conversation +3. The agent sees the steering message as a new user message and adjusts its response +4. If the turn completes before the steering message is processed, it is automatically moved to the regular queue for the next turn + +> **Note:** Steering messages are best-effort within the current turn. If the agent has already committed to a tool call, the steering takes effect after that call completes but still within the same turn. + +## Queueing (Enqueue Mode) + +Queueing buffers messages to be processed sequentially after the current turn finishes. Each queued message starts its own full turn. This is the default mode — if you omit `mode`, the SDK uses `"enqueue"`. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +// Send an initial task +await session.send({ prompt: "Set up the project structure" }); + +// Queue follow-up tasks while the agent is busy +await session.send({ + prompt: "Add unit tests for the auth module", + mode: "enqueue", +}); + +await session.send({ + prompt: "Update the README with setup instructions", + mode: "enqueue", +}); + +// Messages are processed in FIFO order after each turn completes +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import PermissionRequestResult + +async def main(): + client = CopilotClient() + await client.start() + + session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), + }) + + # Send an initial task + await session.send({"prompt": "Set up the project structure"}) + + # Queue follow-up tasks while the agent is busy + await session.send({ + "prompt": "Add unit tests for the auth module", + "mode": "enqueue", + }) + + await session.send({ + "prompt": "Update the README with setup instructions", + "mode": "enqueue", + }) + + # Messages are processed in FIFO order after each turn completes + await client.stop() +``` + +
+ +
+Go + + +```go +// Send an initial task +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Set up the project structure", +}) + +// Queue follow-up tasks while the agent is busy +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Add unit tests for the auth module", + Mode: "enqueue", +}) + +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Update the README with setup instructions", + Mode: "enqueue", +}) + +// Messages are processed in FIFO order after each turn completes +``` + +
+ +
+.NET + + +```csharp +// Send an initial task +await session.SendAsync(new MessageOptions +{ + Prompt = "Set up the project structure" +}); + +// Queue follow-up tasks while the agent is busy +await session.SendAsync(new MessageOptions +{ + Prompt = "Add unit tests for the auth module", + Mode = "enqueue" +}); + +await session.SendAsync(new MessageOptions +{ + Prompt = "Update the README with setup instructions", + Mode = "enqueue" +}); + +// Messages are processed in FIFO order after each turn completes +``` + +
+ +### How Queueing Works Internally + +1. The message is added to the session's `itemQueue` as a `QueuedItem` +2. When the current turn completes and the session becomes idle, `processQueuedItems()` runs +3. Items are dequeued in FIFO order — each message triggers a full agentic turn +4. If a steering message was pending when the turn ended, it is moved to the front of the queue +5. Processing continues until the queue is empty, then the session emits an idle event + +## Combining Steering and Queueing + +You can use both patterns together in a single session. Steering affects the current turn while queued messages wait for their own turns: + +
+Node.js / TypeScript + +```typescript +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +// Start a task +await session.send({ prompt: "Refactor the database layer" }); + +// Steer the current work +await session.send({ + prompt: "Make sure to keep backwards compatibility with the v1 API", + mode: "immediate", +}); + +// Queue a follow-up for after this turn +await session.send({ + prompt: "Now add migration scripts for the schema changes", + mode: "enqueue", +}); +``` + +
+ +
+Python + +```python +session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), +}) + +# Start a task +await session.send({"prompt": "Refactor the database layer"}) + +# Steer the current work +await session.send({ + "prompt": "Make sure to keep backwards compatibility with the v1 API", + "mode": "immediate", +}) + +# Queue a follow-up for after this turn +await session.send({ + "prompt": "Now add migration scripts for the schema changes", + "mode": "enqueue", +}) +``` + +
+ +## Choosing Between Steering and Queueing + +| Scenario | Pattern | Why | +|----------|---------|-----| +| Agent is going down the wrong path | **Steering** | Redirects the current turn without losing progress | +| You thought of something the agent should also do | **Queueing** | Doesn't disrupt current work; runs next | +| Agent is about to make a mistake | **Steering** | Intervenes before the mistake is committed | +| You want to chain multiple tasks | **Queueing** | FIFO ordering ensures predictable execution | +| You want to add context to the current task | **Steering** | Agent incorporates it into its current reasoning | +| You want to batch unrelated requests | **Queueing** | Each gets its own full turn with clean context | + +## Building a UI with Steering & Queueing + +Here's a pattern for building an interactive UI that supports both modes: + +```typescript +import { CopilotClient, CopilotSession } from "@github/copilot-sdk"; + +interface PendingMessage { + prompt: string; + mode: "immediate" | "enqueue"; + sentAt: Date; +} + +class InteractiveChat { + private session: CopilotSession; + private isProcessing = false; + private pendingMessages: PendingMessage[] = []; + + constructor(session: CopilotSession) { + this.session = session; + + session.on((event) => { + if (event.type === "session.idle") { + this.isProcessing = false; + this.onIdle(); + } + if (event.type === "assistant.message") { + this.renderMessage(event); + } + }); + } + + async sendMessage(prompt: string): Promise { + if (!this.isProcessing) { + this.isProcessing = true; + await this.session.send({ prompt }); + return; + } + + // Session is busy — let the user choose how to deliver + // Your UI would present this choice (e.g., buttons, keyboard shortcuts) + } + + async steer(prompt: string): Promise { + this.pendingMessages.push({ + prompt, + mode: "immediate", + sentAt: new Date(), + }); + await this.session.send({ prompt, mode: "immediate" }); + } + + async enqueue(prompt: string): Promise { + this.pendingMessages.push({ + prompt, + mode: "enqueue", + sentAt: new Date(), + }); + await this.session.send({ prompt, mode: "enqueue" }); + } + + private onIdle(): void { + this.pendingMessages = []; + // Update UI to show session is ready for new input + } + + private renderMessage(event: unknown): void { + // Render assistant message in your UI + } +} +``` + +## API Reference + +### MessageOptions + +| Language | Field | Type | Default | Description | +|----------|-------|------|---------|-------------| +| Node.js | `mode` | `"enqueue" \| "immediate"` | `"enqueue"` | Message delivery mode | +| Python | `mode` | `Literal["enqueue", "immediate"]` | `"enqueue"` | Message delivery mode | +| Go | `Mode` | `string` | `"enqueue"` | Message delivery mode | +| .NET | `Mode` | `string?` | `"enqueue"` | Message delivery mode | + +### Delivery Modes + +| Mode | Effect | During active turn | During idle | +|------|--------|-------------------|-------------| +| `"enqueue"` | Queue for next turn | Waits in FIFO queue | Starts a new turn immediately | +| `"immediate"` | Inject into current turn | Injected before next LLM call | Starts a new turn immediately | + +> **Note:** When the session is idle (not processing), both modes behave identically — the message starts a new turn immediately. + +## Best Practices + +1. **Default to queueing** — Use `"enqueue"` (or omit `mode`) for most messages. It's predictable and doesn't risk disrupting in-progress work. + +2. **Reserve steering for corrections** — Use `"immediate"` when the agent is actively doing the wrong thing and you need to redirect it before it goes further. + +3. **Keep steering messages concise** — The agent needs to quickly understand the course correction. Long, complex steering messages may confuse the current context. + +4. **Don't over-steer** — Multiple rapid steering messages can degrade turn quality. If you need to change direction significantly, consider aborting the turn and starting fresh. + +5. **Show queue state in your UI** — Display the number of queued messages so users know what's pending. Listen for idle events to clear the display. + +6. **Handle the steering-to-queue fallback** — If a steering message arrives after the turn completes, it's automatically moved to the queue. Design your UI to reflect this transition. + +## See Also + +- [Getting Started](../getting-started.md) — Set up a session and send messages +- [Custom Agents](./custom-agents.md) — Define specialized agents with scoped tools +- [Session Hooks](../hooks/overview.md) — React to session lifecycle events +- [Session Persistence](./session-persistence.md) — Resume sessions across restarts From 5fbd1458eefd16d987a896583f8887231187c508 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 22:24:04 -0800 Subject: [PATCH 02/61] docs: add Microsoft Agent Framework integration guide (#716) Covers using the Copilot SDK as a provider in MAF workflows including: - Basic usage (wrapping CopilotClient as a MAF AIAgent) - Custom function tools - Multi-agent orchestration (sequential, concurrent) - Streaming responses - Configuration reference and best practices Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/microsoft-agent-framework.md | 456 +++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 docs/guides/microsoft-agent-framework.md diff --git a/docs/guides/microsoft-agent-framework.md b/docs/guides/microsoft-agent-framework.md new file mode 100644 index 0000000000..0b7635e429 --- /dev/null +++ b/docs/guides/microsoft-agent-framework.md @@ -0,0 +1,456 @@ +# Microsoft Agent Framework Integration + +Use the Copilot SDK as an agent provider inside the [Microsoft Agent Framework](https://devblogs.microsoft.com/semantic-kernel/build-ai-agents-with-github-copilot-sdk-and-microsoft-agent-framework/) (MAF) to compose multi-agent workflows alongside Azure OpenAI, Anthropic, and other providers. + +## Overview + +The Microsoft Agent Framework is the unified successor to Semantic Kernel and AutoGen. It provides a standard interface for building, orchestrating, and deploying AI agents. Dedicated integration packages let you wrap a Copilot SDK client as a first-class MAF agent — interchangeable with any other agent provider in the framework. + +| Concept | Description | +|---------|-------------| +| **Microsoft Agent Framework** | Open-source framework for single- and multi-agent orchestration in .NET and Python | +| **Agent provider** | A backend that powers an agent (Copilot, Azure OpenAI, Anthropic, etc.) | +| **Orchestrator** | A MAF component that coordinates agents in sequential, concurrent, or handoff workflows | +| **A2A protocol** | Agent-to-Agent communication standard supported by the framework | + +> **Note:** MAF integration packages are available for **.NET** and **Python**. For TypeScript and Go, use the Copilot SDK directly — the standard SDK APIs already provide tool calling, streaming, and custom agents. + +## Prerequisites + +Before you begin, ensure you have: + +- A working [Copilot SDK setup](../getting-started.md) in your language of choice +- A GitHub Copilot subscription (Individual, Business, or Enterprise) +- The Copilot CLI installed or available via the SDK's bundled CLI + +## Installation + +Install the Copilot SDK alongside the MAF integration package for your language: + +
+.NET + +```shell +dotnet add package GitHub.Copilot.SDK +dotnet add package Microsoft.Agents.AI.GitHub.Copilot --prerelease +``` + +
+ +
+Python + +```shell +pip install copilot-sdk agent-framework-github-copilot +``` + +
+ +## Basic Usage + +Wrap the Copilot SDK client as a MAF agent with a single method call. The resulting agent conforms to the framework's standard interface and can be used anywhere a MAF agent is expected. + +
+.NET + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; + +await using var copilotClient = new CopilotClient(); +await copilotClient.StartAsync(); + +// Wrap as a MAF agent +AIAgent agent = copilotClient.AsAIAgent(); + +// Use the standard MAF interface +string response = await agent.RunAsync("Explain how dependency injection works in ASP.NET Core"); +Console.WriteLine(response); +``` + +
+ +
+Python + + +```python +from agent_framework.github import GitHubCopilotAgent + +async def main(): + agent = GitHubCopilotAgent( + default_options={ + "instructions": "You are a helpful coding assistant.", + } + ) + + async with agent: + result = await agent.run("Explain how dependency injection works in FastAPI") + print(result) +``` + +
+ +## Adding Custom Tools + +Extend your Copilot agent with custom function tools. Tools defined through the standard Copilot SDK are automatically available when the agent runs inside MAF. + +
+.NET + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Extensions.AI; +using Microsoft.Agents.AI; + +// Define a custom tool +AIFunction weatherTool = AIFunctionFactory.Create( + (string location) => $"The weather in {location} is sunny with a high of 25°C.", + "GetWeather", + "Get the current weather for a given location." +); + +await using var copilotClient = new CopilotClient(); +await copilotClient.StartAsync(); + +// Create agent with tools +AIAgent agent = copilotClient.AsAIAgent(new AIAgentOptions +{ + Tools = new[] { weatherTool }, +}); + +string response = await agent.RunAsync("What's the weather like in Seattle?"); +Console.WriteLine(response); +``` + +
+ +
+Python + + +```python +from agent_framework.github import GitHubCopilotAgent + +def get_weather(location: str) -> str: + """Get the current weather for a given location.""" + return f"The weather in {location} is sunny with a high of 25°C." + +async def main(): + agent = GitHubCopilotAgent( + default_options={ + "instructions": "You are a helpful assistant with access to weather data.", + }, + tools=[get_weather], + ) + + async with agent: + result = await agent.run("What's the weather like in Seattle?") + print(result) +``` + +
+ +You can also use Copilot SDK's native tool definition alongside MAF tools: + +
+Node.js / TypeScript (standalone SDK) + +```typescript +import { CopilotClient, DefineTool } from "@github/copilot-sdk"; + +const getWeather = DefineTool({ + name: "GetWeather", + description: "Get the current weather for a given location.", + parameters: { location: { type: "string", description: "City name" } }, + execute: async ({ location }) => `The weather in ${location} is sunny, 25°C.`, +}); + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + tools: [getWeather], + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +await session.sendAndWait({ prompt: "What's the weather like in Seattle?" }); +``` + +
+ +## Multi-Agent Workflows + +The primary benefit of MAF integration is composing Copilot alongside other agent providers in orchestrated workflows. Use the framework's built-in orchestrators to create pipelines where different agents handle different steps. + +### Sequential Workflow + +Run agents one after another, passing output from one to the next: + +
+.NET + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Orchestration; + +await using var copilotClient = new CopilotClient(); +await copilotClient.StartAsync(); + +// Copilot agent for code review +AIAgent reviewer = copilotClient.AsAIAgent(new AIAgentOptions +{ + Instructions = "You review code for bugs, security issues, and best practices. Be thorough.", +}); + +// Azure OpenAI agent for generating documentation +AIAgent documentor = AIAgent.FromOpenAI(new OpenAIAgentOptions +{ + Model = "gpt-4.1", + Instructions = "You write clear, concise documentation for code changes.", +}); + +// Compose in a sequential pipeline +var pipeline = new SequentialOrchestrator(new[] { reviewer, documentor }); + +string result = await pipeline.RunAsync( + "Review and document this pull request: added retry logic to the HTTP client" +); +Console.WriteLine(result); +``` + +
+ +
+Python + + +```python +from agent_framework.github import GitHubCopilotAgent +from agent_framework.openai import OpenAIAgent +from agent_framework.orchestration import SequentialOrchestrator + +async def main(): + # Copilot agent for code review + reviewer = GitHubCopilotAgent( + default_options={ + "instructions": "You review code for bugs, security issues, and best practices.", + } + ) + + # OpenAI agent for documentation + documentor = OpenAIAgent( + model="gpt-4.1", + instructions="You write clear, concise documentation for code changes.", + ) + + # Compose in a sequential pipeline + pipeline = SequentialOrchestrator(agents=[reviewer, documentor]) + + async with pipeline: + result = await pipeline.run( + "Review and document this PR: added retry logic to the HTTP client" + ) + print(result) +``` + +
+ +### Concurrent Workflow + +Run multiple agents in parallel and aggregate their results: + +
+.NET + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Orchestration; + +await using var copilotClient = new CopilotClient(); +await copilotClient.StartAsync(); + +AIAgent securityReviewer = copilotClient.AsAIAgent(new AIAgentOptions +{ + Instructions = "Focus exclusively on security vulnerabilities and risks.", +}); + +AIAgent performanceReviewer = copilotClient.AsAIAgent(new AIAgentOptions +{ + Instructions = "Focus exclusively on performance bottlenecks and optimization opportunities.", +}); + +// Run both reviews concurrently +var concurrent = new ConcurrentOrchestrator(new[] { securityReviewer, performanceReviewer }); + +string combinedResult = await concurrent.RunAsync( + "Analyze this database query module for issues" +); +Console.WriteLine(combinedResult); +``` + +
+ +## Streaming Responses + +When building interactive applications, stream agent responses to show real-time output. The MAF integration preserves the Copilot SDK's streaming capabilities. + +
+.NET + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Agents.AI; + +await using var copilotClient = new CopilotClient(); +await copilotClient.StartAsync(); + +AIAgent agent = copilotClient.AsAIAgent(new AIAgentOptions +{ + Streaming = true, +}); + +await foreach (var chunk in agent.RunStreamingAsync("Write a quicksort implementation in C#")) +{ + Console.Write(chunk); +} +Console.WriteLine(); +``` + +
+ +
+Python + + +```python +from agent_framework.github import GitHubCopilotAgent + +async def main(): + agent = GitHubCopilotAgent( + default_options={"streaming": True} + ) + + async with agent: + async for chunk in agent.run_streaming("Write a quicksort in Python"): + print(chunk, end="", flush=True) + print() +``` + +
+ +You can also stream directly through the Copilot SDK without MAF: + +
+Node.js / TypeScript (standalone SDK) + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + streaming: true, + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.delta ?? ""); +}); + +await session.sendAndWait({ prompt: "Write a quicksort implementation in TypeScript" }); +``` + +
+ +## Configuration Reference + +### MAF Agent Options + +| Property | Type | Description | +|----------|------|-------------| +| `Instructions` / `instructions` | `string` | System prompt for the agent | +| `Tools` / `tools` | `AIFunction[]` / `list` | Custom function tools available to the agent | +| `Streaming` / `streaming` | `bool` | Enable streaming responses | +| `Model` / `model` | `string` | Override the default model | + +### Copilot SDK Options (Passed Through) + +All standard [SessionConfig](../getting-started.md) options are still available when creating the underlying Copilot client. The MAF wrapper delegates to the SDK under the hood: + +| SDK Feature | MAF Support | +|-------------|-------------| +| Custom tools (`DefineTool` / `AIFunctionFactory`) | ✅ Merged with MAF tools | +| MCP servers | ✅ Configured on the SDK client | +| Custom agents / sub-agents | ✅ Available within the Copilot agent | +| Infinite sessions | ✅ Configured on the SDK client | +| Model selection | ✅ Overridable per agent or per call | +| Streaming | ✅ Full delta event support | + +## Best Practices + +### Choose the right level of integration + +Use the MAF wrapper when you need to compose Copilot with other providers in orchestrated workflows. If your application only uses Copilot, the standalone SDK is simpler and gives you full control: + +```typescript +// Standalone SDK — full control, simpler setup +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); +const response = await session.sendAndWait({ prompt: "Explain this code" }); +``` + +### Keep agents focused + +When building multi-agent workflows, give each agent a specific role with clear instructions. Avoid overlapping responsibilities: + +```typescript +// ❌ Too vague — overlapping roles +const agents = [ + { instructions: "Help with code" }, + { instructions: "Assist with programming" }, +]; + +// ✅ Focused — clear separation of concerns +const agents = [ + { instructions: "Review code for security vulnerabilities. Flag SQL injection, XSS, and auth issues." }, + { instructions: "Optimize code performance. Focus on algorithmic complexity and memory usage." }, +]; +``` + +### Handle errors at the orchestration level + +Wrap agent calls in error handling, especially in multi-agent workflows where one agent's failure shouldn't block the entire pipeline: + + +```csharp +try +{ + string result = await pipeline.RunAsync("Analyze this module"); + Console.WriteLine(result); +} +catch (AgentException ex) +{ + Console.Error.WriteLine($"Agent {ex.AgentName} failed: {ex.Message}"); + // Fall back to single-agent mode or retry +} +``` + +## See Also + +- [Getting Started](../getting-started.md) — initial Copilot SDK setup +- [Custom Agents](./custom-agents.md) — define specialized sub-agents within the SDK +- [Custom Skills](./skills.md) — reusable prompt modules +- [Microsoft Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/agents/providers/github-copilot) — official MAF docs for the Copilot provider +- [Blog: Build AI Agents with GitHub Copilot SDK and Microsoft Agent Framework](https://devblogs.microsoft.com/semantic-kernel/build-ai-agents-with-github-copilot-sdk-and-microsoft-agent-framework/) From 416f614e27df1701a5f5e20e5090b9c5b17cb200 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:26:34 -0800 Subject: [PATCH 03/61] Add changelog for v0.1.31 (#705) Co-authored-by: github-actions[bot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abbfefc43..336dd374d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,58 @@ All notable changes to the Copilot SDK are documented in this file. This changelog is automatically generated by an AI agent when stable releases are published. See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list. +## [v0.1.31](https://github.com/github/copilot-sdk/releases/tag/v0.1.31) (2026-03-07) + +### Feature: multi-client tool and permission broadcasts (protocol v3) + +The SDK now uses protocol version 3, where the runtime broadcasts `external_tool.requested` and `permission.requested` as session events to all connected clients. This enables multi-client architectures where different clients contribute different tools, or where multiple clients observe the same permission prompts — if one client approves, all clients see the result. Your existing tool and permission handler code is unchanged. ([#686](https://github.com/github/copilot-sdk/pull/686)) + +```ts +// Two clients each register different tools; the agent can use both +const session1 = await client1.createSession({ + tools: [defineTool("search", { handler: doSearch })], + onPermissionRequest: approveAll, +}); +const session2 = await client2.resumeSession(session1.id, { + tools: [defineTool("analyze", { handler: doAnalyze })], + onPermissionRequest: approveAll, +}); +``` + +```cs +var session1 = await client1.CreateSessionAsync(new SessionConfig { + Tools = [AIFunctionFactory.Create(DoSearch, "search")], + OnPermissionRequest = PermissionHandlers.ApproveAll, +}); +var session2 = await client2.ResumeSessionAsync(session1.Id, new ResumeSessionConfig { + Tools = [AIFunctionFactory.Create(DoAnalyze, "analyze")], + OnPermissionRequest = PermissionHandlers.ApproveAll, +}); +``` + +### Feature: strongly-typed `PermissionRequestResultKind` for .NET and Go + +Rather than comparing `result.Kind` against undiscoverable magic strings like `"approved"` or `"denied-interactively-by-user"`, .NET and Go now provide typed constants. Node and Python already had typed unions for this; this brings full parity. ([#631](https://github.com/github/copilot-sdk/pull/631)) + +```cs +session.OnPermissionCompleted += (e) => { + if (e.Result.Kind == PermissionRequestResultKind.Approved) { /* ... */ } + if (e.Result.Kind == PermissionRequestResultKind.DeniedInteractivelyByUser) { /* ... */ } +}; +``` + +```go +// Go: PermissionKindApproved, PermissionKindDeniedByRules, +// PermissionKindDeniedCouldNotRequestFromUser, PermissionKindDeniedInteractivelyByUser +if result.Kind == copilot.PermissionKindApproved { /* ... */ } +``` + +### Other changes + +- feature: **[Python]** **[Go]** add `get_last_session_id()` / `GetLastSessionID()` for SDK-wide parity (was already available in Node and .NET) ([#671](https://github.com/github/copilot-sdk/pull/671)) +- improvement: **[Python]** add `timeout` parameter to generated RPC methods, allowing callers to override the default 30s timeout for long-running operations ([#681](https://github.com/github/copilot-sdk/pull/681)) +- bugfix: **[Go]** `PermissionRequest` fields are now properly typed (`ToolName`, `Diff`, `Path`, etc.) instead of a generic `Extra map[string]any` catch-all ([#685](https://github.com/github/copilot-sdk/pull/685)) + ## [v0.1.30](https://github.com/github/copilot-sdk/releases/tag/v0.1.30) (2026-03-03) ### Feature: support overriding built-in tools From 667712e2fd598b01194aa8bad9e622ade6ecc502 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:29:53 -0800 Subject: [PATCH 04/61] Add changelog for v0.1.32 (#708) Co-authored-by: github-actions[bot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Patrick Nikoletich --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336dd374d6..ac5712aa58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to the Copilot SDK are documented in this file. This changelog is automatically generated by an AI agent when stable releases are published. See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the full list. +## [v0.1.32](https://github.com/github/copilot-sdk/releases/tag/v0.1.32) (2026-03-07) + +### Feature: backward compatibility with v2 CLI servers + +SDK applications written against the v3 API now also work when connected to a v2 CLI server, with no code changes required. The SDK detects the server's protocol version and automatically adapts v2 `tool.call` and `permission.request` messages into the same user-facing handlers used by v3. ([#706](https://github.com/github/copilot-sdk/pull/706)) + +```ts +const session = await client.createSession({ + tools: [myTool], // unchanged — works with v2 and v3 servers + onPermissionRequest: approveAll, +}); +``` + +```cs +var session = await client.CreateSessionAsync(new SessionConfig { + Tools = [myTool], // unchanged — works with v2 and v3 servers + OnPermissionRequest = approveAll, +}); +``` + ## [v0.1.31](https://github.com/github/copilot-sdk/releases/tag/v0.1.31) (2026-03-07) ### Feature: multi-client tool and permission broadcasts (protocol v3) From 9595cdad20638584dc4e195db4868bf3fb23c2d2 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 22:38:48 -0800 Subject: [PATCH 05/61] docs: add streaming session events field-level reference guide (#717) Add docs/guides/streaming-events.md documenting every session event type with its exact data payload fields, required vs optional status, and descriptions. Includes: - Per-event field tables for all ~40 event types - Ephemeral vs persisted classification - Agentic turn flow diagram showing event ordering - Quick reference summary table - Multi-language subscription examples (TS, Python, Go, .NET) - Permission request discriminated union breakdown - Notes on SDK type differences (unified Data vs per-event types) Addresses user request for a field mapping so developers don't have to read SDK source code to discover which fields are populated on which event types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/streaming-events.md | 724 ++++++++++++++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100644 docs/guides/streaming-events.md diff --git a/docs/guides/streaming-events.md b/docs/guides/streaming-events.md new file mode 100644 index 0000000000..f33e5fa212 --- /dev/null +++ b/docs/guides/streaming-events.md @@ -0,0 +1,724 @@ +# Streaming Session Events + +Every action the Copilot agent takes — thinking, writing code, running tools — is emitted as a **session event** you can subscribe to. This guide is a field-level reference for each event type so you know exactly what data to expect without reading the SDK source. + +## Overview + +When `streaming: true` is set on a session, the SDK emits **ephemeral** events in real time (deltas, progress updates) alongside **persisted** events (complete messages, tool results). All events share a common envelope and carry a `data` payload whose shape depends on the event `type`. + +```mermaid +sequenceDiagram + participant App as Your App + participant SDK as SDK Session + participant Agent as Copilot Agent + + App->>SDK: send({ prompt }) + SDK->>Agent: JSON-RPC + + Agent-->>SDK: assistant.turn_start + SDK-->>App: event + + loop Streaming response + Agent-->>SDK: assistant.message_delta (ephemeral) + SDK-->>App: event + end + + Agent-->>SDK: assistant.message + SDK-->>App: event + + loop Tool execution + Agent-->>SDK: tool.execution_start + SDK-->>App: event + Agent-->>SDK: tool.execution_complete + SDK-->>App: event + end + + Agent-->>SDK: assistant.turn_end + SDK-->>App: event + + Agent-->>SDK: session.idle (ephemeral) + SDK-->>App: event +``` + +| Concept | Description | +|---------|-------------| +| **Ephemeral event** | Transient; streamed in real time but **not** persisted to the session log. Not replayed on session resume. | +| **Persisted event** | Saved to the session event log on disk. Replayed when resuming a session. | +| **Delta event** | An ephemeral streaming chunk (text or reasoning). Accumulate deltas to build the complete content. | +| **`parentId` chain** | Each event's `parentId` points to the previous event, forming a linked list you can walk. | + +## Event Envelope + +Every session event, regardless of type, includes these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` (UUID v4) | Unique event identifier | +| `timestamp` | `string` (ISO 8601) | When the event was created | +| `parentId` | `string \| null` | ID of the previous event in the chain; `null` for the first event | +| `ephemeral` | `boolean?` | `true` for transient events; absent or `false` for persisted events | +| `type` | `string` | Event type discriminator (see tables below) | +| `data` | `object` | Event-specific payload | + +## Subscribing to Events + +
+Node.js / TypeScript + +```typescript +// All events +session.on((event) => { + console.log(event.type, event.data); +}); + +// Specific event type — data is narrowed automatically +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); +}); +``` + +
+ +
+Python + +```python +from copilot.generated.session_events import SessionEventType + +def handle(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content, end="", flush=True) + +session.on(handle) +``` + +
+ +
+Go + + +```go +session.On(func(event copilot.SessionEvent) { + if event.Type == "assistant.message_delta" { + fmt.Print(*event.Data.DeltaContent) + } +}) +``` + +
+ +
+.NET + + +```csharp +session.On(evt => +{ + if (evt is AssistantMessageDeltaEvent delta) + { + Console.Write(delta.Data.DeltaContent); + } +}); +``` + +
+ +> **Tip (Python / Go):** These SDKs use a single `Data` class/struct with all possible fields as optional/nullable. Only the fields listed in the tables below are populated for each event type — the rest will be `None` / `nil`. +> +> **Tip (.NET):** The .NET SDK uses separate, strongly-typed data classes per event (e.g., `AssistantMessageDeltaData`), so only the relevant fields exist on each type. +> +> **Tip (TypeScript):** The TypeScript SDK uses a discriminated union — when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape. + +--- + +## Assistant Events + +These events track the agent's response lifecycle — from turn start through streaming chunks to the final message. + +### `assistant.turn_start` + +Emitted when the agent begins processing a turn. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `turnId` | `string` | ✅ | Turn identifier (typically a stringified turn number) | +| `interactionId` | `string` | | CAPI interaction ID for telemetry correlation | + +### `assistant.intent` + +Ephemeral. Short description of what the agent is currently doing, updated as it works. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `intent` | `string` | ✅ | Human-readable intent (e.g., "Exploring codebase") | + +### `assistant.reasoning` + +Complete extended thinking block from the model. Emitted after reasoning is finished. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `reasoningId` | `string` | ✅ | Unique identifier for this reasoning block | +| `content` | `string` | ✅ | The complete extended thinking text | + +### `assistant.reasoning_delta` + +Ephemeral. Incremental chunk of the model's extended thinking, streamed in real time. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `reasoningId` | `string` | ✅ | Matches the corresponding `assistant.reasoning` event | +| `deltaContent` | `string` | ✅ | Text chunk to append to reasoning content | + +### `assistant.message` + +The assistant's complete response for this LLM call. May include tool invocation requests. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `messageId` | `string` | ✅ | Unique identifier for this message | +| `content` | `string` | ✅ | The assistant's text response | +| `toolRequests` | `ToolRequest[]` | | Tool calls the assistant wants to make (see below) | +| `reasoningOpaque` | `string` | | Encrypted extended thinking (Anthropic models); session-bound | +| `reasoningText` | `string` | | Readable reasoning text from extended thinking | +| `encryptedContent` | `string` | | Encrypted reasoning content (OpenAI models); session-bound | +| `phase` | `string` | | Generation phase (e.g., `"thinking"` vs `"response"`) | +| `outputTokens` | `number` | | Actual output token count from the API response | +| `interactionId` | `string` | | CAPI interaction ID for telemetry | +| `parentToolCallId` | `string` | | Set when this message originates from a sub-agent | + +**`ToolRequest` fields:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Unique ID for this tool call | +| `name` | `string` | ✅ | Tool name (e.g., `"bash"`, `"edit"`, `"grep"`) | +| `arguments` | `object` | | Parsed arguments for the tool | +| `type` | `"function" \| "custom"` | | Call type; defaults to `"function"` when absent | + +### `assistant.message_delta` + +Ephemeral. Incremental chunk of the assistant's text response, streamed in real time. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `messageId` | `string` | ✅ | Matches the corresponding `assistant.message` event | +| `deltaContent` | `string` | ✅ | Text chunk to append to the message | +| `parentToolCallId` | `string` | | Set when originating from a sub-agent | + +### `assistant.turn_end` + +Emitted when the agent finishes a turn (all tool executions complete, final response delivered). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `turnId` | `string` | ✅ | Matches the corresponding `assistant.turn_start` event | + +### `assistant.usage` + +Ephemeral. Token usage and cost information for an individual API call. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `model` | `string` | ✅ | Model identifier (e.g., `"gpt-4.1"`) | +| `inputTokens` | `number` | | Input tokens consumed | +| `outputTokens` | `number` | | Output tokens produced | +| `cacheReadTokens` | `number` | | Tokens read from prompt cache | +| `cacheWriteTokens` | `number` | | Tokens written to prompt cache | +| `cost` | `number` | | Model multiplier cost for billing | +| `duration` | `number` | | API call duration in milliseconds | +| `initiator` | `string` | | What triggered this call (e.g., `"sub-agent"`); absent for user-initiated | +| `apiCallId` | `string` | | Completion ID from the provider (e.g., `chatcmpl-abc123`) | +| `providerCallId` | `string` | | GitHub request tracing ID (`x-github-request-id`) | +| `parentToolCallId` | `string` | | Set when usage originates from a sub-agent | +| `quotaSnapshots` | `Record` | | Per-quota resource usage, keyed by quota identifier | +| `copilotUsage` | `CopilotUsage` | | Itemized token cost breakdown from the API | + +### `assistant.streaming_delta` + +Ephemeral. Low-level network progress indicator — total bytes received from the streaming API response. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `totalResponseSizeBytes` | `number` | ✅ | Cumulative bytes received so far | + +--- + +## Tool Execution Events + +These events track the full lifecycle of each tool invocation — from the model requesting a tool call through execution to completion. + +### `tool.execution_start` + +Emitted when a tool begins executing. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Unique identifier for this tool call | +| `toolName` | `string` | ✅ | Name of the tool (e.g., `"bash"`, `"edit"`, `"grep"`) | +| `arguments` | `object` | | Parsed arguments passed to the tool | +| `mcpServerName` | `string` | | MCP server name, when the tool is provided by an MCP server | +| `mcpToolName` | `string` | | Original tool name on the MCP server | +| `parentToolCallId` | `string` | | Set when invoked by a sub-agent | + +### `tool.execution_partial_result` + +Ephemeral. Incremental output from a running tool (e.g., streaming bash output). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | +| `partialOutput` | `string` | ✅ | Incremental output chunk | + +### `tool.execution_progress` + +Ephemeral. Human-readable progress status from a running tool (e.g., MCP server progress notifications). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | +| `progressMessage` | `string` | ✅ | Progress status message | + +### `tool.execution_complete` + +Emitted when a tool finishes executing — successfully or with an error. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Matches the corresponding `tool.execution_start` | +| `success` | `boolean` | ✅ | Whether execution succeeded | +| `model` | `string` | | Model that generated this tool call | +| `interactionId` | `string` | | CAPI interaction ID | +| `isUserRequested` | `boolean` | | `true` when the user explicitly requested this tool call | +| `result` | `Result` | | Present on success (see below) | +| `error` | `{ message, code? }` | | Present on failure | +| `toolTelemetry` | `object` | | Tool-specific telemetry (e.g., CodeQL check counts) | +| `parentToolCallId` | `string` | | Set when invoked by a sub-agent | + +**`Result` fields:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `content` | `string` | ✅ | Concise result sent to the LLM (may be truncated for token efficiency) | +| `detailedContent` | `string` | | Full result for display, preserving complete content like diffs | +| `contents` | `ContentBlock[]` | | Structured content blocks (text, terminal, image, audio, resource) | + +### `tool.user_requested` + +Emitted when the user explicitly requests a tool invocation (rather than the model choosing to call one). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Unique identifier for this tool call | +| `toolName` | `string` | ✅ | Name of the tool the user wants to invoke | +| `arguments` | `object` | | Arguments for the invocation | + +--- + +## Session Lifecycle Events + +### `session.idle` + +Ephemeral. The agent has finished all processing and is ready for the next message. This is the signal that a turn is fully complete. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `backgroundTasks` | `BackgroundTasks` | | Background agents/shells still running when the agent became idle | + +### `session.error` + +An error occurred during session processing. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `errorType` | `string` | ✅ | Error category (e.g., `"authentication"`, `"quota"`, `"rate_limit"`) | +| `message` | `string` | ✅ | Human-readable error message | +| `stack` | `string` | | Error stack trace | +| `statusCode` | `number` | | HTTP status code from the upstream request | +| `providerCallId` | `string` | | GitHub request tracing ID for server-side log correlation | + +### `session.compaction_start` + +Context window compaction has begun. **Data payload is empty (`{}`)**. + +### `session.compaction_complete` + +Context window compaction finished. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `success` | `boolean` | ✅ | Whether compaction succeeded | +| `error` | `string` | | Error message if compaction failed | +| `preCompactionTokens` | `number` | | Tokens before compaction | +| `postCompactionTokens` | `number` | | Tokens after compaction | +| `preCompactionMessagesLength` | `number` | | Message count before compaction | +| `messagesRemoved` | `number` | | Messages removed | +| `tokensRemoved` | `number` | | Tokens removed | +| `summaryContent` | `string` | | LLM-generated summary of compacted history | +| `checkpointNumber` | `number` | | Checkpoint snapshot number created for recovery | +| `checkpointPath` | `string` | | File path where the checkpoint was stored | +| `compactionTokensUsed` | `{ input, output, cachedInput }` | | Token usage for the compaction LLM call | +| `requestId` | `string` | | GitHub request tracing ID for the compaction call | + +### `session.title_changed` + +Ephemeral. The session's auto-generated title was updated. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `title` | `string` | ✅ | New session title | + +### `session.context_changed` + +The session's working directory or repository context changed. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `cwd` | `string` | ✅ | Current working directory | +| `gitRoot` | `string` | | Git repository root | +| `repository` | `string` | | Repository in `"owner/name"` format | +| `branch` | `string` | | Current git branch | + +### `session.usage_info` + +Ephemeral. Context window utilization snapshot. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `tokenLimit` | `number` | ✅ | Maximum tokens for the model's context window | +| `currentTokens` | `number` | ✅ | Current tokens in the context window | +| `messagesLength` | `number` | ✅ | Current message count in the conversation | + +### `session.task_complete` + +The agent has completed its assigned task. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `summary` | `string` | | Summary of the completed task | + +### `session.shutdown` + +The session has ended. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `shutdownType` | `"routine" \| "error"` | ✅ | Normal shutdown or crash | +| `errorReason` | `string` | | Error description when `shutdownType` is `"error"` | +| `totalPremiumRequests` | `number` | ✅ | Total premium API requests used | +| `totalApiDurationMs` | `number` | ✅ | Cumulative API call time in milliseconds | +| `sessionStartTime` | `number` | ✅ | Unix timestamp (ms) when the session started | +| `codeChanges` | `{ linesAdded, linesRemoved, filesModified }` | ✅ | Aggregate code change metrics | +| `modelMetrics` | `Record` | ✅ | Per-model usage breakdown | +| `currentModel` | `string` | | Model selected at shutdown time | + +--- + +## Permission & User Input Events + +These events are emitted when the agent needs approval or input from the user before continuing. + +### `permission.requested` + +Ephemeral. The agent needs permission to perform an action (run a command, write a file, etc.). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToPermission()` | +| `permissionRequest` | `PermissionRequest` | ✅ | Details of the permission being requested | + +The `permissionRequest` is a discriminated union on `kind`: + +| `kind` | Key Fields | Description | +|--------|------------|-------------| +| `"shell"` | `fullCommandText`, `intention`, `commands[]`, `possiblePaths[]` | Execute a shell command | +| `"write"` | `fileName`, `diff`, `intention`, `newFileContents?` | Write/modify a file | +| `"read"` | `path`, `intention` | Read a file or directory | +| `"mcp"` | `serverName`, `toolName`, `toolTitle`, `args?`, `readOnly` | Call an MCP tool | +| `"url"` | `url`, `intention` | Fetch a URL | +| `"memory"` | `subject`, `fact`, `citations` | Store a memory | +| `"custom-tool"` | `toolName`, `toolDescription`, `args?` | Call a custom tool | + +All `kind` variants also include an optional `toolCallId` linking back to the tool call that triggered the request. + +### `permission.completed` + +Ephemeral. A permission request was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `permission.requested` | +| `result.kind` | `string` | ✅ | One of: `"approved"`, `"denied-by-rules"`, `"denied-interactively-by-user"`, `"denied-no-approval-rule-and-could-not-request-from-user"`, `"denied-by-content-exclusion-policy"` | + +### `user_input.requested` + +Ephemeral. The agent is asking the user a question. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToUserInput()` | +| `question` | `string` | ✅ | The question to present to the user | +| `choices` | `string[]` | | Predefined choices for the user | +| `allowFreeform` | `boolean` | | Whether free-form text input is allowed | + +### `user_input.completed` + +Ephemeral. A user input request was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `user_input.requested` | + +### `elicitation.requested` + +Ephemeral. The agent needs structured form input from the user (MCP elicitation protocol). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToElicitation()` | +| `message` | `string` | ✅ | Description of what information is needed | +| `mode` | `"form"` | | Elicitation mode (currently only `"form"`) | +| `requestedSchema` | `{ type: "object", properties, required? }` | ✅ | JSON Schema describing the form fields | + +### `elicitation.completed` + +Ephemeral. An elicitation request was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `elicitation.requested` | + +--- + +## Sub-Agent & Skill Events + +### `subagent.started` + +A custom agent was invoked as a sub-agent. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Parent tool call that spawned this sub-agent | +| `agentName` | `string` | ✅ | Internal name of the sub-agent | +| `agentDisplayName` | `string` | ✅ | Human-readable display name | +| `agentDescription` | `string` | ✅ | Description of what the sub-agent does | + +### `subagent.completed` + +A sub-agent finished successfully. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Matches the corresponding `subagent.started` | +| `agentName` | `string` | ✅ | Internal name | +| `agentDisplayName` | `string` | ✅ | Display name | + +### `subagent.failed` + +A sub-agent encountered an error. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `toolCallId` | `string` | ✅ | Matches the corresponding `subagent.started` | +| `agentName` | `string` | ✅ | Internal name | +| `agentDisplayName` | `string` | ✅ | Display name | +| `error` | `string` | ✅ | Error message | + +### `subagent.selected` + +A custom agent was selected (inferred) to handle the current request. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `agentName` | `string` | ✅ | Internal name of the selected agent | +| `agentDisplayName` | `string` | ✅ | Display name | +| `tools` | `string[] \| null` | ✅ | Tool names available to this agent; `null` for all tools | + +### `subagent.deselected` + +A custom agent was deselected, returning to the default agent. **Data payload is empty (`{}`)**. + +### `skill.invoked` + +A skill was activated for the current conversation. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `name` | `string` | ✅ | Skill name | +| `path` | `string` | ✅ | File path to the SKILL.md definition | +| `content` | `string` | ✅ | Full skill content injected into the conversation | +| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active | +| `pluginName` | `string` | | Plugin the skill originated from | +| `pluginVersion` | `string` | | Plugin version | + +--- + +## Other Events + +### `abort` + +The current turn was aborted. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `reason` | `string` | ✅ | Why the turn was aborted (e.g., `"user initiated"`) | + +### `user.message` + +The user sent a message. Recorded for the session timeline. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `content` | `string` | ✅ | The user's message text | +| `transformedContent` | `string` | | Transformed version after preprocessing | +| `attachments` | `Attachment[]` | | File, directory, selection, or GitHub reference attachments | +| `source` | `string` | | Message source identifier | +| `agentMode` | `string` | | Agent mode: `"interactive"`, `"plan"`, `"autopilot"`, or `"shell"` | +| `interactionId` | `string` | | CAPI interaction ID | + +### `system.message` + +A system or developer prompt was injected into the conversation. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `content` | `string` | ✅ | The prompt text | +| `role` | `"system" \| "developer"` | ✅ | Message role | +| `name` | `string` | | Source identifier | +| `metadata` | `{ promptVersion?, variables? }` | | Prompt template metadata | + +### `external_tool.requested` + +Ephemeral. The agent wants to invoke an external tool (one provided by the SDK consumer). + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToExternalTool()` | +| `sessionId` | `string` | ✅ | Session this request belongs to | +| `toolCallId` | `string` | ✅ | Tool call ID for this invocation | +| `toolName` | `string` | ✅ | Name of the external tool | +| `arguments` | `object` | | Arguments for the tool | + +### `external_tool.completed` + +Ephemeral. An external tool request was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `external_tool.requested` | + +### `exit_plan_mode.requested` + +Ephemeral. The agent has created a plan and wants to exit plan mode. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToExitPlanMode()` | +| `summary` | `string` | ✅ | Summary of the plan | +| `planContent` | `string` | ✅ | Full plan file content | +| `actions` | `string[]` | ✅ | Available user actions (e.g., approve, edit, reject) | +| `recommendedAction` | `string` | ✅ | Suggested action | + +### `exit_plan_mode.completed` + +Ephemeral. An exit plan mode request was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `exit_plan_mode.requested` | + +### `command.queued` + +Ephemeral. A slash command was queued for execution. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Use this to respond via `session.respondToQueuedCommand()` | +| `command` | `string` | ✅ | The slash command text (e.g., `/help`, `/clear`) | + +### `command.completed` + +Ephemeral. A queued command was resolved. + +| Data Field | Type | Required | Description | +|------------|------|----------|-------------| +| `requestId` | `string` | ✅ | Matches the corresponding `command.queued` | + +--- + +## Quick Reference: Agentic Turn Flow + +A typical agentic turn emits events in this order: + +``` +assistant.turn_start → Turn begins +├── assistant.intent → What the agent plans to do (ephemeral) +├── assistant.reasoning_delta → Streaming thinking chunks (ephemeral, repeated) +├── assistant.reasoning → Complete thinking block +├── assistant.message_delta → Streaming response chunks (ephemeral, repeated) +├── assistant.message → Complete response (may include toolRequests) +├── assistant.usage → Token usage for this API call (ephemeral) +│ +├── [If tools were requested:] +│ ├── permission.requested → Needs user approval (ephemeral) +│ ├── permission.completed → Approval result (ephemeral) +│ ├── tool.execution_start → Tool begins +│ ├── tool.execution_partial_result → Streaming tool output (ephemeral, repeated) +│ ├── tool.execution_progress → Progress updates (ephemeral, repeated) +│ ├── tool.execution_complete → Tool finished +│ │ +│ └── [Agent loops: more reasoning → message → tool calls...] +│ +assistant.turn_end → Turn complete +session.idle → Ready for next message (ephemeral) +``` + +## All Event Types at a Glance + +| Event Type | Ephemeral | Category | Key Data Fields | +|------------|-----------|----------|-----------------| +| `assistant.turn_start` | | Assistant | `turnId`, `interactionId?` | +| `assistant.intent` | ✅ | Assistant | `intent` | +| `assistant.reasoning` | | Assistant | `reasoningId`, `content` | +| `assistant.reasoning_delta` | ✅ | Assistant | `reasoningId`, `deltaContent` | +| `assistant.streaming_delta` | ✅ | Assistant | `totalResponseSizeBytes` | +| `assistant.message` | | Assistant | `messageId`, `content`, `toolRequests?`, `outputTokens?`, `phase?` | +| `assistant.message_delta` | ✅ | Assistant | `messageId`, `deltaContent`, `parentToolCallId?` | +| `assistant.turn_end` | | Assistant | `turnId` | +| `assistant.usage` | ✅ | Assistant | `model`, `inputTokens?`, `outputTokens?`, `cost?`, `duration?` | +| `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` | +| `tool.execution_start` | | Tool | `toolCallId`, `toolName`, `arguments?`, `mcpServerName?` | +| `tool.execution_partial_result` | ✅ | Tool | `toolCallId`, `partialOutput` | +| `tool.execution_progress` | ✅ | Tool | `toolCallId`, `progressMessage` | +| `tool.execution_complete` | | Tool | `toolCallId`, `success`, `result?`, `error?` | +| `session.idle` | ✅ | Session | `backgroundTasks?` | +| `session.error` | | Session | `errorType`, `message`, `statusCode?` | +| `session.compaction_start` | | Session | *(empty)* | +| `session.compaction_complete` | | Session | `success`, `preCompactionTokens?`, `summaryContent?` | +| `session.title_changed` | ✅ | Session | `title` | +| `session.context_changed` | | Session | `cwd`, `gitRoot?`, `repository?`, `branch?` | +| `session.usage_info` | ✅ | Session | `tokenLimit`, `currentTokens`, `messagesLength` | +| `session.task_complete` | | Session | `summary?` | +| `session.shutdown` | | Session | `shutdownType`, `codeChanges`, `modelMetrics` | +| `permission.requested` | ✅ | Permission | `requestId`, `permissionRequest` | +| `permission.completed` | ✅ | Permission | `requestId`, `result.kind` | +| `user_input.requested` | ✅ | User Input | `requestId`, `question`, `choices?` | +| `user_input.completed` | ✅ | User Input | `requestId` | +| `elicitation.requested` | ✅ | User Input | `requestId`, `message`, `requestedSchema` | +| `elicitation.completed` | ✅ | User Input | `requestId` | +| `subagent.started` | | Sub-Agent | `toolCallId`, `agentName`, `agentDisplayName` | +| `subagent.completed` | | Sub-Agent | `toolCallId`, `agentName`, `agentDisplayName` | +| `subagent.failed` | | Sub-Agent | `toolCallId`, `agentName`, `error` | +| `subagent.selected` | | Sub-Agent | `agentName`, `agentDisplayName`, `tools` | +| `subagent.deselected` | | Sub-Agent | *(empty)* | +| `skill.invoked` | | Skill | `name`, `path`, `content`, `allowedTools?` | +| `abort` | | Control | `reason` | +| `user.message` | | User | `content`, `attachments?`, `agentMode?` | +| `system.message` | | System | `content`, `role` | +| `external_tool.requested` | ✅ | External Tool | `requestId`, `toolName`, `arguments?` | +| `external_tool.completed` | ✅ | External Tool | `requestId` | +| `command.queued` | ✅ | Command | `requestId`, `command` | +| `command.completed` | ✅ | Command | `requestId` | +| `exit_plan_mode.requested` | ✅ | Plan Mode | `requestId`, `summary`, `planContent`, `actions` | +| `exit_plan_mode.completed` | ✅ | Plan Mode | `requestId` | From 310e7b461d4a4d481bab117dff2b7ac020b40c3d Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 22:57:59 -0800 Subject: [PATCH 06/61] docs: add image input guide (#719) * docs: add image input guide Add docs/guides/image-input.md documenting how to send images/visual input to Copilot sessions via file attachments. Covers: - Quick start examples in all 4 SDK languages (TS, Python, Go, .NET) - Supported image formats (JPG, PNG, GIF, and other common types) - Automatic image processing by the runtime (resizing, quality reduction) - Vision model capability fields for checking support - Receiving image results from tool execution content blocks - Practical tips and limitations All code blocks verified via docs validation. All claims cross-checked against SDK source code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add docs-validate skip for Python snippet in streaming-events guide The Python subscription example uses 'session' without defining it, which fails mypy validation in CI. Add skip directive to match the Go and .NET snippets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/image-input.md | 219 ++++++++++++++++++++++++++++++++ docs/guides/streaming-events.md | 1 + 2 files changed, 220 insertions(+) create mode 100644 docs/guides/image-input.md diff --git a/docs/guides/image-input.md b/docs/guides/image-input.md new file mode 100644 index 0000000000..acf8737a0b --- /dev/null +++ b/docs/guides/image-input.md @@ -0,0 +1,219 @@ +# Image Input + +Send images to Copilot sessions by attaching them as file attachments. The runtime reads the file from disk, converts it to base64 internally, and sends it to the LLM as an image content block — no manual encoding required. + +## Overview + +```mermaid +sequenceDiagram + participant App as Your App + participant SDK as SDK Session + participant RT as Copilot Runtime + participant LLM as Vision Model + + App->>SDK: send({ prompt, attachments: [{ type: "file", path }] }) + SDK->>RT: JSON-RPC with file attachment + RT->>RT: Read file from disk + RT->>RT: Detect image, convert to base64 + RT->>RT: Resize if needed (model-specific limits) + RT->>LLM: image_url content block (base64) + LLM-->>RT: Response referencing the image + RT-->>SDK: assistant.message events + SDK-->>App: event stream +``` + +| Concept | Description | +|---------|-------------| +| **File attachment** | An attachment with `type: "file"` and an absolute `path` to an image on disk | +| **Automatic encoding** | The runtime reads the image, converts it to base64, and sends it as an `image_url` block | +| **Auto-resize** | The runtime automatically resizes or quality-reduces images that exceed model-specific limits | +| **Vision capability** | The model must have `capabilities.supports.vision = true` to process images | + +## Quick Start + +Attach an image file to any message using the file attachment type. The path must be an absolute path to an image on disk. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +await session.send({ + prompt: "Describe what you see in this image", + attachments: [ + { + type: "file", + path: "/absolute/path/to/screenshot.png", + }, + ], +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import PermissionRequestResult + +client = CopilotClient() +await client.start() + +session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), +}) + +await session.send({ + "prompt": "Describe what you see in this image", + "attachments": [ + { + "type": "file", + "path": "/absolute/path/to/screenshot.png", + }, + ], +}) +``` + +
+ +
+Go + + +```go +ctx := context.Background() +client := copilot.NewClient(nil) +client.Start(ctx) + +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, +}) + +path := "/absolute/path/to/screenshot.png" +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.File, + Path: &path, + }, + }, +}) +``` + +
+ +
+.NET + + +```csharp +using GitHub.Copilot.SDK; + +await using var client = new CopilotClient(); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), +}); + +await session.SendAsync(new MessageOptions +{ + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemFile + { + Path = "/absolute/path/to/screenshot.png", + DisplayName = "screenshot.png", + }, + }, +}); +``` + +
+ +## Supported Formats + +Supported image formats include JPG, PNG, GIF, and other common image types. The runtime reads the image from disk and converts it as needed before sending to the LLM. Use PNG or JPEG for best results, as these are the most widely supported formats. + +The model's `capabilities.limits.vision.supported_media_types` field lists the exact MIME types it accepts. + +## Automatic Processing + +The runtime automatically processes images to fit within the model's constraints. No manual resizing is required. + +- Images that exceed the model's dimension or size limits are automatically resized (preserving aspect ratio) or quality-reduced. +- If an image cannot be brought within limits after processing, it is skipped and not sent to the LLM. +- The model's `capabilities.limits.vision.max_prompt_image_size` field indicates the maximum image size in bytes. + +You can check these limits at runtime via the model capabilities object. For the best experience, use reasonably-sized PNG or JPEG images. + +## Vision Model Capabilities + +Not all models support vision. Check the model's capabilities before sending images. + +### Capability fields + +| Field | Type | Description | +|-------|------|-------------| +| `capabilities.supports.vision` | `boolean` | Whether the model can process image inputs | +| `capabilities.limits.vision.supported_media_types` | `string[]` | MIME types the model accepts (e.g., `["image/png", "image/jpeg"]`) | +| `capabilities.limits.vision.max_prompt_images` | `number` | Maximum number of images per prompt | +| `capabilities.limits.vision.max_prompt_image_size` | `number` | Maximum image size in bytes | + +### Vision limits type + + +```typescript +vision?: { + supported_media_types: string[]; + max_prompt_images: number; + max_prompt_image_size: number; // bytes +}; +``` + +## Receiving Image Results + +When tools return images (e.g., screenshots or generated charts), the result contains `"image"` content blocks with base64-encoded data. + +| Field | Type | Description | +|-------|------|-------------| +| `type` | `"image"` | Content block type discriminator | +| `data` | `string` | Base64-encoded image data | +| `mimeType` | `string` | MIME type (e.g., `"image/png"`) | + +These image blocks appear in `tool.execution_complete` event results. See the [Streaming Events](./streaming-events.md) guide for the full event lifecycle. + +## Tips & Limitations + +| Tip | Details | +|-----|---------| +| **Use PNG or JPEG directly** | Avoids conversion overhead — these are sent to the LLM as-is | +| **Keep images reasonably sized** | Large images may be quality-reduced, which can lose important details | +| **Use absolute paths** | The runtime reads files from disk; relative paths may not resolve correctly | +| **Check vision support first** | Sending images to a non-vision model wastes tokens on the file path without visual understanding | +| **Multiple images are supported** | Attach several file attachments in one message, up to the model's `max_prompt_images` limit | +| **Images are not base64 in your code** | You provide a file path — the runtime handles encoding, resizing, and format conversion | +| **SVG is not supported** | SVG files are text-based and excluded from image processing | + +## See Also + +- [Streaming Events](./streaming-events.md) — event lifecycle including tool result content blocks +- [Steering & Queueing](./steering-and-queueing.md) — sending follow-up messages with attachments diff --git a/docs/guides/streaming-events.md b/docs/guides/streaming-events.md index f33e5fa212..72259858b8 100644 --- a/docs/guides/streaming-events.md +++ b/docs/guides/streaming-events.md @@ -82,6 +82,7 @@ session.on("assistant.message_delta", (event) => {
Python + ```python from copilot.generated.session_events import SessionEventType From c786a3549fe3e212d2f620ed98727d7cb6840e30 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 22:59:02 -0800 Subject: [PATCH 07/61] docs: add hooks guide covering permissions, auditing, and notifications (#720) * docs: add image input guide Add docs/guides/image-input.md documenting how to send images/visual input to Copilot sessions via file attachments. Covers: - Quick start examples in all 4 SDK languages (TS, Python, Go, .NET) - Supported image formats (JPG, PNG, GIF, and other common types) - Automatic image processing by the runtime (resizing, quality reduction) - Vision model capability fields for checking support - Receiving image results from tool execution content blocks - Practical tips and limitations All code blocks verified via docs validation. All claims cross-checked against SDK source code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add hooks guide covering permissions, auditing, and notifications Add a use-case-oriented guide for session hooks that covers: - Permission control (allow-lists, directory restrictions, ask-before-destructive) - Auditing & compliance (structured audit logs, secret redaction) - Notifications & sounds (desktop notifications, Slack webhooks, system sounds) - Prompt enrichment (project metadata injection, shorthand expansion) - Error handling & recovery (retry logic, friendly messages) - Session metrics (duration, tool call counts, end reasons) Includes multi-language examples (TypeScript, Python, Go, .NET) and links to the existing API reference docs in docs/hooks/. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add docs-validate skip for Python snippet in streaming-events guide The Python subscription example uses 'session' without defining it, which fails mypy validation in CI. Add skip directive to match the Go and .NET snippets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: skip docs validation for Python aiofiles example The audit log example imports aiofiles which lacks type stubs in the validation environment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/hooks.md | 843 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 843 insertions(+) create mode 100644 docs/guides/hooks.md diff --git a/docs/guides/hooks.md b/docs/guides/hooks.md new file mode 100644 index 0000000000..60f5cfe714 --- /dev/null +++ b/docs/guides/hooks.md @@ -0,0 +1,843 @@ +# Working with Hooks + +Hooks let you plug custom logic into every stage of a Copilot session — from the moment it starts, through each user prompt and tool call, to the moment it ends. This guide walks through practical use cases so you can ship permissions, auditing, notifications, and more without modifying the core agent behavior. + +## Overview + +A hook is a callback you register once when creating a session. The SDK invokes it at a well-defined point in the conversation lifecycle, passes contextual input, and optionally accepts output that modifies the session's behavior. + +```mermaid +flowchart LR + A[Session starts] -->|onSessionStart| B[User sends prompt] + B -->|onUserPromptSubmitted| C[Agent picks a tool] + C -->|onPreToolUse| D[Tool executes] + D -->|onPostToolUse| E{More work?} + E -->|yes| C + E -->|no| F[Session ends] + F -->|onSessionEnd| G((Done)) + C -.->|error| H[onErrorOccurred] + D -.->|error| H +``` + +| Hook | When it fires | What you can do | +|------|---------------|-----------------| +| [`onSessionStart`](../hooks/session-lifecycle.md#session-start) | Session begins (new or resumed) | Inject context, load preferences | +| [`onUserPromptSubmitted`](../hooks/user-prompt-submitted.md) | User sends a message | Rewrite prompts, add context, filter input | +| [`onPreToolUse`](../hooks/pre-tool-use.md) | Before a tool executes | Allow / deny / modify the call | +| [`onPostToolUse`](../hooks/post-tool-use.md) | After a tool returns | Transform results, redact secrets, audit | +| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end) | Session ends | Clean up, record metrics | +| [`onErrorOccurred`](../hooks/error-handling.md) | An error is raised | Custom logging, retry logic, alerts | + +All hooks are **optional** — register only the ones you need. Returning `null` (or the language equivalent) from any hook tells the SDK to continue with default behavior. + +## Registering Hooks + +Pass a `hooks` object when you create (or resume) a session. Every example below follows this pattern. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { /* ... */ }, + onPreToolUse: async (input, invocation) => { /* ... */ }, + onPostToolUse: async (input, invocation) => { /* ... */ }, + // ... add only the hooks you need + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient + +client = CopilotClient() +await client.start() + +session = await client.create_session({ + "hooks": { + "on_session_start": on_session_start, + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + # ... add only the hooks you need + }, + "on_permission_request": lambda req, inv: {"kind": "approved"}, +}) +``` + +
+ +
+Go + + +```go +client := copilot.NewClient(nil) + +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnSessionStart: onSessionStart, + OnPreToolUse: onPreToolUse, + OnPostToolUse: onPostToolUse, + // ... add only the hooks you need + }, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: "approved"}, nil + }, +}) +``` + +
+ +
+.NET + + +```csharp +var client = new CopilotClient(); + +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnSessionStart = onSessionStart, + OnPreToolUse = onPreToolUse, + OnPostToolUse = onPostToolUse, + // ... add only the hooks you need + }, + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), +}); +``` + +
+ +> **Tip:** Every hook handler receives an `invocation` parameter containing the `sessionId`, which is useful for correlating logs and maintaining per-session state. + +--- + +## Use Case: Permission Control + +Use `onPreToolUse` to build a permission layer that decides which tools the agent may run, what arguments are allowed, and whether the user should be prompted before execution. + +### Allow-list a safe set of tools + +
+Node.js / TypeScript + +```typescript +const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (!READ_ONLY_TOOLS.includes(input.toolName)) { + return { + permissionDecision: "deny", + permissionDecisionReason: + `Only read-only tools are allowed. "${input.toolName}" was blocked.`, + }; + } + return { permissionDecision: "allow" }; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +
+ +
+Python + +```python +READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"] + +async def on_pre_tool_use(input_data, invocation): + if input_data["toolName"] not in READ_ONLY_TOOLS: + return { + "permissionDecision": "deny", + "permissionDecisionReason": + f'Only read-only tools are allowed. "{input_data["toolName"]}" was blocked.', + } + return {"permissionDecision": "allow"} + +session = await client.create_session({ + "hooks": {"on_pre_tool_use": on_pre_tool_use}, + "on_permission_request": lambda req, inv: {"kind": "approved"}, +}) +``` + +
+ +
+Go + + +```go +readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true} + +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + if !readOnlyTools[input.ToolName] { + return &copilot.PreToolUseHookOutput{ + PermissionDecision: "deny", + PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName), + }, nil + } + return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil + }, + }, +}) +``` + +
+ +
+.NET + + +```csharp +var readOnlyTools = new HashSet { "read_file", "glob", "grep", "view" }; + +var session = await client.CreateSessionAsync(new SessionConfig +{ + Hooks = new SessionHooks + { + OnPreToolUse = (input, invocation) => + { + if (!readOnlyTools.Contains(input.ToolName)) + { + return Task.FromResult(new PreToolUseHookOutput + { + PermissionDecision = "deny", + PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.", + }); + } + return Task.FromResult( + new PreToolUseHookOutput { PermissionDecision = "allow" }); + }, + }, +}); +``` + +
+ +### Restrict file access to specific directories + +```typescript +const ALLOWED_DIRS = ["/home/user/projects", "/tmp"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (["read_file", "write_file", "edit"].includes(input.toolName)) { + const filePath = (input.toolArgs as { path: string }).path; + const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir)); + + if (!allowed) { + return { + permissionDecision: "deny", + permissionDecisionReason: + `Access to "${filePath}" is outside the allowed directories.`, + }; + } + } + return { permissionDecision: "allow" }; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +### Ask the user before destructive operations + +```typescript +const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"]; + +const session = await client.createSession({ + hooks: { + onPreToolUse: async (input) => { + if (DESTRUCTIVE_TOOLS.includes(input.toolName)) { + return { permissionDecision: "ask" }; + } + return { permissionDecision: "allow" }; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +Returning `"ask"` delegates the decision to the user at runtime — useful for destructive actions where you want a human in the loop. + +--- + +## Use Case: Auditing & Compliance + +Combine `onPreToolUse`, `onPostToolUse`, and the session lifecycle hooks to build a complete audit trail that records every action the agent takes. + +### Structured audit log + +
+Node.js / TypeScript + +```typescript +interface AuditEntry { + timestamp: number; + sessionId: string; + event: string; + toolName?: string; + toolArgs?: unknown; + toolResult?: unknown; + prompt?: string; +} + +const auditLog: AuditEntry[] = []; + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + event: "session_start", + }); + return null; + }, + onUserPromptSubmitted: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + event: "user_prompt", + prompt: input.prompt, + }); + return null; + }, + onPreToolUse: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + event: "tool_call", + toolName: input.toolName, + toolArgs: input.toolArgs, + }); + return { permissionDecision: "allow" }; + }, + onPostToolUse: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + event: "tool_result", + toolName: input.toolName, + toolResult: input.toolResult, + }); + return null; + }, + onSessionEnd: async (input, invocation) => { + auditLog.push({ + timestamp: input.timestamp, + sessionId: invocation.sessionId, + event: "session_end", + }); + + // Persist the log — swap this with your own storage backend + await fs.promises.writeFile( + `audit-${invocation.sessionId}.json`, + JSON.stringify(auditLog, null, 2), + ); + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +
+ +
+Python + + +```python +import json, aiofiles + +audit_log = [] + +async def on_session_start(input_data, invocation): + audit_log.append({ + "timestamp": input_data["timestamp"], + "session_id": invocation["session_id"], + "event": "session_start", + }) + return None + +async def on_user_prompt_submitted(input_data, invocation): + audit_log.append({ + "timestamp": input_data["timestamp"], + "session_id": invocation["session_id"], + "event": "user_prompt", + "prompt": input_data["prompt"], + }) + return None + +async def on_pre_tool_use(input_data, invocation): + audit_log.append({ + "timestamp": input_data["timestamp"], + "session_id": invocation["session_id"], + "event": "tool_call", + "tool_name": input_data["toolName"], + "tool_args": input_data["toolArgs"], + }) + return {"permissionDecision": "allow"} + +async def on_post_tool_use(input_data, invocation): + audit_log.append({ + "timestamp": input_data["timestamp"], + "session_id": invocation["session_id"], + "event": "tool_result", + "tool_name": input_data["toolName"], + "tool_result": input_data["toolResult"], + }) + return None + +async def on_session_end(input_data, invocation): + audit_log.append({ + "timestamp": input_data["timestamp"], + "session_id": invocation["session_id"], + "event": "session_end", + }) + async with aiofiles.open(f"audit-{invocation['session_id']}.json", "w") as f: + await f.write(json.dumps(audit_log, indent=2)) + return None + +session = await client.create_session({ + "hooks": { + "on_session_start": on_session_start, + "on_user_prompt_submitted": on_user_prompt_submitted, + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_session_end": on_session_end, + }, + "on_permission_request": lambda req, inv: {"kind": "approved"}, +}) +``` + +
+ +### Redact secrets from tool results + +```typescript +const SECRET_PATTERNS = [ + /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi, +]; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + if (typeof input.toolResult !== "string") return null; + + let redacted = input.toolResult; + for (const pattern of SECRET_PATTERNS) { + redacted = redacted.replace(pattern, "[REDACTED]"); + } + + return redacted !== input.toolResult + ? { modifiedResult: redacted } + : null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +--- + +## Use Case: Notifications & Sounds + +Hooks fire in your application's process, so you can trigger any side-effect — desktop notifications, sounds, Slack messages, or webhook calls. + +### Desktop notification on session events + +
+Node.js / TypeScript + +```typescript +import notifier from "node-notifier"; // npm install node-notifier + +const session = await client.createSession({ + hooks: { + onSessionEnd: async (input, invocation) => { + notifier.notify({ + title: "Copilot Session Complete", + message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`, + }); + return null; + }, + onErrorOccurred: async (input) => { + notifier.notify({ + title: "Copilot Error", + message: input.error.slice(0, 200), + }); + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +
+ +
+Python + +```python +import subprocess + +async def on_session_end(input_data, invocation): + sid = invocation["session_id"][:8] + reason = input_data["reason"] + subprocess.Popen([ + "notify-send", "Copilot Session Complete", + f"Session {sid} finished ({reason}).", + ]) + return None + +async def on_error_occurred(input_data, invocation): + subprocess.Popen([ + "notify-send", "Copilot Error", + input_data["error"][:200], + ]) + return None + +session = await client.create_session({ + "hooks": { + "on_session_end": on_session_end, + "on_error_occurred": on_error_occurred, + }, + "on_permission_request": lambda req, inv: {"kind": "approved"}, +}) +``` + +
+ +### Play a sound when a tool finishes + +```typescript +import { exec } from "node:child_process"; + +const session = await client.createSession({ + hooks: { + onPostToolUse: async (input) => { + // macOS: play a system sound after every tool call + exec("afplay /System/Library/Sounds/Pop.aiff"); + return null; + }, + onErrorOccurred: async () => { + exec("afplay /System/Library/Sounds/Basso.aiff"); + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +### Post to Slack on errors + +```typescript +const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!; + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input, invocation) => { + if (!input.recoverable) { + await fetch(SLACK_WEBHOOK_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``, + }), + }); + } + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +--- + +## Use Case: Prompt Enrichment + +Use `onSessionStart` and `onUserPromptSubmitted` to automatically inject context so users don't have to repeat themselves. + +### Inject project metadata at session start + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async (input) => { + const pkg = JSON.parse( + await fs.promises.readFile("package.json", "utf-8"), + ); + return { + additionalContext: [ + `Project: ${pkg.name} v${pkg.version}`, + `Node: ${process.version}`, + `CWD: ${input.cwd}`, + ].join("\n"), + }; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +### Expand shorthand commands in prompts + +```typescript +const SHORTCUTS: Record = { + "/fix": "Find and fix all errors in the current file", + "/test": "Write comprehensive unit tests for this code", + "/explain": "Explain this code in detail", + "/refactor": "Refactor this code to improve readability", +}; + +const session = await client.createSession({ + hooks: { + onUserPromptSubmitted: async (input) => { + for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) { + if (input.prompt.startsWith(shortcut)) { + const rest = input.prompt.slice(shortcut.length).trim(); + return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion }; + } + } + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +--- + +## Use Case: Error Handling & Recovery + +The `onErrorOccurred` hook gives you a chance to react to failures — whether that means retrying, notifying a human, or gracefully shutting down. + +### Retry transient model errors + +```typescript +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input) => { + if (input.errorContext === "model_call" && input.recoverable) { + return { + errorHandling: "retry", + retryCount: 3, + userNotification: "Temporary model issue — retrying…", + }; + } + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +### Friendly error messages + +```typescript +const FRIENDLY_MESSAGES: Record = { + model_call: "The AI model is temporarily unavailable. Please try again.", + tool_execution: "A tool encountered an error. Check inputs and try again.", + system: "A system error occurred. Please try again later.", +}; + +const session = await client.createSession({ + hooks: { + onErrorOccurred: async (input) => { + return { + userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error, + }; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +--- + +## Use Case: Session Metrics + +Track how long sessions run, how many tools are invoked, and why sessions end — useful for dashboards and cost monitoring. + +
+Node.js / TypeScript + +```typescript +const metrics = new Map(); + +const session = await client.createSession({ + hooks: { + onSessionStart: async (input, invocation) => { + metrics.set(invocation.sessionId, { + start: input.timestamp, + toolCalls: 0, + prompts: 0, + }); + return null; + }, + onUserPromptSubmitted: async (_input, invocation) => { + metrics.get(invocation.sessionId)!.prompts++; + return null; + }, + onPreToolUse: async (_input, invocation) => { + metrics.get(invocation.sessionId)!.toolCalls++; + return { permissionDecision: "allow" }; + }, + onSessionEnd: async (input, invocation) => { + const m = metrics.get(invocation.sessionId)!; + const durationSec = (input.timestamp - m.start) / 1000; + + console.log( + `Session ${invocation.sessionId.slice(0, 8)}: ` + + `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` + + `${m.toolCalls} tool calls, ended: ${input.reason}`, + ); + + metrics.delete(invocation.sessionId); + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +
+ +
+Python + +```python +session_metrics = {} + +async def on_session_start(input_data, invocation): + session_metrics[invocation["session_id"]] = { + "start": input_data["timestamp"], + "tool_calls": 0, + "prompts": 0, + } + return None + +async def on_user_prompt_submitted(input_data, invocation): + session_metrics[invocation["session_id"]]["prompts"] += 1 + return None + +async def on_pre_tool_use(input_data, invocation): + session_metrics[invocation["session_id"]]["tool_calls"] += 1 + return {"permissionDecision": "allow"} + +async def on_session_end(input_data, invocation): + m = session_metrics.pop(invocation["session_id"]) + duration = (input_data["timestamp"] - m["start"]) / 1000 + sid = invocation["session_id"][:8] + print( + f"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, " + f"{m['tool_calls']} tool calls, ended: {input_data['reason']}" + ) + return None + +session = await client.create_session({ + "hooks": { + "on_session_start": on_session_start, + "on_user_prompt_submitted": on_user_prompt_submitted, + "on_pre_tool_use": on_pre_tool_use, + "on_session_end": on_session_end, + }, + "on_permission_request": lambda req, inv: {"kind": "approved"}, +}) +``` + +
+ +--- + +## Combining Hooks + +Hooks compose naturally. A single `hooks` object can handle permissions **and** auditing **and** notifications — each hook does its own job. + +```typescript +const session = await client.createSession({ + hooks: { + onSessionStart: async (input) => { + console.log(`[audit] session started in ${input.cwd}`); + return { additionalContext: "Project uses TypeScript and Vitest." }; + }, + onPreToolUse: async (input) => { + console.log(`[audit] tool requested: ${input.toolName}`); + if (input.toolName === "shell") { + return { permissionDecision: "ask" }; + } + return { permissionDecision: "allow" }; + }, + onPostToolUse: async (input) => { + console.log(`[audit] tool completed: ${input.toolName}`); + return null; + }, + onErrorOccurred: async (input) => { + console.error(`[alert] ${input.errorContext}: ${input.error}`); + return null; + }, + onSessionEnd: async (input, invocation) => { + console.log(`[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`); + return null; + }, + }, + onPermissionRequest: async () => ({ kind: "approved" }), +}); +``` + +## Best Practices + +1. **Keep hooks fast.** Every hook runs inline — slow hooks delay the conversation. Offload heavy work (database writes, HTTP calls) to a background queue when possible. + +2. **Return `null` when you have nothing to change.** This tells the SDK to proceed with defaults and avoids unnecessary object allocation. + +3. **Be explicit with permission decisions.** Returning `{ permissionDecision: "allow" }` is clearer than returning `null`, even though both allow the tool. + +4. **Don't swallow critical errors.** It's fine to suppress recoverable tool errors, but always log or alert on unrecoverable ones. + +5. **Use `additionalContext` instead of `modifiedPrompt` when possible.** Appending context preserves the user's original intent while still guiding the model. + +6. **Scope state by session ID.** If you track per-session data, key it on `invocation.sessionId` and clean up in `onSessionEnd`. + +## Reference + +For full type definitions, input/output field tables, and additional examples for every hook, see the API reference: + +- [Hooks Overview](../hooks/overview.md) +- [Pre-Tool Use](../hooks/pre-tool-use.md) +- [Post-Tool Use](../hooks/post-tool-use.md) +- [User Prompt Submitted](../hooks/user-prompt-submitted.md) +- [Session Lifecycle](../hooks/session-lifecycle.md) +- [Error Handling](../hooks/error-handling.md) + +## See Also + +- [Getting Started](../getting-started.md) +- [Custom Agents & Sub-Agent Orchestration](./custom-agents.md) +- [Streaming Session Events](./streaming-events.md) +- [Debugging Guide](../debugging.md) From 327a7986c3415024cb15c5a37b6530ca83bc30cb Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 23:27:43 -0800 Subject: [PATCH 08/61] docs: replace 72 skip directives with hidden compilable blocks (#721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit that the validator checks, while keeping user-facing snippets unchanged. Changes across 22 markdown files: - Hook type signatures (5 files × 4 langs): 20 skips removed - Hook Go/C# implementations (4 files): 10 skips removed - Guide Go/C#/Py snippets (8 files): 21 skips removed - Setup/auth snippets (7 files): 11 skips removed - OpenTelemetry Python (1 file): 4 skips removed - Misc snippets (2 files): 4 skips removed Remaining 14 skips are due to external package dependencies not available to the validator (Microsoft.Agents.AI, @azure/identity, Azure.Identity, aiofiles, Microsoft.Extensions.Logging). All 305 extracted code blocks pass validation across TypeScript (160), Python (62), Go (41), and C# (42). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/auth/byok.md | 32 +++++- docs/auth/index.md | 74 ++++++++++++- docs/debugging.md | 30 ++++- docs/guides/custom-agents.md | 88 ++++++++++++++- docs/guides/hooks.md | 153 +++++++++++++++++++++++++- docs/guides/image-input.md | 82 +++++++++++++- docs/guides/session-persistence.md | 73 +++++++++++- docs/guides/setup/backend-services.md | 59 +++++++++- docs/guides/setup/bundled-cli.md | 30 ++++- docs/guides/setup/byok.md | 34 +++++- docs/guides/setup/github-oauth.md | 62 ++++++++++- docs/guides/setup/local-cli.md | 28 ++++- docs/guides/skills.md | 49 ++++++++- docs/guides/steering-and-queueing.md | 77 ++++++++++++- docs/guides/streaming-events.md | 72 +++++++++++- docs/hooks/error-handling.md | 102 ++++++++++++++++- docs/hooks/post-tool-use.md | 102 ++++++++++++++++- docs/hooks/pre-tool-use.md | 104 ++++++++++++++++- docs/hooks/session-lifecycle.md | 74 ++++++++++++- docs/hooks/user-prompt-submitted.md | 98 ++++++++++++++++- docs/mcp/debugging.md | 69 +++++++++++- docs/opentelemetry-instrumentation.md | 58 +++++++++- 22 files changed, 1478 insertions(+), 72 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index ca7861c160..49d2452d97 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -363,7 +363,21 @@ const session = await client.createSession({ For Azure OpenAI endpoints (`*.openai.azure.com`), use the correct type: - + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + provider: { + type: "azure", + baseUrl: "https://my-resource.openai.azure.com", + }, +}); +``` + + ```typescript // ❌ Wrong: Using "openai" type with native Azure endpoint provider: { @@ -380,7 +394,21 @@ provider: { However, if your Azure AI Foundry deployment provides an OpenAI-compatible endpoint path (e.g., `/openai/v1/`), use `type: "openai"`: - + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +const session = await client.createSession({ + model: "gpt-4.1", + provider: { + type: "openai", + baseUrl: "https://your-resource.openai.azure.com/openai/v1/", + }, +}); +``` + + ```typescript // ✅ Correct: OpenAI-compatible Azure AI Foundry endpoint provider: { diff --git a/docs/auth/index.md b/docs/auth/index.md index 9fc65fe28b..67bbed1aa9 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -50,7 +50,20 @@ await client.start()
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +func main() { + // Default: uses logged-in user credentials + client := copilot.NewClient(nil) + _ = client +} +``` + + ```go import copilot "github.com/github/copilot-sdk/go" @@ -120,7 +133,23 @@ await client.start()
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +func main() { + userAccessToken := "token" + client := copilot.NewClient(&copilot.ClientOptions{ + GitHubToken: userAccessToken, + UseLoggedInUser: copilot.Bool(false), + }) + _ = client +} +``` + + ```go import copilot "github.com/github/copilot-sdk/go" @@ -135,7 +164,19 @@ client := copilot.NewClient(&copilot.ClientOptions{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +var userAccessToken = "token"; +await using var client = new CopilotClient(new CopilotClientOptions +{ + GithubToken = userAccessToken, + UseLoggedInUser = false, +}); +``` + + ```csharp using GitHub.Copilot.SDK; @@ -254,7 +295,16 @@ const client = new CopilotClient({
Python - + +```python +from copilot import CopilotClient + +client = CopilotClient({ + "use_logged_in_user": False, +}) +``` + + ```python client = CopilotClient({ "use_logged_in_user": False, # Only use explicit tokens @@ -266,7 +316,21 @@ client = CopilotClient({
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +func main() { + client := copilot.NewClient(&copilot.ClientOptions{ + UseLoggedInUser: copilot.Bool(false), + }) + _ = client +} +``` + + ```go client := copilot.NewClient(&copilot.ClientOptions{ UseLoggedInUser: copilot.Bool(false), // Only use explicit tokens diff --git a/docs/debugging.md b/docs/debugging.md index bf953b2ffd..b74ff51cac 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -44,7 +44,21 @@ client = CopilotClient({"log_level": "debug"})
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +func main() { + client := copilot.NewClient(&copilot.ClientOptions{ + LogLevel: "debug", + }) + _ = client +} +``` + + ```go import copilot "github.com/github/copilot-sdk/go" @@ -59,6 +73,7 @@ client := copilot.NewClient(&copilot.ClientOptions{ .NET + ```csharp using GitHub.Copilot.SDK; using Microsoft.Extensions.Logging; @@ -110,7 +125,18 @@ const client = new CopilotClient({
Go - + +```go +package main + +func main() { + // The Go SDK does not currently support passing extra CLI arguments. + // For custom log directories, run the CLI manually with --log-dir + // and connect via CLIUrl option. +} +``` + + ```go // The Go SDK does not currently support passing extra CLI arguments. // For custom log directories, run the CLI manually with --log-dir diff --git a/docs/guides/custom-agents.md b/docs/guides/custom-agents.md index 82790d3417..de642e194b 100644 --- a/docs/guides/custom-agents.md +++ b/docs/guides/custom-agents.md @@ -97,7 +97,47 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "researcher", + DisplayName: "Research Agent", + Description: "Explores codebases and answers questions using read-only tools", + Tools: []string{"grep", "glob", "view"}, + Prompt: "You are a research assistant. Analyze code and answer questions. Do not modify any files.", + }, + { + Name: "editor", + DisplayName: "Editor Agent", + Description: "Makes targeted code changes", + Tools: []string{"view", "edit", "bash"}, + Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", + }, + }, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + _ = session +} +``` + + ```go ctx := context.Background() client := copilot.NewClient(nil) @@ -287,7 +327,51 @@ response = await session.send_and_wait({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + session.On(func(event copilot.SessionEvent) { + switch event.Type { + case "subagent.started": + fmt.Printf("▶ Sub-agent started: %s\n", *event.Data.AgentDisplayName) + fmt.Printf(" Description: %s\n", *event.Data.AgentDescription) + fmt.Printf(" Tool call ID: %s\n", *event.Data.ToolCallID) + case "subagent.completed": + fmt.Printf("✅ Sub-agent completed: %s\n", *event.Data.AgentDisplayName) + case "subagent.failed": + fmt.Printf("❌ Sub-agent failed: %s — %v\n", *event.Data.AgentDisplayName, event.Data.Error) + case "subagent.selected": + fmt.Printf("🎯 Agent selected: %s\n", *event.Data.AgentDisplayName) + } + }) + + _, err := session.SendAndWait(ctx, copilot.MessageOptions{ + Prompt: "Research how authentication works in this codebase", + }) + _ = err +} +``` + + ```go session.On(func(event copilot.SessionEvent) { switch event.Type { diff --git a/docs/guides/hooks.md b/docs/guides/hooks.md index 60f5cfe714..eeb0ec4722 100644 --- a/docs/guides/hooks.md +++ b/docs/guides/hooks.md @@ -81,7 +81,47 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func onSessionStart(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { + return nil, nil +} + +func onPreToolUse(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + return nil, nil +} + +func onPostToolUse(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) { + return nil, nil +} + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnSessionStart: onSessionStart, + OnPreToolUse: onPreToolUse, + OnPostToolUse: onPostToolUse, + }, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: "approved"}, nil + }, + }) + _ = session + _ = err +} +``` + + ```go client := copilot.NewClient(nil) @@ -103,7 +143,39 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class HooksExample +{ + static Task onSessionStart(SessionStartHookInput input, HookInvocation invocation) => + Task.FromResult(null); + static Task onPreToolUse(PreToolUseHookInput input, HookInvocation invocation) => + Task.FromResult(null); + static Task onPostToolUse(PostToolUseHookInput input, HookInvocation invocation) => + Task.FromResult(null); + + public static async Task Main() + { + var client = new CopilotClient(); + + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnSessionStart = onSessionStart, + OnPreToolUse = onPreToolUse, + OnPostToolUse = onPostToolUse, + }, + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + } +} +``` + + ```csharp var client = new CopilotClient(); @@ -184,7 +256,43 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true} + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Hooks: &copilot.SessionHooks{ + OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + if !readOnlyTools[input.ToolName] { + return &copilot.PreToolUseHookOutput{ + PermissionDecision: "deny", + PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName), + }, nil + } + return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil + }, + }, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + _ = session +} +``` + + ```go readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true} @@ -208,7 +316,44 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class PermissionControlExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + + var readOnlyTools = new HashSet { "read_file", "glob", "grep", "view" }; + + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnPreToolUse = (input, invocation) => + { + if (!readOnlyTools.Contains(input.ToolName)) + { + return Task.FromResult(new PreToolUseHookOutput + { + PermissionDecision = "deny", + PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.", + }); + } + return Task.FromResult( + new PreToolUseHookOutput { PermissionDecision = "allow" }); + }, + }, + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + } +} +``` + + ```csharp var readOnlyTools = new HashSet { "read_file", "glob", "grep", "view" }; diff --git a/docs/guides/image-input.md b/docs/guides/image-input.md index acf8737a0b..aa3bf2f643 100644 --- a/docs/guides/image-input.md +++ b/docs/guides/image-input.md @@ -91,7 +91,41 @@ await session.send({
Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + path := "/absolute/path/to/screenshot.png" + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.File, + Path: &path, + }, + }, + }) +} +``` + + ```go ctx := context.Background() client := copilot.NewClient(nil) @@ -121,7 +155,39 @@ session.Send(ctx, copilot.MessageOptions{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class ImageInputExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + + await session.SendAsync(new MessageOptions + { + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemFile + { + Path = "/absolute/path/to/screenshot.png", + DisplayName = "screenshot.png", + }, + }, + }); + } +} +``` + + ```csharp using GitHub.Copilot.SDK; @@ -180,7 +246,17 @@ Not all models support vision. Check the model's capabilities before sending ima ### Vision limits type - + +```typescript +interface VisionCapabilities { + vision?: { + supported_media_types: string[]; + max_prompt_images: number; + max_prompt_image_size: number; // bytes + }; +} +``` + ```typescript vision?: { supported_media_types: string[]; diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index e2b736c1b5..df96c9ea00 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -65,7 +65,33 @@ await session.send_and_wait({"prompt": "Analyze my codebase"}) ### Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "user-123-task-456", + Model: "gpt-5.2-codex", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Analyze my codebase"}) + _ = session +} +``` + + ```go ctx := context.Background() client := copilot.NewClient(nil) @@ -142,7 +168,27 @@ await session.send_and_wait({"prompt": "What did we discuss earlier?"}) ### Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + session, _ := client.ResumeSession(ctx, "user-123-task-456", nil) + + session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss earlier?"}) + _ = session +} +``` + + ```go ctx := context.Background() @@ -155,7 +201,28 @@ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss ear ### C# (.NET) - + +```csharp +using GitHub.Copilot.SDK; + +public static class ResumeSessionExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + + var session = await client.ResumeSessionAsync("user-123-task-456", new ResumeSessionConfig + { + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + + await session.SendAndWaitAsync(new MessageOptions { Prompt = "What did we discuss earlier?" }); + } +} +``` + + ```csharp // Resume from a different client instance (or after restart) var session = await client.ResumeSessionAsync("user-123-task-456"); diff --git a/docs/guides/setup/backend-services.md b/docs/guides/setup/backend-services.md index e0d0975dbf..494d3574c8 100644 --- a/docs/guides/setup/backend-services.md +++ b/docs/guides/setup/backend-services.md @@ -131,7 +131,39 @@ response = await session.send_and_wait({"prompt": message})
Go - + +```go +package main + +import ( + "context" + "fmt" + "time" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + userID := "user1" + message := "Hello" + + client := copilot.NewClient(&copilot.ClientOptions{ + CLIUrl: "localhost:4321", + }) + client.Start(ctx) + defer client.Stop() + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: fmt.Sprintf("user-%s-%d", userID, time.Now().Unix()), + Model: "gpt-4.1", + }) + + response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message}) + _ = response +} +``` + + ```go client := copilot.NewClient(&copilot.ClientOptions{ CLIUrl:"localhost:4321", @@ -152,7 +184,30 @@ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: message})
.NET - + +```csharp +using GitHub.Copilot.SDK; + +var userId = "user1"; +var message = "Hello"; + +var client = new CopilotClient(new CopilotClientOptions +{ + CliUrl = "localhost:4321", + UseStdio = false, +}); + +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + SessionId = $"user-{userId}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}", + Model = "gpt-4.1", +}); + +var response = await session.SendAndWaitAsync( + new MessageOptions { Prompt = message }); +``` + + ```csharp var client = new CopilotClient(new CopilotClientOptions { diff --git a/docs/guides/setup/bundled-cli.md b/docs/guides/setup/bundled-cli.md index 6daf57b56e..8c5b0cbdd4 100644 --- a/docs/guides/setup/bundled-cli.md +++ b/docs/guides/setup/bundled-cli.md @@ -105,7 +105,35 @@ await client.stop()
Go - + +```go +package main + +import ( + "context" + "fmt" + "log" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: "./vendor/copilot", + }) + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) + response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + fmt.Println(*response.Data.Content) +} +``` + + ```go client := copilot.NewClient(&copilot.ClientOptions{ CLIPath:"./vendor/copilot", diff --git a/docs/guides/setup/byok.md b/docs/guides/setup/byok.md index 5b8b8a460c..35c5f1adc4 100644 --- a/docs/guides/setup/byok.md +++ b/docs/guides/setup/byok.md @@ -118,7 +118,39 @@ await client.stop()
Go - + +```go +package main + +import ( + "context" + "fmt" + "os" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + + client := copilot.NewClient(nil) + client.Start(ctx) + defer client.Stop() + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + Provider: &copilot.ProviderConfig{ + Type: "openai", + BaseURL: "https://api.openai.com/v1", + APIKey: os.Getenv("OPENAI_API_KEY"), + }, + }) + + response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + fmt.Println(*response.Data.Content) +} +``` + + ```go client := copilot.NewClient(nil) client.Start(ctx) diff --git a/docs/guides/setup/github-oauth.md b/docs/guides/setup/github-oauth.md index 07251c8fbe..aa12542e53 100644 --- a/docs/guides/setup/github-oauth.md +++ b/docs/guides/setup/github-oauth.md @@ -170,7 +170,41 @@ response = await session.send_and_wait({"prompt": "Hello!"})
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func createClientForUser(userToken string) *copilot.Client { + return copilot.NewClient(&copilot.ClientOptions{ + GitHubToken: userToken, + UseLoggedInUser: copilot.Bool(false), + }) +} + +func main() { + ctx := context.Background() + userID := "user1" + + client := createClientForUser("gho_user_access_token") + client.Start(ctx) + defer client.Stop() + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: fmt.Sprintf("user-%s-session", userID), + Model: "gpt-4.1", + }) + response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + _ = response +} +``` + + ```go func createClientForUser(userToken string) *copilot.Client { return copilot.NewClient(&copilot.ClientOptions{ @@ -196,7 +230,31 @@ response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}
.NET - + +```csharp +using GitHub.Copilot.SDK; + +CopilotClient CreateClientForUser(string userToken) => + new CopilotClient(new CopilotClientOptions + { + GithubToken = userToken, + UseLoggedInUser = false, + }); + +var userId = "user1"; + +await using var client = CreateClientForUser("gho_user_access_token"); +await using var session = await client.CreateSessionAsync(new SessionConfig +{ + SessionId = $"user-{userId}-session", + Model = "gpt-4.1", +}); + +var response = await session.SendAndWaitAsync( + new MessageOptions { Prompt = "Hello!" }); +``` + + ```csharp CopilotClient CreateClientForUser(string userToken) => new CopilotClient(new CopilotClientOptions diff --git a/docs/guides/setup/local-cli.md b/docs/guides/setup/local-cli.md index a5fa906b89..402368fcca 100644 --- a/docs/guides/setup/local-cli.md +++ b/docs/guides/setup/local-cli.md @@ -68,7 +68,33 @@ await client.stop()
Go - + +```go +package main + +import ( + "context" + "fmt" + "log" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + + client := copilot.NewClient(nil) + if err := client.Start(ctx); err != nil { + log.Fatal(err) + } + defer client.Stop() + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) + response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) + fmt.Println(*response.Data.Content) +} +``` + + ```go client := copilot.NewClient(nil) if err := client.Start(ctx); err != nil { diff --git a/docs/guides/skills.md b/docs/guides/skills.md index b9b07ae882..8ed9402513 100644 --- a/docs/guides/skills.md +++ b/docs/guides/skills.md @@ -171,7 +171,31 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + SkillDirectories: []string{"./skills"}, + DisabledSkills: []string{"experimental-feature", "deprecated-tool"}, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + _ = session +} +``` + + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ SkillDirectories: []string{"./skills"}, @@ -184,7 +208,28 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class SkillsExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + + var session = await client.CreateSessionAsync(new SessionConfig + { + SkillDirectories = new List { "./skills" }, + DisabledSkills = new List { "experimental-feature", "deprecated-tool" }, + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + } +} +``` + + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/guides/steering-and-queueing.md b/docs/guides/steering-and-queueing.md index da66caa64f..ef2eb46dbd 100644 --- a/docs/guides/steering-and-queueing.md +++ b/docs/guides/steering-and-queueing.md @@ -263,7 +263,44 @@ async def main():
Go - + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Set up the project structure", + }) + + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Add unit tests for the auth module", + Mode: "enqueue", + }) + + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Update the README with setup instructions", + Mode: "enqueue", + }) +} +``` + + ```go // Send an initial task session.Send(ctx, copilot.MessageOptions{ @@ -289,7 +326,43 @@ session.Send(ctx, copilot.MessageOptions{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class QueueingExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + + await session.SendAsync(new MessageOptions + { + Prompt = "Set up the project structure" + }); + + await session.SendAsync(new MessageOptions + { + Prompt = "Add unit tests for the auth module", + Mode = "enqueue" + }); + + await session.SendAsync(new MessageOptions + { + Prompt = "Update the README with setup instructions", + Mode = "enqueue" + }); + } +} +``` + + ```csharp // Send an initial task await session.SendAsync(new MessageOptions diff --git a/docs/guides/streaming-events.md b/docs/guides/streaming-events.md index 72259858b8..81b27f80fd 100644 --- a/docs/guides/streaming-events.md +++ b/docs/guides/streaming-events.md @@ -82,7 +82,23 @@ session.on("assistant.message_delta", (event) => {
Python - + +```python +from copilot import CopilotClient +from copilot.generated.session_events import SessionEventType + +client = CopilotClient() + +session = None # assume session is created elsewhere + +def handle(event): + if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: + print(event.data.delta_content, end="", flush=True) + +# session.on(handle) +``` + + ```python from copilot.generated.session_events import SessionEventType @@ -98,7 +114,38 @@ session.on(handle)
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + Streaming: true, + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + session.On(func(event copilot.SessionEvent) { + if event.Type == "assistant.message_delta" { + fmt.Print(*event.Data.DeltaContent) + } + }) + _ = session +} +``` + + ```go session.On(func(event copilot.SessionEvent) { if event.Type == "assistant.message_delta" { @@ -112,7 +159,26 @@ session.On(func(event copilot.SessionEvent) {
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class StreamingEventsExample +{ + public static async Task Example(CopilotSession session) + { + session.On(evt => + { + if (evt is AssistantMessageDeltaEvent delta) + { + Console.Write(delta.Data.DeltaContent); + } + }); + } +} +``` + + ```csharp session.On(evt => { diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 0f705868df..c3c8fa529c 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -12,7 +12,15 @@ The `onErrorOccurred` hook is called when errors occur during session execution.
Node.js / TypeScript - + +```ts +import type { ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput } from "@github/copilot-sdk"; +type ErrorOccurredHandler = ( + input: ErrorOccurredHookInput, + invocation: HookInvocation +) => Promise; +``` + ```typescript type ErrorOccurredHandler = ( input: ErrorOccurredHookInput, @@ -25,7 +33,17 @@ type ErrorOccurredHandler = (
Python - + +```python +from copilot.types import ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput +from typing import Callable, Awaitable + +ErrorOccurredHandler = Callable[ + [ErrorOccurredHookInput, HookInvocation], + Awaitable[ErrorOccurredHookOutput | None] +] +``` + ```python ErrorOccurredHandler = Callable[ [ErrorOccurredHookInput, HookInvocation], @@ -38,7 +56,20 @@ ErrorOccurredHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type ErrorOccurredHandler func( + input copilot.ErrorOccurredHookInput, + invocation copilot.HookInvocation, +) (*copilot.ErrorOccurredHookOutput, error) + +func main() {} +``` + ```go type ErrorOccurredHandler func( input ErrorOccurredHookInput, @@ -51,7 +82,15 @@ type ErrorOccurredHandler func(
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public delegate Task ErrorOccurredHandler( + ErrorOccurredHookInput input, + HookInvocation invocation); +``` + ```csharp public delegate Task ErrorOccurredHandler( ErrorOccurredHookInput input, @@ -123,7 +162,33 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Hooks: &copilot.SessionHooks{ + OnErrorOccurred: func(input copilot.ErrorOccurredHookInput, inv copilot.HookInvocation) (*copilot.ErrorOccurredHookOutput, error) { + fmt.Printf("[%s] Error: %s\n", inv.SessionID, input.Error) + fmt.Printf(" Context: %s\n", input.ErrorContext) + fmt.Printf(" Recoverable: %v\n", input.Recoverable) + return nil, nil + }, + }, + }) + _ = session +} +``` + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -142,7 +207,32 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class ErrorHandlingExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnErrorOccurred = (input, invocation) => + { + Console.Error.WriteLine($"[{invocation.SessionId}] Error: {input.Error}"); + Console.Error.WriteLine($" Context: {input.ErrorContext}"); + Console.Error.WriteLine($" Recoverable: {input.Recoverable}"); + return Task.FromResult(null); + }, + }, + }); + } +} +``` + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 0021e20a0d..d50ff6b488 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -12,7 +12,15 @@ The `onPostToolUse` hook is called **after** a tool executes. Use it to:
Node.js / TypeScript - + +```ts +import type { PostToolUseHookInput, HookInvocation, PostToolUseHookOutput } from "@github/copilot-sdk"; +type PostToolUseHandler = ( + input: PostToolUseHookInput, + invocation: HookInvocation +) => Promise; +``` + ```typescript type PostToolUseHandler = ( input: PostToolUseHookInput, @@ -25,7 +33,17 @@ type PostToolUseHandler = (
Python - + +```python +from copilot.types import PostToolUseHookInput, HookInvocation, PostToolUseHookOutput +from typing import Callable, Awaitable + +PostToolUseHandler = Callable[ + [PostToolUseHookInput, HookInvocation], + Awaitable[PostToolUseHookOutput | None] +] +``` + ```python PostToolUseHandler = Callable[ [PostToolUseHookInput, HookInvocation], @@ -38,7 +56,20 @@ PostToolUseHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type PostToolUseHandler func( + input copilot.PostToolUseHookInput, + invocation copilot.HookInvocation, +) (*copilot.PostToolUseHookOutput, error) + +func main() {} +``` + ```go type PostToolUseHandler func( input PostToolUseHookInput, @@ -51,7 +82,15 @@ type PostToolUseHandler func(
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public delegate Task PostToolUseHandler( + PostToolUseHookInput input, + HookInvocation invocation); +``` + ```csharp public delegate Task PostToolUseHandler( PostToolUseHookInput input, @@ -122,7 +161,33 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Hooks: &copilot.SessionHooks{ + OnPostToolUse: func(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) { + fmt.Printf("[%s] Tool: %s\n", inv.SessionID, input.ToolName) + fmt.Printf(" Args: %v\n", input.ToolArgs) + fmt.Printf(" Result: %v\n", input.ToolResult) + return nil, nil + }, + }, + }) + _ = session +} +``` + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -141,7 +206,32 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class PostToolUseExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnPostToolUse = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] Tool: {input.ToolName}"); + Console.WriteLine($" Args: {input.ToolArgs}"); + Console.WriteLine($" Result: {input.ToolResult}"); + return Task.FromResult(null); + }, + }, + }); + } +} +``` + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index ac12df4fab..a09fca6ef3 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -12,7 +12,15 @@ The `onPreToolUse` hook is called **before** a tool executes. Use it to:
Node.js / TypeScript - + +```ts +import type { PreToolUseHookInput, HookInvocation, PreToolUseHookOutput } from "@github/copilot-sdk"; +type PreToolUseHandler = ( + input: PreToolUseHookInput, + invocation: HookInvocation +) => Promise; +``` + ```typescript type PreToolUseHandler = ( input: PreToolUseHookInput, @@ -25,7 +33,17 @@ type PreToolUseHandler = (
Python - + +```python +from copilot.types import PreToolUseHookInput, HookInvocation, PreToolUseHookOutput +from typing import Callable, Awaitable + +PreToolUseHandler = Callable[ + [PreToolUseHookInput, HookInvocation], + Awaitable[PreToolUseHookOutput | None] +] +``` + ```python PreToolUseHandler = Callable[ [PreToolUseHookInput, HookInvocation], @@ -38,7 +56,20 @@ PreToolUseHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type PreToolUseHandler func( + input copilot.PreToolUseHookInput, + invocation copilot.HookInvocation, +) (*copilot.PreToolUseHookOutput, error) + +func main() {} +``` + ```go type PreToolUseHandler func( input PreToolUseHookInput, @@ -51,7 +82,15 @@ type PreToolUseHandler func(
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public delegate Task PreToolUseHandler( + PreToolUseHookInput input, + HookInvocation invocation); +``` + ```csharp public delegate Task PreToolUseHandler( PreToolUseHookInput input, @@ -129,7 +168,34 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Hooks: &copilot.SessionHooks{ + OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { + fmt.Printf("[%s] Calling %s\n", inv.SessionID, input.ToolName) + fmt.Printf(" Args: %v\n", input.ToolArgs) + return &copilot.PreToolUseHookOutput{ + PermissionDecision: "allow", + }, nil + }, + }, + }) + _ = session +} +``` + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -149,7 +215,33 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class PreToolUseExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnPreToolUse = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] Calling {input.ToolName}"); + Console.WriteLine($" Args: {input.ToolArgs}"); + return Task.FromResult( + new PreToolUseHookOutput { PermissionDecision = "allow" } + ); + }, + }, + }); + } +} +``` + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 74f4666f45..22ff5dd61f 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -16,7 +16,15 @@ The `onSessionStart` hook is called when a session begins (new or resumed).
Node.js / TypeScript - + +```ts +import type { SessionStartHookInput, HookInvocation, SessionStartHookOutput } from "@github/copilot-sdk"; +type SessionStartHandler = ( + input: SessionStartHookInput, + invocation: HookInvocation +) => Promise; +``` + ```typescript type SessionStartHandler = ( input: SessionStartHookInput, @@ -29,7 +37,17 @@ type SessionStartHandler = (
Python - + +```python +from copilot.types import SessionStartHookInput, HookInvocation, SessionStartHookOutput +from typing import Callable, Awaitable + +SessionStartHandler = Callable[ + [SessionStartHookInput, HookInvocation], + Awaitable[SessionStartHookOutput | None] +] +``` + ```python SessionStartHandler = Callable[ [SessionStartHookInput, HookInvocation], @@ -42,7 +60,20 @@ SessionStartHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type SessionStartHandler func( + input copilot.SessionStartHookInput, + invocation copilot.HookInvocation, +) (*copilot.SessionStartHookOutput, error) + +func main() {} +``` + ```go type SessionStartHandler func( input SessionStartHookInput, @@ -55,7 +86,15 @@ type SessionStartHandler func(
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public delegate Task SessionStartHandler( + SessionStartHookInput input, + HookInvocation invocation); +``` + ```csharp public delegate Task SessionStartHandler( SessionStartHookInput input, @@ -208,7 +247,17 @@ type SessionEndHandler = (
Python - + +```python +from copilot.types import SessionEndHookInput, HookInvocation +from typing import Callable, Awaitable + +SessionEndHandler = Callable[ + [SessionEndHookInput, HookInvocation], + Awaitable[None] +] +``` + ```python SessionEndHandler = Callable[ [SessionEndHookInput, HookInvocation], @@ -221,7 +270,20 @@ SessionEndHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type SessionEndHandler func( + input copilot.SessionEndHookInput, + invocation copilot.HookInvocation, +) error + +func main() {} +``` + ```go type SessionEndHandler func( input SessionEndHookInput, diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 3205b95cd4..0eb959d477 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -12,7 +12,15 @@ The `onUserPromptSubmitted` hook is called when a user submits a message. Use it
Node.js / TypeScript - + +```ts +import type { UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput } from "@github/copilot-sdk"; +type UserPromptSubmittedHandler = ( + input: UserPromptSubmittedHookInput, + invocation: HookInvocation +) => Promise; +``` + ```typescript type UserPromptSubmittedHandler = ( input: UserPromptSubmittedHookInput, @@ -25,7 +33,17 @@ type UserPromptSubmittedHandler = (
Python - + +```python +from copilot.types import UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput +from typing import Callable, Awaitable + +UserPromptSubmittedHandler = Callable[ + [UserPromptSubmittedHookInput, HookInvocation], + Awaitable[UserPromptSubmittedHookOutput | None] +] +``` + ```python UserPromptSubmittedHandler = Callable[ [UserPromptSubmittedHookInput, HookInvocation], @@ -38,7 +56,20 @@ UserPromptSubmittedHandler = Callable[
Go - + +```go +package main + +import copilot "github.com/github/copilot-sdk/go" + +type UserPromptSubmittedHandler func( + input copilot.UserPromptSubmittedHookInput, + invocation copilot.HookInvocation, +) (*copilot.UserPromptSubmittedHookOutput, error) + +func main() {} +``` + ```go type UserPromptSubmittedHandler func( input UserPromptSubmittedHookInput, @@ -51,7 +82,15 @@ type UserPromptSubmittedHandler func(
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public delegate Task UserPromptSubmittedHandler( + UserPromptSubmittedHookInput input, + HookInvocation invocation); +``` + ```csharp public delegate Task UserPromptSubmittedHandler( UserPromptSubmittedHookInput input, @@ -116,7 +155,31 @@ session = await client.create_session({
Go - + +```go +package main + +import ( + "context" + "fmt" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(nil) + session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Hooks: &copilot.SessionHooks{ + OnUserPromptSubmitted: func(input copilot.UserPromptSubmittedHookInput, inv copilot.HookInvocation) (*copilot.UserPromptSubmittedHookOutput, error) { + fmt.Printf("[%s] User: %s\n", inv.SessionID, input.Prompt) + return nil, nil + }, + }, + }) + _ = session +} +``` + ```go session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ Hooks: &copilot.SessionHooks{ @@ -133,7 +196,30 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
.NET - + +```csharp +using GitHub.Copilot.SDK; + +public static class UserPromptSubmittedExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + var session = await client.CreateSessionAsync(new SessionConfig + { + Hooks = new SessionHooks + { + OnUserPromptSubmitted = (input, invocation) => + { + Console.WriteLine($"[{invocation.SessionId}] User: {input.Prompt}"); + return Task.FromResult(null); + }, + }, + }); + } +} +``` + ```csharp var session = await client.CreateSessionAsync(new SessionConfig { diff --git a/docs/mcp/debugging.md b/docs/mcp/debugging.md index 5ca51d1e3b..783a4af8b6 100644 --- a/docs/mcp/debugging.md +++ b/docs/mcp/debugging.md @@ -242,7 +242,37 @@ cd /expected/working/dir #### .NET Console Apps / Tools - + +```csharp +using GitHub.Copilot.SDK; + +public static class McpDotnetConfigExample +{ + public static void Main() + { + var servers = new Dictionary + { + ["my-dotnet-server"] = new McpLocalServerConfig + { + Type = "local", + Command = @"C:\Tools\MyServer\MyServer.exe", + Args = new List(), + Cwd = @"C:\Tools\MyServer", + Tools = new List { "*" }, + }, + ["my-dotnet-tool"] = new McpLocalServerConfig + { + Type = "local", + Command = "dotnet", + Args = new List { @"C:\Tools\MyTool\MyTool.dll" }, + Cwd = @"C:\Tools\MyTool", + Tools = new List { "*" }, + } + }; + } +} +``` + ```csharp // Correct configuration for .NET exe ["my-dotnet-server"] = new McpLocalServerConfig @@ -267,7 +297,28 @@ cd /expected/working/dir #### NPX Commands - + +```csharp +using GitHub.Copilot.SDK; + +public static class McpNpxConfigExample +{ + public static void Main() + { + var servers = new Dictionary + { + ["filesystem"] = new McpLocalServerConfig + { + Type = "local", + Command = "cmd", + Args = new List { "/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", "C:\\allowed\\path" }, + Tools = new List { "*" }, + } + }; + } +} +``` + ```csharp // Windows needs cmd /c for npx ["filesystem"] = new McpLocalServerConfig @@ -304,7 +355,19 @@ xattr -d com.apple.quarantine /path/to/mcp-server #### Homebrew Paths - + +```typescript +import { MCPLocalServerConfig } from "@github/copilot-sdk"; + +const mcpServers: Record = { + "my-server": { + command: "/opt/homebrew/bin/node", + args: ["/path/to/server.js"], + tools: ["*"], + }, +}; +``` + ```typescript // GUI apps may not have /opt/homebrew in PATH mcpServers: { diff --git a/docs/opentelemetry-instrumentation.md b/docs/opentelemetry-instrumentation.md index f0e1b2556c..0ba9802019 100644 --- a/docs/opentelemetry-instrumentation.md +++ b/docs/opentelemetry-instrumentation.md @@ -165,7 +165,18 @@ await session.send({"prompt": "Hello"}) ``` **Event Data Structure:** - + +```python +from dataclasses import dataclass + +@dataclass +class Usage: + input_tokens: float + output_tokens: float + cache_read_tokens: float + cache_write_tokens: float +``` + ```python @dataclass class Usage: @@ -431,7 +442,21 @@ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true ### Checking at Runtime - + +```python +import os +from typing import Any + +span: Any = None +event: Any = None + +def should_record_content(): + return os.getenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false").lower() == "true" + +if should_record_content() and event.data.content: + span.add_event("gen_ai.output.messages", ...) +``` + ```python import os @@ -447,7 +472,23 @@ if should_record_content() and event.data.content: For MCP-based tools, add these additional attributes following the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/): - + +```python +from typing import Any + +data: Any = None +session: Any = None + +tool_attrs = { + "mcp.method.name": "tools/call", + "mcp.server.name": data.mcp_server_name, + "mcp.session.id": session.session_id, + "gen_ai.tool.name": data.mcp_tool_name, + "gen_ai.operation.name": "execute_tool", + "network.transport": "pipe", +} +``` + ```python tool_attrs = { # Required @@ -552,7 +593,16 @@ View traces in the Azure Portal under your Application Insights resource → Tra ### Tool spans not showing as children Make sure to attach the tool span to the parent context: - + +```python +from opentelemetry import trace, context +from opentelemetry.trace import SpanKind + +tracer = trace.get_tracer(__name__) +tool_span = tracer.start_span("test", kind=SpanKind.CLIENT) +tool_token = context.attach(trace.set_span_in_context(tool_span)) +``` + ```python tool_token = context.attach(trace.set_span_in_context(tool_span)) ``` From f4b0956a6a49012e35adf89e4a7d7e808ff03f2c Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 23:54:45 -0800 Subject: [PATCH 09/61] docs: reorganize docs/ into intent-based sections (#723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: reorganize docs/ around user intent Restructure the docs folder from an organic flat layout into intent-based sections that are easier to navigate: - setup/ — get it running (was guides/setup/, moved up one level) - auth/ — configure access (unchanged) - features/ — build things (was guides/, scoped to SDK capabilities) - hooks/ — API reference per hook (unchanged, overview.md → index.md) - troubleshooting/ — fix problems (new, consolidates debugging + compatibility) - observability/ — monitoring (new, houses OpenTelemetry docs) Key changes: - Add docs/index.md as top-level table of contents with persona routing - Add docs/features/index.md as feature guide overview - Remove duplicate guides/setup/byok.md (auth/byok.md is source of truth) - Rename hooks/overview.md → hooks/index.md for consistency - Move mcp/overview.md → features/mcp.md, mcp/debugging.md → troubleshooting/ - Update all internal cross-references (verified zero broken links) - Expand README Quick Links to surface more documentation sections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: move Microsoft Agent Framework to new integrations/ section Create a dedicated integrations/ section at the bottom of the docs hierarchy for platform/framework-specific guides. Move MAF out of features/ since it's an integration pattern, not an SDK capability. This makes it easy to add future guides for other platforms alongside MAF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 + docs/auth/index.md | 2 +- docs/{guides => features}/custom-agents.md | 0 docs/{guides => features}/hooks.md | 4 +- docs/{guides => features}/image-input.md | 0 docs/features/index.md | 25 ++ docs/{mcp/overview.md => features/mcp.md} | 6 +- .../session-persistence.md | 8 +- docs/{guides => features}/skills.md | 2 +- .../steering-and-queueing.md | 2 +- docs/{guides => features}/streaming-events.md | 0 docs/getting-started.md | 4 +- docs/guides/setup/byok.md | 392 ------------------ docs/hooks/error-handling.md | 4 +- docs/hooks/{overview.md => index.md} | 2 +- docs/hooks/post-tool-use.md | 2 +- docs/hooks/pre-tool-use.md | 4 +- docs/hooks/session-lifecycle.md | 4 +- docs/hooks/user-prompt-submitted.md | 2 +- docs/index.md | 76 ++++ .../microsoft-agent-framework.md | 4 +- .../opentelemetry.md} | 0 .../setup/azure-managed-identity.md | 6 +- docs/{guides => }/setup/backend-services.md | 6 +- docs/{guides => }/setup/bundled-cli.md | 10 +- docs/{guides => }/setup/github-oauth.md | 4 +- docs/{guides => }/setup/index.md | 8 +- docs/{guides => }/setup/local-cli.md | 8 +- docs/{guides => }/setup/scaling.md | 4 +- docs/{ => troubleshooting}/compatibility.md | 6 +- docs/{ => troubleshooting}/debugging.md | 10 +- .../mcp-debugging.md} | 4 +- 32 files changed, 163 insertions(+), 450 deletions(-) rename docs/{guides => features}/custom-agents.md (100%) rename docs/{guides => features}/hooks.md (99%) rename docs/{guides => features}/image-input.md (100%) create mode 100644 docs/features/index.md rename docs/{mcp/overview.md => features/mcp.md} (97%) rename docs/{guides => features}/session-persistence.md (98%) rename docs/{guides => features}/skills.md (99%) rename docs/{guides => features}/steering-and-queueing.md (99%) rename docs/{guides => features}/streaming-events.md (100%) delete mode 100644 docs/guides/setup/byok.md rename docs/hooks/{overview.md => index.md} (99%) create mode 100644 docs/index.md rename docs/{guides => integrations}/microsoft-agent-framework.md (98%) rename docs/{opentelemetry-instrumentation.md => observability/opentelemetry.md} (100%) rename docs/{guides => }/setup/azure-managed-identity.md (94%) rename docs/{guides => }/setup/backend-services.md (97%) rename docs/{guides => }/setup/bundled-cli.md (96%) rename docs/{guides => }/setup/github-oauth.md (98%) rename docs/{guides => }/setup/index.md (94%) rename docs/{guides => }/setup/local-cli.md (95%) rename docs/{guides => }/setup/scaling.md (99%) rename docs/{ => troubleshooting}/compatibility.md (98%) rename docs/{ => troubleshooting}/debugging.md (97%) rename docs/{mcp/debugging.md => troubleshooting/mcp-debugging.md} (98%) diff --git a/README.md b/README.md index be9b4694b6..b4770ed0be 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,12 @@ Please use the [GitHub Issues](https://github.com/github/copilot-sdk/issues) pag ## Quick Links +- **[Documentation](./docs/index.md)** – Full documentation index - **[Getting Started](./docs/getting-started.md)** – Tutorial to get up and running +- **[Setup Guides](./docs/setup/index.md)** – Architecture, deployment, and scaling - **[Authentication](./docs/auth/index.md)** – GitHub OAuth, BYOK, and more +- **[Features](./docs/features/index.md)** – Hooks, custom agents, MCP, skills, and more +- **[Troubleshooting](./docs/troubleshooting/debugging.md)** – Common issues and solutions - **[Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk)** – Practical recipes for common tasks across all languages - **[More Resources](https://github.com/github/awesome-copilot/blob/main/collections/copilot-sdk.md)** – Additional examples, tutorials, and community resources diff --git a/docs/auth/index.md b/docs/auth/index.md index 67bbed1aa9..2f36d8b21d 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -355,4 +355,4 @@ await using var client = new CopilotClient(new CopilotClientOptions - [BYOK Documentation](./byok.md) - Learn how to use your own API keys - [Getting Started Guide](../getting-started.md) - Build your first Copilot-powered app -- [MCP Servers](../mcp) - Connect to external tools +- [MCP Servers](../features/mcp.md) - Connect to external tools diff --git a/docs/guides/custom-agents.md b/docs/features/custom-agents.md similarity index 100% rename from docs/guides/custom-agents.md rename to docs/features/custom-agents.md diff --git a/docs/guides/hooks.md b/docs/features/hooks.md similarity index 99% rename from docs/guides/hooks.md rename to docs/features/hooks.md index eeb0ec4722..5c6c2f2c51 100644 --- a/docs/guides/hooks.md +++ b/docs/features/hooks.md @@ -973,7 +973,7 @@ const session = await client.createSession({ For full type definitions, input/output field tables, and additional examples for every hook, see the API reference: -- [Hooks Overview](../hooks/overview.md) +- [Hooks Overview](../hooks/index.md) - [Pre-Tool Use](../hooks/pre-tool-use.md) - [Post-Tool Use](../hooks/post-tool-use.md) - [User Prompt Submitted](../hooks/user-prompt-submitted.md) @@ -985,4 +985,4 @@ For full type definitions, input/output field tables, and additional examples fo - [Getting Started](../getting-started.md) - [Custom Agents & Sub-Agent Orchestration](./custom-agents.md) - [Streaming Session Events](./streaming-events.md) -- [Debugging Guide](../debugging.md) +- [Debugging Guide](../troubleshooting/debugging.md) diff --git a/docs/guides/image-input.md b/docs/features/image-input.md similarity index 100% rename from docs/guides/image-input.md rename to docs/features/image-input.md diff --git a/docs/features/index.md b/docs/features/index.md new file mode 100644 index 0000000000..3eb63a799a --- /dev/null +++ b/docs/features/index.md @@ -0,0 +1,25 @@ +# Features + +These guides cover the capabilities you can add to your Copilot SDK application. Each guide includes examples in all supported languages (TypeScript, Python, Go, and .NET). + +> **New to the SDK?** Start with the [Getting Started tutorial](../getting-started.md) first, then come back here to add more capabilities. + +## Guides + +| Feature | Description | +|---|---| +| [Hooks](./hooks.md) | Intercept and customize session behavior — control tool execution, transform results, handle errors | +| [Custom Agents](./custom-agents.md) | Define specialized sub-agents with scoped tools and instructions | +| [MCP Servers](./mcp.md) | Integrate Model Context Protocol servers for external tool access | +| [Skills](./skills.md) | Load reusable prompt modules from directories | +| [Image Input](./image-input.md) | Send images to sessions as attachments | +| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | +| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery — immediate steering vs. sequential queueing | +| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | + +## Related + +- [Hooks Reference](../hooks/index.md) — detailed API reference for each hook type +- [Integrations](../integrations/microsoft-agent-framework.md) — use the SDK with other platforms (MAF, etc.) +- [Troubleshooting](../troubleshooting/debugging.md) — when things don't work as expected +- [Compatibility](../troubleshooting/compatibility.md) — SDK vs CLI feature matrix diff --git a/docs/mcp/overview.md b/docs/features/mcp.md similarity index 97% rename from docs/mcp/overview.md rename to docs/features/mcp.md index 5ad8b1df39..f1ad381871 100644 --- a/docs/mcp/overview.md +++ b/docs/features/mcp.md @@ -258,7 +258,7 @@ directories for different applications. | "Timeout" errors | Increase the `timeout` value or check server performance | | Tools work but aren't called | Ensure your prompt clearly requires the tool's functionality | -For detailed debugging guidance, see the **[MCP Debugging Guide](./debugging.md)**. +For detailed debugging guidance, see the **[MCP Debugging Guide](../troubleshooting/mcp-debugging.md)**. ## Related Resources @@ -266,10 +266,10 @@ For detailed debugging guidance, see the **[MCP Debugging Guide](./debugging.md) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Community MCP servers - [GitHub MCP Server](https://github.com/github/github-mcp-server) - Official GitHub MCP server - [Getting Started Guide](../getting-started.md) - SDK basics and custom tools -- [General Debugging Guide](../debugging.md) - SDK-wide debugging +- [General Debugging Guide](.../troubleshooting/mcp-debugging.md) - SDK-wide debugging ## See Also -- [MCP Debugging Guide](./debugging.md) - Detailed MCP troubleshooting +- [MCP Debugging Guide](../troubleshooting/mcp-debugging.md) - Detailed MCP troubleshooting - [Issue #9](https://github.com/github/copilot-sdk/issues/9) - Original MCP tools usage question - [Issue #36](https://github.com/github/copilot-sdk/issues/36) - MCP documentation tracking issue diff --git a/docs/guides/session-persistence.md b/docs/features/session-persistence.md similarity index 98% rename from docs/guides/session-persistence.md rename to docs/features/session-persistence.md index df96c9ea00..7f3759df82 100644 --- a/docs/guides/session-persistence.md +++ b/docs/features/session-persistence.md @@ -561,7 +561,7 @@ const session = await client.createSession({ }); ``` -> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the [Compatibility Guide](../compatibility.md) for details. +> **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. See the [Compatibility Guide](../troubleshooting/compatibility.md) for details. ## Limitations & Considerations @@ -621,6 +621,6 @@ await withSessionLock("user-123-task-456", async () => { ## Next Steps -- [Hooks Overview](../hooks/overview.md) - Customize session behavior with hooks -- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison -- [Debugging Guide](../debugging.md) - Troubleshoot session issues +- [Hooks Overview](../hooks/index.md) - Customize session behavior with hooks +- [Compatibility Guide](../troubleshooting/compatibility.md) - SDK vs CLI feature comparison +- [Debugging Guide](../troubleshooting/debugging.md) - Troubleshoot session issues diff --git a/docs/guides/skills.md b/docs/features/skills.md similarity index 99% rename from docs/guides/skills.md rename to docs/features/skills.md index 8ed9402513..1d584ced18 100644 --- a/docs/guides/skills.md +++ b/docs/features/skills.md @@ -365,4 +365,4 @@ If multiple skills provide conflicting instructions: - [Custom Agents](../getting-started.md#create-custom-agents) - Define specialized AI personas - [Custom Tools](../getting-started.md#step-4-add-a-custom-tool) - Build your own tools -- [MCP Servers](../mcp/overview.md) - Connect external tool providers +- [MCP Servers](./mcp.md) - Connect external tool providers diff --git a/docs/guides/steering-and-queueing.md b/docs/features/steering-and-queueing.md similarity index 99% rename from docs/guides/steering-and-queueing.md rename to docs/features/steering-and-queueing.md index ef2eb46dbd..ad27c4ee06 100644 --- a/docs/guides/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -575,5 +575,5 @@ class InteractiveChat { - [Getting Started](../getting-started.md) — Set up a session and send messages - [Custom Agents](./custom-agents.md) — Define specialized agents with scoped tools -- [Session Hooks](../hooks/overview.md) — React to session lifecycle events +- [Session Hooks](../hooks/index.md) — React to session lifecycle events - [Session Persistence](./session-persistence.md) — Resume sessions across restarts diff --git a/docs/guides/streaming-events.md b/docs/features/streaming-events.md similarity index 100% rename from docs/guides/streaming-events.md rename to docs/features/streaming-events.md diff --git a/docs/getting-started.md b/docs/getting-started.md index 05bbde8dc6..de95a02761 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1224,7 +1224,7 @@ const session = await client.createSession({ }); ``` -📖 **[Full MCP documentation →](./mcp/overview.md)** - Learn about local vs remote servers, all configuration options, and troubleshooting. +📖 **[Full MCP documentation →](./features/mcp.md)** - Learn about local vs remote servers, all configuration options, and troubleshooting. ### Create Custom Agents @@ -1401,7 +1401,7 @@ await using var session = await client.CreateSessionAsync(new() - [Python SDK Reference](../python/README.md) - [Go SDK Reference](../go/README.md) - [.NET SDK Reference](../dotnet/README.md) -- [Using MCP Servers](./mcp) - Integrate external tools via Model Context Protocol +- [Using MCP Servers](./features/mcp.md) - Integrate external tools via Model Context Protocol - [GitHub MCP Server Documentation](https://github.com/github/github-mcp-server) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Explore more MCP servers diff --git a/docs/guides/setup/byok.md b/docs/guides/setup/byok.md deleted file mode 100644 index 35c5f1adc4..0000000000 --- a/docs/guides/setup/byok.md +++ /dev/null @@ -1,392 +0,0 @@ -# BYOK (Bring Your Own Key) Setup - -Use your own model provider API keys instead of GitHub Copilot authentication. You control the identity layer, the model provider, and the billing — the SDK provides the agent runtime. - -**Best for:** Apps where users don't have GitHub accounts, enterprise deployments with existing model provider contracts, apps needing full control over identity and billing. - -## How It Works - -With BYOK, the SDK uses the Copilot CLI as an agent runtime only — it doesn't call GitHub's Copilot API. Instead, model requests go directly to your configured provider (OpenAI, Azure AI Foundry, Anthropic, etc.). - -```mermaid -flowchart LR - subgraph App["Your Application"] - SDK["SDK Client"] - IdP["Your Identity
Provider"] - end - - subgraph CLI["Copilot CLI"] - Runtime["Agent Runtime"] - end - - subgraph Provider["Your Model Provider"] - API["OpenAI / Azure /
Anthropic / Ollama"] - end - - IdP -.->|"authenticates
users"| SDK - SDK --> Runtime - Runtime -- "API key" --> API - - style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 - style CLI fill:#0d1117,stroke:#3fb950,color:#c9d1d9 - style Provider fill:#161b22,stroke:#f0883e,color:#c9d1d9 -``` - -**Key characteristics:** -- No GitHub Copilot subscription needed -- No GitHub account needed for end users -- You manage authentication and identity yourself -- Model requests go to your provider, billed to your account -- Full agent runtime capabilities (tools, sessions, streaming) still work - -## Architecture: GitHub Auth vs. BYOK - -```mermaid -flowchart TB - subgraph GitHub["GitHub Auth Path"] - direction LR - G1["User"] --> G2["GitHub OAuth"] - G2 --> G3["SDK + CLI"] - G3 --> G4["☁️ Copilot API"] - end - - subgraph BYOK["BYOK Path"] - direction LR - B1["User"] --> B2["Your Auth"] - B2 --> B3["SDK + CLI"] - B3 --> B4["☁️ Your Provider"] - end - - style GitHub fill:#161b22,stroke:#8b949e,color:#c9d1d9 - style BYOK fill:#0d1117,stroke:#3fb950,color:#c9d1d9 -``` - -## Quick Start - -
-Node.js / TypeScript - -```typescript -import { CopilotClient } from "@github/copilot-sdk"; - -const client = new CopilotClient(); - -const session = await client.createSession({ - model: "gpt-4.1", - provider: { - type: "openai", - baseUrl: "https://api.openai.com/v1", - apiKey: process.env.OPENAI_API_KEY, - }, -}); - -const response = await session.sendAndWait({ prompt: "Hello!" }); -console.log(response?.data.content); - -await client.stop(); -``` - -
- -
-Python - -```python -import os -from copilot import CopilotClient - -client = CopilotClient() -await client.start() - -session = await client.create_session({ - "model": "gpt-4.1", - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": os.environ["OPENAI_API_KEY"], - }, -}) - -response = await session.send_and_wait({"prompt": "Hello!"}) -print(response.data.content) - -await client.stop() -``` - -
- -
-Go - - -```go -package main - -import ( - "context" - "fmt" - "os" - copilot "github.com/github/copilot-sdk/go" -) - -func main() { - ctx := context.Background() - - client := copilot.NewClient(nil) - client.Start(ctx) - defer client.Stop() - - session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", - Provider: &copilot.ProviderConfig{ - Type: "openai", - BaseURL: "https://api.openai.com/v1", - APIKey: os.Getenv("OPENAI_API_KEY"), - }, - }) - - response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) - fmt.Println(*response.Data.Content) -} -``` - - -```go -client := copilot.NewClient(nil) -client.Start(ctx) -defer client.Stop() - -session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ - Model: "gpt-4.1", - Provider: &copilot.ProviderConfig{ - Type: "openai", - BaseURL: "https://api.openai.com/v1", - APIKey: os.Getenv("OPENAI_API_KEY"), - }, -}) - -response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) -fmt.Println(*response.Data.Content) -``` - -
- -
-.NET - -```csharp -await using var client = new CopilotClient(); -await using var session = await client.CreateSessionAsync(new SessionConfig -{ - Model = "gpt-4.1", - Provider = new ProviderConfig - { - Type = "openai", - BaseUrl = "https://api.openai.com/v1", - ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"), - }, -}); - -var response = await session.SendAndWaitAsync( - new MessageOptions { Prompt = "Hello!" }); -Console.WriteLine(response?.Data.Content); -``` - -
- -## Provider Configurations - -### OpenAI - -```typescript -provider: { - type: "openai", - baseUrl: "https://api.openai.com/v1", - apiKey: process.env.OPENAI_API_KEY, -} -``` - -### Azure AI Foundry - -```typescript -provider: { - type: "openai", - baseUrl: "https://your-resource.openai.azure.com/openai/v1/", - apiKey: process.env.FOUNDRY_API_KEY, - wireApi: "responses", // For GPT-5 series models -} -``` - -### Azure OpenAI (Native) - -```typescript -provider: { - type: "azure", - baseUrl: "https://your-resource.openai.azure.com", - apiKey: process.env.AZURE_OPENAI_KEY, - azure: { apiVersion: "2024-10-21" }, -} -``` - -### Anthropic - -```typescript -provider: { - type: "anthropic", - baseUrl: "https://api.anthropic.com", - apiKey: process.env.ANTHROPIC_API_KEY, -} -``` - -### Ollama (Local) - -```typescript -provider: { - type: "openai", - baseUrl: "http://localhost:11434/v1", - // No API key needed for local Ollama -} -``` - -## Managing Identity Yourself - -With BYOK, you're responsible for authentication. Here are common patterns: - -### Pattern 1: Your Own Identity Provider - -```mermaid -sequenceDiagram - participant User - participant App as Your App - participant IdP as Your Identity Provider - participant SDK as SDK + CLI - participant LLM as Model Provider - - User->>App: Login - App->>IdP: Authenticate user - IdP-->>App: User identity + permissions - - App->>App: Look up API key for user's tier - App->>SDK: Create session (with provider config) - SDK->>LLM: Model request (your API key) - LLM-->>SDK: Response - SDK-->>App: Result - App-->>User: Display -``` - -```typescript -// Your app handles auth, then creates sessions with your API key -app.post("/chat", authMiddleware, async (req, res) => { - const user = req.user; // From your auth middleware - - // Use your API key — not the user's - const session = await getOrCreateSession(user.id, { - model: getModelForTier(user.tier), // "gpt-4.1" for pro, etc. - provider: { - type: "openai", - baseUrl: "https://api.openai.com/v1", - apiKey: process.env.OPENAI_API_KEY, // Your key, your billing - }, - }); - - const response = await session.sendAndWait({ prompt: req.body.message }); - res.json({ content: response?.data.content }); -}); -``` - -### Pattern 2: Per-Customer API Keys - -For B2B apps where each customer brings their own model provider keys: - -```mermaid -flowchart TB - subgraph Customers - C1["Customer A
(OpenAI key)"] - C2["Customer B
(Azure key)"] - C3["Customer C
(Anthropic key)"] - end - - subgraph App["Your App"] - Router["Request Router"] - KS["Key Store
(encrypted)"] - end - - C1 --> Router - C2 --> Router - C3 --> Router - - Router --> KS - KS --> SDK1["SDK → OpenAI"] - KS --> SDK2["SDK → Azure"] - KS --> SDK3["SDK → Anthropic"] - - style App fill:#0d1117,stroke:#58a6ff,color:#c9d1d9 -``` - -```typescript -async function createSessionForCustomer(customerId: string) { - const config = await keyStore.getProviderConfig(customerId); - - return client.createSession({ - sessionId: `customer-${customerId}-${Date.now()}`, - model: config.model, - provider: { - type: config.providerType, - baseUrl: config.baseUrl, - apiKey: config.apiKey, - }, - }); -} -``` - -## Session Persistence with BYOK - -When resuming BYOK sessions, you **must** re-provide the provider configuration. API keys are never persisted to disk for security. - -```typescript -// Create session -const session = await client.createSession({ - sessionId: "task-123", - model: "gpt-4.1", - provider: { - type: "openai", - baseUrl: "https://api.openai.com/v1", - apiKey: process.env.OPENAI_API_KEY, - }, -}); - -// Resume later — must re-provide provider config -const resumed = await client.resumeSession("task-123", { - provider: { - type: "openai", - baseUrl: "https://api.openai.com/v1", - apiKey: process.env.OPENAI_API_KEY, // Required again - }, -}); -``` - -## Limitations - -| Limitation | Details | -|------------|---------| -| **Static credentials only** | API keys or bearer tokens — no native Entra ID, OIDC, or managed identity support. See [Azure Managed Identity workaround](./azure-managed-identity.md) for using `DefaultAzureCredential` with short-lived tokens. | -| **No auto-refresh** | If a bearer token expires, you must create a new session | -| **Your billing** | All model usage is billed to your provider account | -| **Model availability** | Limited to what your provider offers | -| **Keys not persisted** | Must re-provide on session resume | - -For the full BYOK reference, see the **[BYOK documentation](../../auth/byok.md)**. - -## When to Move On - -| Need | Next Guide | -|------|-----------| -| Run the SDK on a server | [Backend Services](./backend-services.md) | -| Multiple users with GitHub accounts | [GitHub OAuth](./github-oauth.md) | -| Handle many concurrent users | [Scaling & Multi-Tenancy](./scaling.md) | - -## Next Steps - -- **[BYOK reference](../../auth/byok.md)** — Full provider config details and troubleshooting -- **[Backend Services](./backend-services.md)** — Deploy the SDK server-side -- **[Scaling & Multi-Tenancy](./scaling.md)** — Serve many customers at scale diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index c3c8fa529c..2e7848bc5e 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -471,6 +471,6 @@ const session = await client.createSession({ ## See Also -- [Hooks Overview](./overview.md) +- [Hooks Overview](./index.md) - [Session Lifecycle Hooks](./session-lifecycle.md) -- [Debugging Guide](../debugging.md) +- [Debugging Guide](../troubleshooting/debugging.md) diff --git a/docs/hooks/overview.md b/docs/hooks/index.md similarity index 99% rename from docs/hooks/overview.md rename to docs/hooks/index.md index a51ef04640..b09701066b 100644 --- a/docs/hooks/overview.md +++ b/docs/hooks/index.md @@ -233,4 +233,4 @@ const session = await client.createSession({ - [Getting Started Guide](../getting-started.md) - [Custom Tools](../getting-started.md#step-4-add-a-custom-tool) -- [Debugging Guide](../debugging.md) +- [Debugging Guide](../troubleshooting/debugging.md) diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index d50ff6b488..415acce9e6 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -428,6 +428,6 @@ const session = await client.createSession({ ## See Also -- [Hooks Overview](./overview.md) +- [Hooks Overview](./index.md) - [Pre-Tool Use Hook](./pre-tool-use.md) - [Error Handling Hook](./error-handling.md) diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index a09fca6ef3..df194aaf3e 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -386,6 +386,6 @@ const session = await client.createSession({ ## See Also -- [Hooks Overview](./overview.md) +- [Hooks Overview](./index.md) - [Post-Tool Use Hook](./post-tool-use.md) -- [Debugging Guide](../debugging.md) +- [Debugging Guide](../troubleshooting/debugging.md) diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 22ff5dd61f..93696530e7 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -504,6 +504,6 @@ Session Summary: ## See Also -- [Hooks Overview](./overview.md) +- [Hooks Overview](./index.md) - [Error Handling Hook](./error-handling.md) -- [Debugging Guide](../debugging.md) +- [Debugging Guide](../troubleshooting/debugging.md) diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 0eb959d477..370c37b8c5 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -446,6 +446,6 @@ const session = await client.createSession({ ## See Also -- [Hooks Overview](./overview.md) +- [Hooks Overview](./index.md) - [Session Lifecycle Hooks](./session-lifecycle.md) - [Pre-Tool Use Hook](./pre-tool-use.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..9459a7b808 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,76 @@ +# GitHub Copilot SDK Documentation + +Welcome to the GitHub Copilot SDK docs. Whether you're building your first Copilot-powered app or deploying to production, you'll find what you need here. + +## Where to Start + +| I want to... | Go to | +|---|---| +| **Build my first app** | [Getting Started](./getting-started.md) — end-to-end tutorial with streaming & custom tools | +| **Set up for production** | [Setup Guides](./setup/index.md) — architecture, deployment patterns, scaling | +| **Configure authentication** | [Authentication](./auth/index.md) — GitHub OAuth, environment variables, BYOK | +| **Add features to my app** | [Features](./features/index.md) — hooks, custom agents, MCP, skills, and more | +| **Debug an issue** | [Troubleshooting](./troubleshooting/debugging.md) — common problems and solutions | + +## Documentation Map + +### [Getting Started](./getting-started.md) + +Step-by-step tutorial that takes you from zero to a working Copilot app with streaming responses and custom tools. + +### [Setup](./setup/index.md) + +How to configure and deploy the SDK for your use case. + +- [Local CLI](./setup/local-cli.md) — simplest path, uses your signed-in CLI +- [Bundled CLI](./setup/bundled-cli.md) — ship the CLI with your app +- [Backend Services](./setup/backend-services.md) — server-side with headless CLI over TCP +- [GitHub OAuth](./setup/github-oauth.md) — implement the OAuth flow +- [Azure Managed Identity](./setup/azure-managed-identity.md) — BYOK with Azure AI Foundry +- [Scaling & Multi-Tenancy](./setup/scaling.md) — horizontal scaling, isolation patterns + +### [Authentication](./auth/index.md) + +Configuring how users and services authenticate with Copilot. + +- [Authentication Overview](./auth/index.md) — methods, priority order, and examples +- [Bring Your Own Key (BYOK)](./auth/byok.md) — use your own API keys from OpenAI, Azure, Anthropic, and more + +### [Features](./features/index.md) + +Guides for building with the SDK's capabilities. + +- [Hooks](./features/hooks.md) — intercept and customize session behavior +- [Custom Agents](./features/custom-agents.md) — define specialized sub-agents +- [MCP Servers](./features/mcp.md) — integrate Model Context Protocol servers +- [Skills](./features/skills.md) — load reusable prompt modules +- [Image Input](./features/image-input.md) — send images as attachments +- [Streaming Events](./features/streaming-events.md) — real-time event reference +- [Steering & Queueing](./features/steering-and-queueing.md) — message delivery modes +- [Session Persistence](./features/session-persistence.md) — resume sessions across restarts + +### [Hooks Reference](./hooks/index.md) + +Detailed API reference for each session hook. + +- [Pre-Tool Use](./hooks/pre-tool-use.md) — approve, deny, or modify tool calls +- [Post-Tool Use](./hooks/post-tool-use.md) — transform tool results +- [User Prompt Submitted](./hooks/user-prompt-submitted.md) — modify or filter user messages +- [Session Lifecycle](./hooks/session-lifecycle.md) — session start and end +- [Error Handling](./hooks/error-handling.md) — custom error handling + +### [Troubleshooting](./troubleshooting/debugging.md) + +- [Debugging Guide](./troubleshooting/debugging.md) — common issues and solutions +- [MCP Debugging](./troubleshooting/mcp-debugging.md) — MCP-specific troubleshooting +- [Compatibility](./troubleshooting/compatibility.md) — SDK vs CLI feature matrix + +### [Observability](./observability/opentelemetry.md) + +- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — add tracing to your SDK usage + +### [Integrations](./integrations/microsoft-agent-framework.md) + +Guides for using the SDK with other platforms and frameworks. + +- [Microsoft Agent Framework](./integrations/microsoft-agent-framework.md) — MAF multi-agent workflows diff --git a/docs/guides/microsoft-agent-framework.md b/docs/integrations/microsoft-agent-framework.md similarity index 98% rename from docs/guides/microsoft-agent-framework.md rename to docs/integrations/microsoft-agent-framework.md index 0b7635e429..8e794759b7 100644 --- a/docs/guides/microsoft-agent-framework.md +++ b/docs/integrations/microsoft-agent-framework.md @@ -450,7 +450,7 @@ catch (AgentException ex) ## See Also - [Getting Started](../getting-started.md) — initial Copilot SDK setup -- [Custom Agents](./custom-agents.md) — define specialized sub-agents within the SDK -- [Custom Skills](./skills.md) — reusable prompt modules +- [Custom Agents](../features/custom-agents.md) — define specialized sub-agents within the SDK +- [Custom Skills](../features/skills.md) — reusable prompt modules - [Microsoft Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/agents/providers/github-copilot) — official MAF docs for the Copilot provider - [Blog: Build AI Agents with GitHub Copilot SDK and Microsoft Agent Framework](https://devblogs.microsoft.com/semantic-kernel/build-ai-agents-with-github-copilot-sdk-and-microsoft-agent-framework/) diff --git a/docs/opentelemetry-instrumentation.md b/docs/observability/opentelemetry.md similarity index 100% rename from docs/opentelemetry-instrumentation.md rename to docs/observability/opentelemetry.md diff --git a/docs/guides/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md similarity index 94% rename from docs/guides/setup/azure-managed-identity.md rename to docs/setup/azure-managed-identity.md index 9ad1ddb157..b2fa152645 100644 --- a/docs/guides/setup/azure-managed-identity.md +++ b/docs/setup/azure-managed-identity.md @@ -1,6 +1,6 @@ # Azure Managed Identity with BYOK -The Copilot SDK's [BYOK mode](./byok.md) accepts static API keys, but Azure deployments often use **Managed Identity** (Entra ID) instead of long-lived keys. Since the SDK doesn't natively support Entra ID authentication, you can use a short-lived bearer token via the `bearer_token` provider config field. +The Copilot SDK's [BYOK mode](../auth/byok.md) accepts static API keys, but Azure deployments often use **Managed Identity** (Entra ID) instead of long-lived keys. Since the SDK doesn't natively support Entra ID authentication, you can use a short-lived bearer token via the `bearer_token` provider config field. This guide shows how to use `DefaultAzureCredential` from the [Azure Identity](https://learn.microsoft.com/python/api/azure-identity/azure.identity.defaultazurecredential) library to authenticate with Azure AI Foundry models through the Copilot SDK. @@ -207,11 +207,11 @@ See the [DefaultAzureCredential documentation](https://learn.microsoft.com/pytho | Azure-hosted app with Managed Identity | ✅ Use this pattern | | App with existing Azure AD service principal | ✅ Use this pattern | | Local development with `az login` | ✅ Use this pattern | -| Non-Azure environment with static API key | Use [standard BYOK](./byok.md) | +| Non-Azure environment with static API key | Use [standard BYOK](../auth/byok.md) | | GitHub Copilot subscription available | Use [GitHub OAuth](./github-oauth.md) | ## See Also -- [BYOK Setup Guide](./byok.md) — Static API key configuration +- [BYOK Setup Guide](../auth/byok.md) — Static API key configuration - [Backend Services](./backend-services.md) — Server-side deployment - [Azure Identity documentation](https://learn.microsoft.com/python/api/overview/azure/identity-readme) diff --git a/docs/guides/setup/backend-services.md b/docs/setup/backend-services.md similarity index 97% rename from docs/guides/setup/backend-services.md rename to docs/setup/backend-services.md index 494d3574c8..a7bc6c8c96 100644 --- a/docs/guides/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -280,7 +280,7 @@ app.post("/chat", authMiddleware, async (req, res) => { ### BYOK (No GitHub Auth) -Use your own API keys for the model provider. See [BYOK](./byok.md) for details. +Use your own API keys for the model provider. See [BYOK](../auth/byok.md) for details. ```typescript const client = new CopilotClient({ @@ -478,10 +478,10 @@ setInterval(() => cleanupSessions(24 * 60 * 60 * 1000), 60 * 60 * 1000); |------|-----------| | Multiple CLI servers / high availability | [Scaling & Multi-Tenancy](./scaling.md) | | GitHub account auth for users | [GitHub OAuth](./github-oauth.md) | -| Your own model keys | [BYOK](./byok.md) | +| Your own model keys | [BYOK](../auth/byok.md) | ## Next Steps - **[Scaling & Multi-Tenancy](./scaling.md)** — Handle more users, add redundancy -- **[Session Persistence](../session-persistence.md)** — Resume sessions across restarts +- **[Session Persistence](../features/session-persistence.md)** — Resume sessions across restarts - **[GitHub OAuth](./github-oauth.md)** — Add user authentication diff --git a/docs/guides/setup/bundled-cli.md b/docs/setup/bundled-cli.md similarity index 96% rename from docs/guides/setup/bundled-cli.md rename to docs/setup/bundled-cli.md index 8c5b0cbdd4..04df0286f1 100644 --- a/docs/guides/setup/bundled-cli.md +++ b/docs/setup/bundled-cli.md @@ -231,7 +231,7 @@ const session = await client.createSession({ }); ``` -See the **[BYOK guide](./byok.md)** for full details. +See the **[BYOK guide](../auth/byok.md)** for full details. ## Session Management @@ -346,10 +346,10 @@ const client = new CopilotClient({ |------|-----------| | Users signing in with GitHub accounts | [GitHub OAuth](./github-oauth.md) | | Run on a server instead of user machines | [Backend Services](./backend-services.md) | -| Use your own model keys | [BYOK](./byok.md) | +| Use your own model keys | [BYOK](../auth/byok.md) | ## Next Steps -- **[BYOK guide](./byok.md)** — Use your own model provider keys -- **[Session Persistence](../session-persistence.md)** — Advanced session management -- **[Getting Started tutorial](../../getting-started.md)** — Build a complete app +- **[BYOK guide](../auth/byok.md)** — Use your own model provider keys +- **[Session Persistence](../features/session-persistence.md)** — Advanced session management +- **[Getting Started tutorial](../getting-started.md)** — Build a complete app diff --git a/docs/guides/setup/github-oauth.md b/docs/setup/github-oauth.md similarity index 98% rename from docs/guides/setup/github-oauth.md rename to docs/setup/github-oauth.md index aa12542e53..e7b1c634a5 100644 --- a/docs/guides/setup/github-oauth.md +++ b/docs/setup/github-oauth.md @@ -432,12 +432,12 @@ For a lighter resource footprint, you can run a single external CLI server and p | Need | Next Guide | |------|-----------| -| Users without GitHub accounts | [BYOK](./byok.md) | +| Users without GitHub accounts | [BYOK](../auth/byok.md) | | Run the SDK on servers | [Backend Services](./backend-services.md) | | Handle many concurrent users | [Scaling & Multi-Tenancy](./scaling.md) | ## Next Steps -- **[Authentication docs](../../auth/index.md)** — Full auth method reference +- **[Authentication docs](../auth/index.md)** — Full auth method reference - **[Backend Services](./backend-services.md)** — Run the SDK server-side - **[Scaling & Multi-Tenancy](./scaling.md)** — Handle many users at scale diff --git a/docs/guides/setup/index.md b/docs/setup/index.md similarity index 94% rename from docs/guides/setup/index.md rename to docs/setup/index.md index 2613fe29df..268e266882 100644 --- a/docs/guides/setup/index.md +++ b/docs/setup/index.md @@ -58,7 +58,7 @@ You're building a product for customers. You need to handle authentication for y **Start with:** 1. **[GitHub OAuth](./github-oauth.md)** — Let customers sign in with GitHub -2. **[BYOK](./byok.md)** — Manage identity yourself with your own model keys +2. **[BYOK](../auth/byok.md)** — Manage identity yourself with your own model keys 3. **[Backend Services](./backend-services.md)** — Power your product from server-side code **For production:** @@ -74,7 +74,7 @@ You're embedding Copilot into a platform — APIs, developer tools, or infrastru **Depending on your auth model:** 3. **[GitHub OAuth](./github-oauth.md)** — For GitHub-authenticated users -4. **[BYOK](./byok.md)** — For self-managed identity and model access +4. **[BYOK](../auth/byok.md)** — For self-managed identity and model access ## Decision Matrix @@ -85,7 +85,7 @@ Use this table to find the right guides based on what you need to do: | Simplest possible setup | [Local CLI](./local-cli.md) | | Ship a standalone app with Copilot | [Bundled CLI](./bundled-cli.md) | | Users sign in with GitHub | [GitHub OAuth](./github-oauth.md) | -| Use your own model keys (OpenAI, Azure, etc.) | [BYOK](./byok.md) | +| Use your own model keys (OpenAI, Azure, etc.) | [BYOK](../auth/byok.md) | | Azure BYOK with Managed Identity (no API keys) | [Azure Managed Identity](./azure-managed-identity.md) | | Run the SDK on a server | [Backend Services](./backend-services.md) | | Serve multiple users / scale horizontally | [Scaling & Multi-Tenancy](./scaling.md) | @@ -136,7 +136,7 @@ All guides assume you have: - Go: `go get github.com/github/copilot-sdk/go` - .NET: `dotnet add package GitHub.Copilot.SDK` -If you're brand new, start with the **[Getting Started tutorial](../../getting-started.md)** first, then come back here for production configuration. +If you're brand new, start with the **[Getting Started tutorial](../getting-started.md)** first, then come back here for production configuration. ## Next Steps diff --git a/docs/guides/setup/local-cli.md b/docs/setup/local-cli.md similarity index 95% rename from docs/guides/setup/local-cli.md rename to docs/setup/local-cli.md index 402368fcca..188c511d45 100644 --- a/docs/guides/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -225,10 +225,10 @@ If you need any of these, it's time to pick a more advanced setup: | Ship your app to others | [Bundled CLI](./bundled-cli.md) | | Multiple users signing in | [GitHub OAuth](./github-oauth.md) | | Run on a server | [Backend Services](./backend-services.md) | -| Use your own model keys | [BYOK](./byok.md) | +| Use your own model keys | [BYOK](../auth/byok.md) | ## Next Steps -- **[Getting Started tutorial](../../getting-started.md)** — Build a complete interactive app -- **[Authentication docs](../../auth/index.md)** — All auth methods in detail -- **[Session Persistence](../session-persistence.md)** — Advanced session management +- **[Getting Started tutorial](../getting-started.md)** — Build a complete interactive app +- **[Authentication docs](../auth/index.md)** — All auth methods in detail +- **[Session Persistence](../features/session-persistence.md)** — Advanced session management diff --git a/docs/guides/setup/scaling.md b/docs/setup/scaling.md similarity index 99% rename from docs/guides/setup/scaling.md rename to docs/setup/scaling.md index 974276e5e1..325d9244dd 100644 --- a/docs/guides/setup/scaling.md +++ b/docs/setup/scaling.md @@ -629,7 +629,7 @@ flowchart TB ## Next Steps -- **[Session Persistence](../session-persistence.md)** — Deep dive on resumable sessions +- **[Session Persistence](../features/session-persistence.md)** — Deep dive on resumable sessions - **[Backend Services](./backend-services.md)** — Core server-side setup - **[GitHub OAuth](./github-oauth.md)** — Multi-user authentication -- **[BYOK](./byok.md)** — Use your own model provider +- **[BYOK](../auth/byok.md)** — Use your own model provider diff --git a/docs/compatibility.md b/docs/troubleshooting/compatibility.md similarity index 98% rename from docs/compatibility.md rename to docs/troubleshooting/compatibility.md index bfd17915b5..af304ac6ef 100644 --- a/docs/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -182,7 +182,7 @@ The SDK and CLI must have compatible protocol versions. The SDK will log warning ## See Also -- [Getting Started Guide](./getting-started.md) -- [Hooks Documentation](./hooks/overview.md) -- [MCP Servers Guide](./mcp/overview.md) +- [Getting Started Guide](../getting-started.md) +- [Hooks Documentation](../hooks/index.md) +- [MCP Servers Guide](../features/mcp.md) - [Debugging Guide](./debugging.md) diff --git a/docs/debugging.md b/docs/troubleshooting/debugging.md similarity index 97% rename from docs/debugging.md rename to docs/troubleshooting/debugging.md index b74ff51cac..4bb2616217 100644 --- a/docs/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -316,7 +316,7 @@ var client = new CopilotClient(new CopilotClientOptions ## MCP Server Debugging -MCP (Model Context Protocol) servers can be tricky to debug. For comprehensive MCP debugging guidance, see the dedicated **[MCP Debugging Guide](./mcp/debugging.md)**. +MCP (Model Context Protocol) servers can be tricky to debug. For comprehensive MCP debugging guidance, see the dedicated **[MCP Debugging Guide](./mcp-debugging.md)**. ### Quick MCP Checklist @@ -334,7 +334,7 @@ Before integrating with the SDK, verify your MCP server works: echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /path/to/your/mcp-server ``` -See [MCP Debugging Guide](./mcp/debugging.md) for detailed troubleshooting. +See [MCP Debugging Guide](./mcp-debugging.md) for detailed troubleshooting. --- @@ -519,7 +519,7 @@ If you're still stuck: ## See Also -- [Getting Started Guide](./getting-started.md) -- [MCP Overview](./mcp/overview.md) - MCP configuration and setup -- [MCP Debugging Guide](./mcp/debugging.md) - Detailed MCP troubleshooting +- [Getting Started Guide](../getting-started.md) +- [MCP Overview](../features/mcp.md) - MCP configuration and setup +- [MCP Debugging Guide](./mcp-debugging.md) - Detailed MCP troubleshooting - [API Reference](https://github.com/github/copilot-sdk) diff --git a/docs/mcp/debugging.md b/docs/troubleshooting/mcp-debugging.md similarity index 98% rename from docs/mcp/debugging.md rename to docs/troubleshooting/mcp-debugging.md index 783a4af8b6..30e05fd3e9 100644 --- a/docs/mcp/debugging.md +++ b/docs/troubleshooting/mcp-debugging.md @@ -473,6 +473,6 @@ When opening an issue or asking for help, collect: ## See Also -- [MCP Overview](./overview.md) - Configuration and setup -- [General Debugging Guide](../debugging.md) - SDK-wide debugging +- [MCP Overview](../features/mcp.md) - Configuration and setup +- [General Debugging Guide](./debugging.md) - SDK-wide debugging - [MCP Specification](https://modelcontextprotocol.io/) - Official protocol docs From 7766b1a3ba7455a2e18210726afe06fa8c686381 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sun, 8 Mar 2026 00:00:41 -0800 Subject: [PATCH 10/61] feat: add agent parameter to session creation for pre-selecting custom agents (#722) Add an optional `agent` field to SessionConfig and ResumeSessionConfig across all four SDKs (Node.js, Python, Go, .NET) that allows specifying which custom agent should be active when the session starts. Previously, users had to create a session and then make a separate `session.rpc.agent.select()` call to activate a specific custom agent. This change allows setting the agent directly in the session config, equivalent to passing `--agent ` in the Copilot CLI. The `agent` value must match the `name` of one of the agents defined in `customAgents`. Changes: - Node.js: Added `agent?: string` to SessionConfig and ResumeSessionConfig, wired in client.ts for both session.create and session.resume RPC calls - Python: Added `agent: str` to SessionConfig and ResumeSessionConfig, wired in client.py for both create and resume payloads - Go: Added `Agent string` to SessionConfig and ResumeSessionConfig, wired in client.go for both request types - .NET: Added `Agent` property to SessionConfig and ResumeSessionConfig, updated copy constructors, CreateSessionRequest/ResumeSessionRequest records, and CreateSessionAsync/ResumeSessionAsync call sites - Docs: Added "Selecting an Agent at Session Creation" section with examples in all 4 languages to custom-agents.md, updated session-persistence.md and getting-started.md - Tests: Added unit tests verifying agent parameter is forwarded in both session.create and session.resume RPC calls Closes #317, closes #410, closes #547 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/custom-agents.md | 96 ++++++++++++++++++++++++++++ docs/features/session-persistence.md | 1 + docs/getting-started.md | 2 + dotnet/src/Client.cs | 4 ++ dotnet/src/Types.cs | 14 ++++ dotnet/test/CloneTests.cs | 30 +++++++++ go/client.go | 2 + go/client_test.go | 54 ++++++++++++++++ go/types.go | 8 +++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 8 +++ nodejs/test/client.test.ts | 52 +++++++++++++++ python/copilot/client.py | 10 +++ python/copilot/types.py | 6 ++ python/test_client.py | 57 +++++++++++++++++ 15 files changed, 346 insertions(+) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index de642e194b..f9c1a37340 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -219,6 +219,102 @@ await using var session = await client.CreateSessionAsync(new SessionConfig > **Tip:** A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. +In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below. + +| Session Config Property | Type | Description | +|-------------------------|------|-------------| +| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. | + +## Selecting an Agent at Session Creation + +You can pass `agent` in the session config to pre-select which custom agent should be active when the session starts. The value must match the `name` of one of the agents defined in `customAgents`. + +This is equivalent to calling `session.rpc.agent.select()` after creation, but avoids the extra API call and ensures the agent is active from the very first prompt. + +
+Node.js / TypeScript + + +```typescript +const session = await client.createSession({ + customAgents: [ + { + name: "researcher", + prompt: "You are a research assistant. Analyze code and answer questions.", + }, + { + name: "editor", + prompt: "You are a code editor. Make minimal, surgical changes.", + }, + ], + agent: "researcher", // Pre-select the researcher agent +}); +``` + +
+ +
+Python + + +```python +session = await client.create_session({ + "custom_agents": [ + { + "name": "researcher", + "prompt": "You are a research assistant. Analyze code and answer questions.", + }, + { + "name": "editor", + "prompt": "You are a code editor. Make minimal, surgical changes.", + }, + ], + "agent": "researcher", # Pre-select the researcher agent +}) +``` + +
+ +
+Go + + +```go +session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "researcher", + Prompt: "You are a research assistant. Analyze code and answer questions.", + }, + { + Name: "editor", + Prompt: "You are a code editor. Make minimal, surgical changes.", + }, + }, + Agent: "researcher", // Pre-select the researcher agent +}) +``` + +
+ +
+.NET + + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + CustomAgents = new List + { + new() { Name = "researcher", Prompt = "You are a research assistant. Analyze code and answer questions." }, + new() { Name = "editor", Prompt = "You are a code editor. Make minimal, surgical changes." }, + }, + Agent = "researcher", // Pre-select the researcher agent +}); +``` + +
+ ## How Sub-Agent Delegation Works When you send a prompt to a session with custom agents, the runtime evaluates whether to delegate to a sub-agent: diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 7f3759df82..59a5d9d505 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -248,6 +248,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `configDir` | Override configuration directory | | `mcpServers` | Configure MCP servers | | `customAgents` | Configure custom agents | +| `agent` | Pre-select a custom agent by name | | `skillDirectories` | Directories to load skills from | | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | diff --git a/docs/getting-started.md b/docs/getting-started.md index de95a02761..fe952182c1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1241,6 +1241,8 @@ const session = await client.createSession({ }); ``` +> **Tip:** You can also set `agent: "pr-reviewer"` in the session config to pre-select this agent from the start. See the [Custom Agents guide](./guides/custom-agents.md#selecting-an-agent-at-session-creation) for details. + ### Customize the System Message Control the AI's behavior and personality: diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 8cad6b0489..91b6353ffe 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -419,6 +419,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.McpServers, "direct", config.CustomAgents, + config.Agent, config.ConfigDir, config.SkillDirectories, config.DisabledSkills, @@ -512,6 +513,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.McpServers, "direct", config.CustomAgents, + config.Agent, config.SkillDirectories, config.DisabledSkills, config.InfiniteSessions); @@ -1407,6 +1409,7 @@ internal record CreateSessionRequest( Dictionary? McpServers, string? EnvValueMode, List? CustomAgents, + string? Agent, string? ConfigDir, List? SkillDirectories, List? DisabledSkills, @@ -1450,6 +1453,7 @@ internal record ResumeSessionRequest( Dictionary? McpServers, string? EnvValueMode, List? CustomAgents, + string? Agent, List? SkillDirectories, List? DisabledSkills, InfiniteSessionConfig? InfiniteSessions); diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index dbee05cfd8..52d870b80d 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1197,6 +1197,7 @@ protected SessionConfig(SessionConfig? other) ClientName = other.ClientName; ConfigDir = other.ConfigDir; CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null; + Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; Hooks = other.Hooks; @@ -1307,6 +1308,12 @@ protected SessionConfig(SessionConfig? other) /// public List? CustomAgents { get; set; } + /// + /// Name of the custom agent to activate when the session starts. + /// Must match the of one of the agents in . + /// + public string? Agent { get; set; } + /// /// Directories to load skills from. /// @@ -1361,6 +1368,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) ClientName = other.ClientName; ConfigDir = other.ConfigDir; CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null; + Agent = other.Agent; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; DisableResume = other.DisableResume; ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null; @@ -1476,6 +1484,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public List? CustomAgents { get; set; } + /// + /// Name of the custom agent to activate when the session starts. + /// Must match the of one of the agents in . + /// + public string? Agent { get; set; } + /// /// Directories to load skills from. /// diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs index 8982c5d64f..cc6e5ad56d 100644 --- a/dotnet/test/CloneTests.cs +++ b/dotnet/test/CloneTests.cs @@ -88,6 +88,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Streaming = true, McpServers = new Dictionary { ["server1"] = new object() }, CustomAgents = [new CustomAgentConfig { Name = "agent1" }], + Agent = "agent1", SkillDirectories = ["/skills"], DisabledSkills = ["skill1"], }; @@ -105,6 +106,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.Streaming, clone.Streaming); Assert.Equal(original.McpServers.Count, clone.McpServers!.Count); Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); + Assert.Equal(original.Agent, clone.Agent); Assert.Equal(original.SkillDirectories, clone.SkillDirectories); Assert.Equal(original.DisabledSkills, clone.DisabledSkills); } @@ -242,4 +244,32 @@ public void Clone_WithNullCollections_ReturnsNullCollections() Assert.Null(clone.DisabledSkills); Assert.Null(clone.Tools); } + + [Fact] + public void SessionConfig_Clone_CopiesAgentProperty() + { + var original = new SessionConfig + { + Agent = "test-agent", + CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }], + }; + + var clone = original.Clone(); + + Assert.Equal("test-agent", clone.Agent); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesAgentProperty() + { + var original = new ResumeSessionConfig + { + Agent = "test-agent", + CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }], + }; + + var clone = original.Clone(); + + Assert.Equal("test-agent", clone.Agent); + } } diff --git a/go/client.go b/go/client.go index a43530adb5..3c1fb28cf7 100644 --- a/go/client.go +++ b/go/client.go @@ -502,6 +502,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.MCPServers = config.MCPServers req.EnvValueMode = "direct" req.CustomAgents = config.CustomAgents + req.Agent = config.Agent req.SkillDirectories = config.SkillDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions @@ -616,6 +617,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.MCPServers = config.MCPServers req.EnvValueMode = "direct" req.CustomAgents = config.CustomAgents + req.Agent = config.Agent req.SkillDirectories = config.SkillDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions diff --git a/go/client_test.go b/go/client_test.go index d740fd79b4..76efe98bab 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -413,6 +413,60 @@ func TestResumeSessionRequest_ClientName(t *testing.T) { }) } +func TestCreateSessionRequest_Agent(t *testing.T) { + t.Run("includes agent in JSON when set", func(t *testing.T) { + req := createSessionRequest{Agent: "test-agent"} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["agent"] != "test-agent" { + t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"]) + } + }) + + t.Run("omits agent from JSON when empty", func(t *testing.T) { + req := createSessionRequest{} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["agent"]; ok { + t.Error("Expected agent to be omitted when empty") + } + }) +} + +func TestResumeSessionRequest_Agent(t *testing.T) { + t.Run("includes agent in JSON when set", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1", Agent: "test-agent"} + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["agent"] != "test-agent" { + t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"]) + } + }) + + t.Run("omits agent from JSON when empty", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1"} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["agent"]; ok { + t.Error("Expected agent to be omitted when empty") + } + }) +} + func TestOverridesBuiltInTool(t *testing.T) { t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) { tool := Tool{ diff --git a/go/types.go b/go/types.go index d749de74a8..7970b2fe0a 100644 --- a/go/types.go +++ b/go/types.go @@ -384,6 +384,9 @@ type SessionConfig struct { MCPServers map[string]MCPServerConfig // CustomAgents configures custom agents for the session CustomAgents []CustomAgentConfig + // Agent is the name of the custom agent to activate when the session starts. + // Must match the Name of one of the agents in CustomAgents. + Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string // DisabledSkills is a list of skill names to disable @@ -467,6 +470,9 @@ type ResumeSessionConfig struct { MCPServers map[string]MCPServerConfig // CustomAgents configures custom agents for the session CustomAgents []CustomAgentConfig + // Agent is the name of the custom agent to activate when the session starts. + // Must match the Name of one of the agents in CustomAgents. + Agent string // SkillDirectories is a list of directories to load skills from SkillDirectories []string // DisabledSkills is a list of skill names to disable @@ -652,6 +658,7 @@ type createSessionRequest struct { MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + Agent string `json:"agent,omitempty"` ConfigDir string `json:"configDir,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` @@ -685,6 +692,7 @@ type resumeSessionRequest struct { MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` EnvValueMode string `json:"envValueMode,omitempty"` CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + Agent string `json:"agent,omitempty"` SkillDirectories []string `json:"skillDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index de5f1856eb..1108edaea3 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -567,6 +567,7 @@ export class CopilotClient { mcpServers: config.mcpServers, envValueMode: "direct", customAgents: config.customAgents, + agent: config.agent, configDir: config.configDir, skillDirectories: config.skillDirectories, disabledSkills: config.disabledSkills, @@ -654,6 +655,7 @@ export class CopilotClient { mcpServers: config.mcpServers, envValueMode: "direct", customAgents: config.customAgents, + agent: config.agent, skillDirectories: config.skillDirectories, disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 7eef940972..acda50fef0 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -725,6 +725,13 @@ export interface SessionConfig { */ customAgents?: CustomAgentConfig[]; + /** + * Name of the custom agent to activate when the session starts. + * Must match the `name` of one of the agents in `customAgents`. + * Equivalent to calling `session.rpc.agent.select({ name })` after creation. + */ + agent?: string; + /** * Directories to load skills from. */ @@ -764,6 +771,7 @@ export type ResumeSessionConfig = Pick< | "configDir" | "mcpServers" | "customAgents" + | "agent" | "skillDirectories" | "disabledSkills" | "infiniteSessions" diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index b7dd343956..22f969998f 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -336,4 +336,56 @@ describe("CopilotClient", () => { spy.mockRestore(); }); }); + + describe("agent parameter in session creation", () => { + it("forwards agent in session.create request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ + onPermissionRequest: approveAll, + customAgents: [ + { + name: "test-agent", + prompt: "You are a test agent.", + }, + ], + agent: "test-agent", + }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; + expect(payload.agent).toBe("test-agent"); + expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]); + }); + + it("forwards agent in session.resume request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + customAgents: [ + { + name: "test-agent", + prompt: "You are a test agent.", + }, + ], + agent: "test-agent", + }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any; + expect(payload.agent).toBe("test-agent"); + spy.mockRestore(); + }); + }); }); diff --git a/python/copilot/client.py b/python/copilot/client.py index 7ea4e97a1a..c29f35d124 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -569,6 +569,11 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents ] + # Add agent selection if provided + agent = cfg.get("agent") + if agent: + payload["agent"] = agent + # Add config directory override if provided config_dir = cfg.get("config_dir") if config_dir: @@ -758,6 +763,11 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents ] + # Add agent selection if provided + agent = cfg.get("agent") + if agent: + payload["agent"] = agent + # Add skill directories configuration if provided skill_directories = cfg.get("skill_directories") if skill_directories: diff --git a/python/copilot/types.py b/python/copilot/types.py index 6c484ce409..f094666ced 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -507,6 +507,9 @@ class SessionConfig(TypedDict, total=False): mcp_servers: dict[str, MCPServerConfig] # Custom agent configurations for the session custom_agents: list[CustomAgentConfig] + # Name of the custom agent to activate when the session starts. + # Must match the name of one of the agents in custom_agents. + agent: str # Override the default configuration directory location. # When specified, the session will use this directory for storing config and state. config_dir: str @@ -575,6 +578,9 @@ class ResumeSessionConfig(TypedDict, total=False): mcp_servers: dict[str, MCPServerConfig] # Custom agent configurations for the session custom_agents: list[CustomAgentConfig] + # Name of the custom agent to activate when the session starts. + # Must match the name of one of the agents in custom_agents. + agent: str # Directories to load skills from skill_directories: list[str] # List of skill names to disable diff --git a/python/test_client.py b/python/test_client.py index bcc249f302..ef068b7a15 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -265,6 +265,63 @@ async def mock_request(method, params): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_forwards_agent(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session( + { + "agent": "test-agent", + "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}], + "on_permission_request": PermissionHandler.approve_all, + } + ) + assert captured["session.create"]["agent"] == "test-agent" + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_forwards_agent(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + if method == "session.resume": + return {"sessionId": session.session_id} + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session( + session.session_id, + { + "agent": "test-agent", + "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}], + "on_permission_request": PermissionHandler.approve_all, + }, + ) + assert captured["session.resume"]["agent"] == "test-agent" + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): client = CopilotClient({"cli_path": CLI_PATH}) From 6195e3e48f35c9c23e6bdf1c2176ad78708cc94e Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sun, 8 Mar 2026 10:34:41 -0700 Subject: [PATCH 11/61] docs: update compatibility matrix with missing SDK and CLI features (#727) Add missing SDK features to the Available section: - Session-scoped RPC APIs (model, mode, plan, workspace) - Experimental APIs (fleet, agent management, compaction) - Message delivery modes (steering/queueing) - File and directory attachments - Ping and foreground session management - Session config options (configDir, clientName, disabledSkills) Update CLI-Only section with comprehensive slash commands: - /fleet, /research, /chronicle, /context, /copy, /diagnose - /instructions, /model, /skills, /tasks, /session, /rename - /resume, /reindex, /streamer-mode, /collect-debug-logs, etc. Add missing CLI flags: - Permission flags (--allow-tool, --deny-tool, --allow-url, etc.) - Non-interactive mode (-p, -i, -s, --continue, --agent) - Terminal options (--alt-screen, --mouse, --bash-env) Add workaround sections for plan management and message steering. Update version compatibility table with actual protocol version negotiation details (v2-v3 range, automatic v2 adapters). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/troubleshooting/compatibility.md | 134 +++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 16 deletions(-) diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index af304ac6ef..1a322b88c2 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -20,9 +20,15 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Delete session | `deleteSession()` | Remove from storage | | List sessions | `listSessions()` | All stored sessions | | Get last session | `getLastSessionId()` | For quick resume | +| Get foreground session | `getForegroundSessionId()` | Multi-session coordination | +| Set foreground session | `setForegroundSessionId()` | Multi-session coordination | | **Messaging** | | | | Send message | `send()` | With attachments | | Send and wait | `sendAndWait()` | Blocks until complete | +| Steering (immediate mode) | `send({ mode: "immediate" })` | Inject mid-turn without aborting | +| Queueing (enqueue mode) | `send({ mode: "enqueue" })` | Buffer for sequential processing (default) | +| File attachments | `send({ attachments: [{ type: "file", path }] })` | Images auto-encoded and resized | +| Directory attachments | `send({ attachments: [{ type: "directory", path }] })` | Attach directory context | | Get history | `getMessages()` | All session events | | Abort | `abort()` | Cancel in-flight request | | **Tools** | | | @@ -31,12 +37,28 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Tool result modification | `onPostToolUse` hook | Transform results | | Available/excluded tools | `availableTools`, `excludedTools` config | Filter tools | | **Models** | | | -| List models | `listModels()` | With capabilities | -| Set model | `model` in session config | Per-session | +| List models | `listModels()` | With capabilities, billing, policy | +| Set model (at creation) | `model` in session config | Per-session | +| Switch model (mid-session) | `session.setModel()` | Also via `session.rpc.model.switchTo()` | +| Get current model | `session.rpc.model.getCurrent()` | Query active model | | Reasoning effort | `reasoningEffort` config | For supported models | +| **Agent Mode** | | | +| Get current mode | `session.rpc.mode.get()` | Returns current mode | +| Set mode | `session.rpc.mode.set()` | Switch between modes | +| **Plan Management** | | | +| Read plan | `session.rpc.plan.read()` | Get plan.md content and path | +| Update plan | `session.rpc.plan.update()` | Write plan.md content | +| Delete plan | `session.rpc.plan.delete()` | Remove plan.md | +| **Workspace Files** | | | +| List workspace files | `session.rpc.workspace.listFiles()` | Files in session workspace | +| Read workspace file | `session.rpc.workspace.readFile()` | Read file content | +| Create workspace file | `session.rpc.workspace.createFile()` | Create file in workspace | | **Authentication** | | | | Get auth status | `getAuthStatus()` | Check login state | | Use token | `githubToken` option | Programmatic auth | +| **Connectivity** | | | +| Ping | `client.ping()` | Health check with server timestamp | +| Get server status | `client.getStatus()` | Protocol version and server info | | **MCP Servers** | | | | Local/stdio servers | `mcpServers` config | Spawn processes | | Remote HTTP/SSE | `mcpServers` config | Connect to services | @@ -44,19 +66,27 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Pre-tool use | `onPreToolUse` | Permission, modify args | | Post-tool use | `onPostToolUse` | Modify results | | User prompt | `onUserPromptSubmitted` | Modify prompts | -| Session start/end | `onSessionStart`, `onSessionEnd` | Lifecycle | +| Session start/end | `onSessionStart`, `onSessionEnd` | Lifecycle with source/reason | | Error handling | `onErrorOccurred` | Custom handling | | **Events** | | | | All session events | `on()`, `once()` | 40+ event types | | Streaming | `streaming: true` | Delta events | -| **Advanced** | | | -| Custom agents | `customAgents` config | Load agent definitions | +| **Session Config** | | | +| Custom agents | `customAgents` config | Define specialized agents | | System message | `systemMessage` config | Append or replace | | Custom provider | `provider` config | BYOK support | | Infinite sessions | `infiniteSessions` config | Auto-compaction | | Permission handler | `onPermissionRequest` | Approve/deny requests | | User input handler | `onUserInputRequest` | Handle ask_user | | Skills | `skillDirectories` config | Custom skills | +| Disabled skills | `disabledSkills` config | Disable specific skills | +| Config directory | `configDir` config | Override default config location | +| Client name | `clientName` config | Identify app in User-Agent | +| Working directory | `workingDirectory` config | Set session cwd | +| **Experimental** | | | +| Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent | +| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution | +| Manual compaction | `session.rpc.compaction.compact()` | Trigger compaction on demand | ### ❌ Not Available in SDK (CLI-Only) @@ -66,20 +96,32 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Export to file | `--share`, `/share` | Not in protocol | | Export to gist | `--share-gist`, `/share gist` | Not in protocol | | **Interactive UI** | | | -| Slash commands | `/help`, `/clear`, etc. | TUI-only | +| Slash commands | `/help`, `/clear`, `/exit`, etc. | TUI-only | | Agent picker dialog | `/agent` | Interactive UI | | Diff mode dialog | `/diff` | Interactive UI | | Feedback dialog | `/feedback` | Interactive UI | | Theme picker | `/theme` | Terminal UI | +| Model picker | `/model` | Interactive UI (use SDK `setModel()` instead) | +| Copy to clipboard | `/copy` | Terminal-specific | +| Context management | `/context` | Interactive UI | +| **Research & History** | | | +| Deep research | `/research` | TUI workflow with web search | +| Session history tools | `/chronicle` | Standup, tips, improve, reindex | | **Terminal Features** | | | | Color output | `--no-color` | Terminal-specific | | Screen reader mode | `--screen-reader` | Accessibility | | Rich diff rendering | `--plain-diff` | Terminal rendering | | Startup banner | `--banner` | Visual element | +| Streamer mode | `/streamer-mode` | TUI display mode | +| Alternate screen buffer | `--alt-screen`, `--no-alt-screen` | Terminal rendering | +| Mouse support | `--mouse`, `--no-mouse` | Terminal input | | **Path/Permission Shortcuts** | | | | Allow all paths | `--allow-all-paths` | Use permission handler | | Allow all URLs | `--allow-all-urls` | Use permission handler | -| YOLO mode | `--yolo` | Use permission handler | +| Allow all permissions | `--yolo`, `--allow-all`, `/allow-all` | Use permission handler | +| Granular tool permissions | `--allow-tool`, `--deny-tool` | Use `onPreToolUse` hook | +| URL access control | `--allow-url`, `--deny-url` | Use permission handler | +| Reset allowed tools | `/reset-allowed-tools` | TUI command | | **Directory Management** | | | | Add directory | `/add-dir`, `--add-dir` | Configure in session | | List directories | `/list-dirs` | TUI command | @@ -93,8 +135,13 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | User info | `/user` | TUI command | | **Session Operations** | | | | Clear conversation | `/clear` | TUI-only | -| Compact context | `/compact` | Use `infiniteSessions` config | -| Plan view | `/plan` | TUI-only | +| Plan view | `/plan` | TUI-only (use SDK `session.rpc.plan.*` instead) | +| Session management | `/session`, `/resume`, `/rename` | TUI workflow | +| Fleet mode (interactive) | `/fleet` | TUI-only (use SDK `session.rpc.fleet.start()` instead) | +| **Skills Management** | | | +| Manage skills | `/skills` | Interactive UI | +| **Task Management** | | | +| View background tasks | `/tasks` | TUI command | | **Usage & Stats** | | | | Token usage | `/usage` | Subscribe to usage events | | **Code Review** | | | @@ -103,8 +150,20 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Delegate to PR | `/delegate` | TUI workflow | | **Terminal Setup** | | | | Shell integration | `/terminal-setup` | Shell-specific | -| **Experimental** | | | -| Toggle experimental | `/experimental` | Runtime flag | +| **Development** | | | +| Toggle experimental | `/experimental`, `--experimental` | Runtime flag | +| Custom instructions control | `--no-custom-instructions` | CLI flag | +| Diagnose session | `/diagnose` | TUI command | +| View/manage instructions | `/instructions` | TUI command | +| Collect debug logs | `/collect-debug-logs` | Diagnostic tool | +| Reindex workspace | `/reindex` | TUI command | +| IDE integration | `/ide` | IDE-specific workflow | +| **Non-interactive Mode** | | | +| Prompt mode | `-p`, `--prompt` | Single-shot execution | +| Interactive prompt | `-i`, `--interactive` | Auto-execute then interactive | +| Silent output | `-s`, `--silent` | Script-friendly | +| Continue session | `--continue` | Resume most recent | +| Agent selection | `--agent ` | CLI flag | ## Workarounds @@ -150,9 +209,10 @@ session.on("assistant.usage", (event) => { ### Context Compaction -Instead of `/compact`, configure automatic compaction: +Instead of `/compact`, configure automatic compaction or trigger it manually: ```typescript +// Automatic compaction via config const session = await client.createSession({ infiniteSessions: { enabled: true, @@ -160,10 +220,44 @@ const session = await client.createSession({ bufferExhaustionThreshold: 0.95, // Block and compact at 95% context utilization }, }); + +// Manual compaction (experimental) +const result = await session.rpc.compaction.compact(); +console.log(`Removed ${result.tokensRemoved} tokens, ${result.messagesRemoved} messages`); ``` > **Note:** Thresholds are context utilization ratios (0.0-1.0), not absolute token counts. +### Plan Management + +Read and write session plans programmatically: + +```typescript +// Read the current plan +const plan = await session.rpc.plan.read(); +if (plan.exists) { + console.log(plan.content); +} + +// Update the plan +await session.rpc.plan.update({ content: "# My Plan\n- Step 1\n- Step 2" }); + +// Delete the plan +await session.rpc.plan.delete(); +``` + +### Message Steering + +Inject a message into the current LLM turn without aborting: + +```typescript +// Steer the agent mid-turn +await session.send({ prompt: "Focus on error handling first", mode: "immediate" }); + +// Default: enqueue for next turn +await session.send({ prompt: "Next, add tests" }); +``` + ## Protocol Limitations The SDK can only access features exposed through the CLI's JSON-RPC protocol. If you need a CLI feature that's not available: @@ -174,11 +268,19 @@ The SDK can only access features exposed through the CLI's JSON-RPC protocol. If ## Version Compatibility -| SDK Version | CLI Version | Protocol Version | -|-------------|-------------|------------------| -| Check `package.json` | `copilot --version` | `getStatus().protocolVersion` | +| SDK Protocol Range | CLI Protocol Version | Compatibility | +|--------------------|---------------------|---------------| +| v2–v3 | v3 | Full support | +| v2–v3 | v2 | Supported with automatic v2 adapters | + +The SDK negotiates protocol versions with the CLI at startup. The SDK supports protocol versions 2 through 3. When connecting to a v2 CLI server, the SDK automatically adapts `tool.call` and `permission.request` messages to the v3 event model — no code changes required. + +Check versions at runtime: -The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched. +```typescript +const status = await client.getStatus(); +console.log("Protocol version:", status.protocolVersion); +``` ## See Also From e478657b5eb1b95740f86293627bfde8a450eff9 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sun, 8 Mar 2026 14:59:15 -0700 Subject: [PATCH 12/61] feat: add onListModels handler to CopilotClientOptions for BYOK mode (#730) Add an optional onListModels handler to CopilotClientOptions across all 4 SDKs (Node, Python, Go, .NET). When provided, client.listModels() calls the handler instead of sending the models.list RPC to the CLI server. This enables BYOK users to return their provider's available models in the standard ModelInfo format. - Handler completely replaces CLI RPC when set (no fallback) - Results cached identically to CLI path (same locking/thread-safety) - No connection required when handler is provided - Supports both sync and async handlers - 10 new unit tests across all SDKs - Updated BYOK docs with usage examples in all 4 languages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/auth/byok.md | 110 ++++++++++++++++++++++++++++++++++++ dotnet/src/Client.cs | 29 +++++++--- dotnet/src/Types.cs | 9 +++ dotnet/test/ClientTests.cs | 100 +++++++++++++++++++++++++++++++++ go/client.go | 51 +++++++++++------ go/client_test.go | 60 ++++++++++++++++++++ go/types.go | 10 +++- nodejs/src/client.ts | 35 ++++++++---- nodejs/src/types.ts | 8 +++ nodejs/test/client.test.ts | 89 ++++++++++++++++++++++++++++- python/copilot/client.py | 34 ++++++++---- python/copilot/types.py | 5 ++ python/test_client.py | 111 +++++++++++++++++++++++++++++++++++++ 13 files changed, 600 insertions(+), 51 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 49d2452d97..df334508d4 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -306,6 +306,116 @@ provider: { > **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token. +## Custom Model Listing + +When using BYOK, the CLI server may not know which models your provider supports. You can supply a custom `onListModels` handler at the client level so that `client.listModels()` returns your provider's models in the standard `ModelInfo` format. This lets downstream consumers discover available models without querying the CLI. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; +import type { ModelInfo } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + onListModels: () => [ + { + id: "my-custom-model", + name: "My Custom Model", + capabilities: { + supports: { vision: false, reasoningEffort: false }, + limits: { max_context_window_tokens: 128000 }, + }, + }, + ], +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits + +client = CopilotClient({ + "on_list_models": lambda: [ + ModelInfo( + id="my-custom-model", + name="My Custom Model", + capabilities=ModelCapabilities( + supports=ModelSupports(vision=False, reasoning_effort=False), + limits=ModelLimits(max_context_window_tokens=128000), + ), + ) + ], +}) +``` + +
+ +
+Go + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + client := copilot.NewClient(&copilot.ClientOptions{ + OnListModels: func(ctx context.Context) ([]copilot.ModelInfo, error) { + return []copilot.ModelInfo{ + { + ID: "my-custom-model", + Name: "My Custom Model", + Capabilities: copilot.ModelCapabilities{ + Supports: copilot.ModelSupports{Vision: false, ReasoningEffort: false}, + Limits: copilot.ModelLimits{MaxContextWindowTokens: 128000}, + }, + }, + }, nil + }, + }) + _ = client +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot.SDK; + +var client = new CopilotClient(new CopilotClientOptions +{ + OnListModels = (ct) => Task.FromResult(new List + { + new() + { + Id = "my-custom-model", + Name = "My Custom Model", + Capabilities = new ModelCapabilities + { + Supports = new ModelSupports { Vision = false, ReasoningEffort = false }, + Limits = new ModelLimits { MaxContextWindowTokens = 128000 } + } + } + }) +}); +``` + +
+ +Results are cached after the first call, just like the default behavior. The handler completely replaces the CLI's `models.list` RPC — no fallback to the server occurs. + ## Limitations When using BYOK, be aware of these limitations: diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 91b6353ffe..1b4da2ffbe 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -70,6 +70,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable private int? _negotiatedProtocolVersion; private List? _modelsCache; private readonly SemaphoreSlim _modelsCacheLock = new(1, 1); + private readonly Func>>? _onListModels; private readonly List> _lifecycleHandlers = []; private readonly Dictionary>> _typedLifecycleHandlers = []; private readonly object _lifecycleHandlersLock = new(); @@ -136,6 +137,7 @@ public CopilotClient(CopilotClientOptions? options = null) } _logger = _options.Logger ?? NullLogger.Instance; + _onListModels = _options.OnListModels; // Parse CliUrl if provided if (!string.IsNullOrEmpty(_options.CliUrl)) @@ -624,9 +626,6 @@ public async Task GetAuthStatusAsync(CancellationToken ca /// Thrown when the client is not connected or not authenticated. public async Task> ListModelsAsync(CancellationToken cancellationToken = default) { - var connection = await EnsureConnectedAsync(cancellationToken); - - // Use semaphore for async locking to prevent race condition with concurrent calls await _modelsCacheLock.WaitAsync(cancellationToken); try { @@ -636,14 +635,26 @@ public async Task> ListModelsAsync(CancellationToken cancellatio return [.. _modelsCache]; // Return a copy to prevent cache mutation } - // Cache miss - fetch from backend while holding lock - var response = await InvokeRpcAsync( - connection.Rpc, "models.list", [], cancellationToken); + List models; + if (_onListModels is not null) + { + // Use custom handler instead of CLI RPC + models = await _onListModels(cancellationToken); + } + else + { + var connection = await EnsureConnectedAsync(cancellationToken); + + // Cache miss - fetch from backend while holding lock + var response = await InvokeRpcAsync( + connection.Rpc, "models.list", [], cancellationToken); + models = response.Models; + } - // Update cache before releasing lock - _modelsCache = response.Models; + // Update cache before releasing lock (copy to prevent external mutation) + _modelsCache = [.. models]; - return [.. response.Models]; // Return a copy to prevent cache mutation + return [.. models]; // Return a copy to prevent cache mutation } finally { diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 52d870b80d..a132e48181 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -63,6 +63,7 @@ protected CopilotClientOptions(CopilotClientOptions? other) Port = other.Port; UseLoggedInUser = other.UseLoggedInUser; UseStdio = other.UseStdio; + OnListModels = other.OnListModels; } /// @@ -136,6 +137,14 @@ public string? GithubToken /// public bool? UseLoggedInUser { get; set; } + /// + /// Custom handler for listing available models. + /// When provided, ListModelsAsync() calls this handler instead of + /// querying the CLI server. Useful in BYOK mode to return models + /// available from your custom provider. + /// + public Func>>? OnListModels { get; set; } + /// /// Creates a shallow clone of this instance. /// diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs index 3c3f3bdaae..6c70ffaa3a 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -274,4 +274,104 @@ public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandl Assert.Contains("OnPermissionRequest", ex.Message); Assert.Contains("is required", ex.Message); } + + [Fact] + public async Task ListModels_WithCustomHandler_CallsHandler() + { + var customModels = new List + { + new() + { + Id = "my-custom-model", + Name = "My Custom Model", + Capabilities = new ModelCapabilities + { + Supports = new ModelSupports { Vision = false, ReasoningEffort = false }, + Limits = new ModelLimits { MaxContextWindowTokens = 128000 } + } + } + }; + + var callCount = 0; + await using var client = new CopilotClient(new CopilotClientOptions + { + OnListModels = (ct) => + { + callCount++; + return Task.FromResult(customModels); + } + }); + await client.StartAsync(); + + var models = await client.ListModelsAsync(); + Assert.Equal(1, callCount); + Assert.Single(models); + Assert.Equal("my-custom-model", models[0].Id); + } + + [Fact] + public async Task ListModels_WithCustomHandler_CachesResults() + { + var customModels = new List + { + new() + { + Id = "cached-model", + Name = "Cached Model", + Capabilities = new ModelCapabilities + { + Supports = new ModelSupports { Vision = false, ReasoningEffort = false }, + Limits = new ModelLimits { MaxContextWindowTokens = 128000 } + } + } + }; + + var callCount = 0; + await using var client = new CopilotClient(new CopilotClientOptions + { + OnListModels = (ct) => + { + callCount++; + return Task.FromResult(customModels); + } + }); + await client.StartAsync(); + + await client.ListModelsAsync(); + await client.ListModelsAsync(); + Assert.Equal(1, callCount); // Only called once due to caching + } + + [Fact] + public async Task ListModels_WithCustomHandler_WorksWithoutStart() + { + var customModels = new List + { + new() + { + Id = "no-start-model", + Name = "No Start Model", + Capabilities = new ModelCapabilities + { + Supports = new ModelSupports { Vision = false, ReasoningEffort = false }, + Limits = new ModelLimits { MaxContextWindowTokens = 128000 } + } + } + }; + + var callCount = 0; + await using var client = new CopilotClient(new CopilotClientOptions + { + OnListModels = (ct) => + { + callCount++; + return Task.FromResult(customModels); + } + }); + + var models = await client.ListModelsAsync(); + Assert.Equal(1, callCount); + Assert.Single(models); + Assert.Equal("no-start-model", models[0].Id); + } } diff --git a/go/client.go b/go/client.go index 3c1fb28cf7..d440b49b40 100644 --- a/go/client.go +++ b/go/client.go @@ -92,6 +92,7 @@ type Client struct { processErrorPtr *error osProcess atomic.Pointer[os.Process] negotiatedProtocolVersion int + onListModels func(ctx context.Context) ([]ModelInfo, error) // RPC provides typed server-scoped RPC methods. // This field is nil until the client is connected via Start(). @@ -188,6 +189,9 @@ func NewClient(options *ClientOptions) *Client { if options.UseLoggedInUser != nil { opts.UseLoggedInUser = options.UseLoggedInUser } + if options.OnListModels != nil { + client.onListModels = options.OnListModels + } } // Default Env to current environment if not set @@ -1035,40 +1039,51 @@ func (c *Client) GetAuthStatus(ctx context.Context) (*GetAuthStatusResponse, err // Results are cached after the first successful call to avoid rate limiting. // The cache is cleared when the client disconnects. func (c *Client) ListModels(ctx context.Context) ([]ModelInfo, error) { - if c.client == nil { - return nil, fmt.Errorf("client not connected") - } - // Use mutex for locking to prevent race condition with concurrent calls c.modelsCacheMux.Lock() defer c.modelsCacheMux.Unlock() // Check cache (already inside lock) if c.modelsCache != nil { - // Return a copy to prevent cache mutation result := make([]ModelInfo, len(c.modelsCache)) copy(result, c.modelsCache) return result, nil } - // Cache miss - fetch from backend while holding lock - result, err := c.client.Request("models.list", listModelsRequest{}) - if err != nil { - return nil, err - } + var models []ModelInfo + if c.onListModels != nil { + // Use custom handler instead of CLI RPC + var err error + models, err = c.onListModels(ctx) + if err != nil { + return nil, err + } + } else { + if c.client == nil { + return nil, fmt.Errorf("client not connected") + } + // Cache miss - fetch from backend while holding lock + result, err := c.client.Request("models.list", listModelsRequest{}) + if err != nil { + return nil, err + } - var response listModelsResponse - if err := json.Unmarshal(result, &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal models response: %w", err) + var response listModelsResponse + if err := json.Unmarshal(result, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal models response: %w", err) + } + models = response.Models } - // Update cache before releasing lock - c.modelsCache = response.Models + // Update cache before releasing lock (copy to prevent external mutation) + cache := make([]ModelInfo, len(models)) + copy(cache, models) + c.modelsCache = cache // Return a copy to prevent cache mutation - models := make([]ModelInfo, len(response.Models)) - copy(models, response.Models) - return models, nil + result := make([]ModelInfo, len(models)) + copy(result, models) + return result, nil } // minProtocolVersion is the minimum protocol version this SDK can communicate with. diff --git a/go/client_test.go b/go/client_test.go index 76efe98bab..601215cbe0 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,6 +1,7 @@ package copilot import ( + "context" "encoding/json" "os" "path/filepath" @@ -548,6 +549,65 @@ func TestClient_ResumeSession_RequiresPermissionHandler(t *testing.T) { }) } +func TestListModelsWithCustomHandler(t *testing.T) { + customModels := []ModelInfo{ + { + ID: "my-custom-model", + Name: "My Custom Model", + Capabilities: ModelCapabilities{ + Supports: ModelSupports{Vision: false, ReasoningEffort: false}, + Limits: ModelLimits{MaxContextWindowTokens: 128000}, + }, + }, + } + + callCount := 0 + handler := func(ctx context.Context) ([]ModelInfo, error) { + callCount++ + return customModels, nil + } + + client := NewClient(&ClientOptions{OnListModels: handler}) + + models, err := client.ListModels(t.Context()) + if err != nil { + t.Fatalf("ListModels failed: %v", err) + } + if callCount != 1 { + t.Errorf("expected handler called once, got %d", callCount) + } + if len(models) != 1 || models[0].ID != "my-custom-model" { + t.Errorf("unexpected models: %+v", models) + } +} + +func TestListModelsHandlerCachesResults(t *testing.T) { + customModels := []ModelInfo{ + { + ID: "cached-model", + Name: "Cached Model", + Capabilities: ModelCapabilities{ + Supports: ModelSupports{Vision: false, ReasoningEffort: false}, + Limits: ModelLimits{MaxContextWindowTokens: 128000}, + }, + }, + } + + callCount := 0 + handler := func(ctx context.Context) ([]ModelInfo, error) { + callCount++ + return customModels, nil + } + + client := NewClient(&ClientOptions{OnListModels: handler}) + + _, _ = client.ListModels(t.Context()) + _, _ = client.ListModels(t.Context()) + if callCount != 1 { + t.Errorf("expected handler called once due to caching, got %d", callCount) + } +} + func TestClient_StartStopRace(t *testing.T) { cliPath := findCLIPathForTest() if cliPath == "" { diff --git a/go/types.go b/go/types.go index 7970b2fe0a..eaee2fb111 100644 --- a/go/types.go +++ b/go/types.go @@ -1,6 +1,9 @@ package copilot -import "encoding/json" +import ( + "context" + "encoding/json" +) // ConnectionState represents the client connection state type ConnectionState string @@ -54,6 +57,11 @@ type ClientOptions struct { // Default: true (but defaults to false when GitHubToken is provided). // Use Bool(false) to explicitly disable. UseLoggedInUser *bool + // OnListModels is a custom handler for listing available models. + // When provided, client.ListModels() calls this handler instead of + // querying the CLI server. Useful in BYOK mode to return models + // available from your custom provider. + OnListModels func(ctx context.Context) ([]ModelInfo, error) } // Bool returns a pointer to the given bool value. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 1108edaea3..8cc79bf56e 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -141,7 +141,7 @@ export class CopilotClient { private sessions: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages private options: Required< - Omit + Omit > & { cliUrl?: string; githubToken?: string; @@ -149,6 +149,7 @@ export class CopilotClient { }; private isExternalServer: boolean = false; private forceStopping: boolean = false; + private onListModels?: () => Promise | ModelInfo[]; private modelsCache: ModelInfo[] | null = null; private modelsCacheLock: Promise = Promise.resolve(); private sessionLifecycleHandlers: Set = new Set(); @@ -226,6 +227,8 @@ export class CopilotClient { this.isExternalServer = true; } + this.onListModels = options.onListModels; + this.options = { cliPath: options.cliPath || getBundledCliPath(), cliArgs: options.cliArgs ?? [], @@ -751,16 +754,15 @@ export class CopilotClient { /** * List available models with their metadata. * + * If an `onListModels` handler was provided in the client options, + * it is called instead of querying the CLI server. + * * Results are cached after the first successful call to avoid rate limiting. * The cache is cleared when the client disconnects. * - * @throws Error if not authenticated + * @throws Error if not connected (when no custom handler is set) */ async listModels(): Promise { - if (!this.connection) { - throw new Error("Client not connected"); - } - // Use promise-based locking to prevent race condition with concurrent calls await this.modelsCacheLock; @@ -775,13 +777,22 @@ export class CopilotClient { return [...this.modelsCache]; // Return a copy to prevent cache mutation } - // Cache miss - fetch from backend while holding lock - const result = await this.connection.sendRequest("models.list", {}); - const response = result as { models: ModelInfo[] }; - const models = response.models; + let models: ModelInfo[]; + if (this.onListModels) { + // Use custom handler instead of CLI RPC + models = await this.onListModels(); + } else { + if (!this.connection) { + throw new Error("Client not connected"); + } + // Cache miss - fetch from backend while holding lock + const result = await this.connection.sendRequest("models.list", {}); + const response = result as { models: ModelInfo[] }; + models = response.models; + } - // Update cache before releasing lock - this.modelsCache = models; + // Update cache before releasing lock (copy to prevent external mutation) + this.modelsCache = [...models]; return [...models]; // Return a copy to prevent cache mutation } finally { diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index acda50fef0..69c29396a1 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -96,6 +96,14 @@ export interface CopilotClientOptions { * @default true (but defaults to false when githubToken is provided) */ useLoggedInUser?: boolean; + + /** + * Custom handler for listing available models. + * When provided, client.listModels() calls this handler instead of + * querying the CLI server. Useful in BYOK mode to return models + * available from your custom provider. + */ + onListModels?: () => Promise | ModelInfo[]; } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 22f969998f..ef227b6988 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, expect, it, onTestFinished, vi } from "vitest"; -import { approveAll, CopilotClient } from "../src/index.js"; +import { approveAll, CopilotClient, type ModelInfo } from "../src/index.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead @@ -388,4 +388,91 @@ describe("CopilotClient", () => { spy.mockRestore(); }); }); + + describe("onListModels", () => { + it("calls onListModels handler instead of RPC when provided", async () => { + const customModels: ModelInfo[] = [ + { + id: "my-custom-model", + name: "My Custom Model", + capabilities: { + supports: { vision: false, reasoningEffort: false }, + limits: { max_context_window_tokens: 128000 }, + }, + }, + ]; + + const handler = vi.fn().mockReturnValue(customModels); + const client = new CopilotClient({ onListModels: handler }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const models = await client.listModels(); + expect(handler).toHaveBeenCalledTimes(1); + expect(models).toEqual(customModels); + }); + + it("caches onListModels results on subsequent calls", async () => { + const customModels: ModelInfo[] = [ + { + id: "cached-model", + name: "Cached Model", + capabilities: { + supports: { vision: false, reasoningEffort: false }, + limits: { max_context_window_tokens: 128000 }, + }, + }, + ]; + + const handler = vi.fn().mockReturnValue(customModels); + const client = new CopilotClient({ onListModels: handler }); + await client.start(); + onTestFinished(() => client.forceStop()); + + await client.listModels(); + await client.listModels(); + expect(handler).toHaveBeenCalledTimes(1); // Only called once due to caching + }); + + it("supports async onListModels handler", async () => { + const customModels: ModelInfo[] = [ + { + id: "async-model", + name: "Async Model", + capabilities: { + supports: { vision: false, reasoningEffort: false }, + limits: { max_context_window_tokens: 128000 }, + }, + }, + ]; + + const handler = vi.fn().mockResolvedValue(customModels); + const client = new CopilotClient({ onListModels: handler }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const models = await client.listModels(); + expect(models).toEqual(customModels); + }); + + it("does not require client.start when onListModels is provided", async () => { + const customModels: ModelInfo[] = [ + { + id: "no-start-model", + name: "No Start Model", + capabilities: { + supports: { vision: false, reasoningEffort: false }, + limits: { max_context_window_tokens: 128000 }, + }, + }, + ]; + + const handler = vi.fn().mockReturnValue(customModels); + const client = new CopilotClient({ onListModels: handler }); + + const models = await client.listModels(); + expect(handler).toHaveBeenCalledTimes(1); + expect(models).toEqual(customModels); + }); + }); }); diff --git a/python/copilot/client.py b/python/copilot/client.py index c29f35d124..ff587d9974 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -200,6 +200,8 @@ def __init__(self, options: CopilotClientOptions | None = None): if github_token: self.options["github_token"] = github_token + self._on_list_models = opts.get("on_list_models") + self._process: subprocess.Popen | None = None self._client: JsonRpcClient | None = None self._state: ConnectionState = "disconnected" @@ -897,11 +899,15 @@ async def list_models(self) -> list["ModelInfo"]: Results are cached after the first successful call to avoid rate limiting. The cache is cleared when the client disconnects. + If a custom ``on_list_models`` handler was provided in the client options, + it is called instead of querying the CLI server. The handler may be sync + or async. + Returns: A list of ModelInfo objects with model details. Raises: - RuntimeError: If the client is not connected. + RuntimeError: If the client is not connected (when no custom handler is set). Exception: If not authenticated. Example: @@ -909,22 +915,30 @@ async def list_models(self) -> list["ModelInfo"]: >>> for model in models: ... print(f"{model.id}: {model.name}") """ - if not self._client: - raise RuntimeError("Client not connected") - # Use asyncio lock to prevent race condition with concurrent calls async with self._models_cache_lock: # Check cache (already inside lock) if self._models_cache is not None: return list(self._models_cache) # Return a copy to prevent cache mutation - # Cache miss - fetch from backend while holding lock - response = await self._client.request("models.list", {}) - models_data = response.get("models", []) - models = [ModelInfo.from_dict(model) for model in models_data] + if self._on_list_models: + # Use custom handler instead of CLI RPC + result = self._on_list_models() + if inspect.isawaitable(result): + models = await result + else: + models = result + else: + if not self._client: + raise RuntimeError("Client not connected") + + # Cache miss - fetch from backend while holding lock + response = await self._client.request("models.list", {}) + models_data = response.get("models", []) + models = [ModelInfo.from_dict(model) for model in models_data] - # Update cache before releasing lock - self._models_cache = models + # Update cache before releasing lock (copy to prevent external mutation) + self._models_cache = list(models) return list(models) # Return a copy to prevent cache mutation diff --git a/python/copilot/types.py b/python/copilot/types.py index f094666ced..5f4b7e20d7 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -98,6 +98,11 @@ class CopilotClientOptions(TypedDict, total=False): # When False, only explicit tokens (github_token or environment variables) are used. # Default: True (but defaults to False when github_token is provided) use_logged_in_user: bool + # Custom handler for listing available models. + # When provided, client.list_models() calls this handler instead of + # querying the CLI server. Useful in BYOK mode to return models + # available from your custom provider. + on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] ToolResultType = Literal["success", "failure", "rejected", "denied"] diff --git a/python/test_client.py b/python/test_client.py index ef068b7a15..4a06966d4e 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -7,6 +7,7 @@ import pytest from copilot import CopilotClient, PermissionHandler, define_tool +from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports from e2e.testharness import CLI_PATH @@ -214,6 +215,116 @@ def grep(params) -> str: await client.force_stop() +class TestOnListModels: + @pytest.mark.asyncio + async def test_list_models_with_custom_handler(self): + """Test that on_list_models handler is called instead of RPC""" + custom_models = [ + ModelInfo( + id="my-custom-model", + name="My Custom Model", + capabilities=ModelCapabilities( + supports=ModelSupports(vision=False, reasoning_effort=False), + limits=ModelLimits(max_context_window_tokens=128000), + ), + ) + ] + + handler_calls = [] + + def handler(): + handler_calls.append(1) + return custom_models + + client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + await client.start() + try: + models = await client.list_models() + assert len(handler_calls) == 1 + assert models == custom_models + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_list_models_handler_caches_results(self): + """Test that on_list_models results are cached""" + custom_models = [ + ModelInfo( + id="cached-model", + name="Cached Model", + capabilities=ModelCapabilities( + supports=ModelSupports(vision=False, reasoning_effort=False), + limits=ModelLimits(max_context_window_tokens=128000), + ), + ) + ] + + handler_calls = [] + + def handler(): + handler_calls.append(1) + return custom_models + + client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + await client.start() + try: + await client.list_models() + await client.list_models() + assert len(handler_calls) == 1 # Only called once due to caching + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_list_models_async_handler(self): + """Test that async on_list_models handler works""" + custom_models = [ + ModelInfo( + id="async-model", + name="Async Model", + capabilities=ModelCapabilities( + supports=ModelSupports(vision=False, reasoning_effort=False), + limits=ModelLimits(max_context_window_tokens=128000), + ), + ) + ] + + async def handler(): + return custom_models + + client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + await client.start() + try: + models = await client.list_models() + assert models == custom_models + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_list_models_handler_without_start(self): + """Test that on_list_models works without starting the CLI connection""" + custom_models = [ + ModelInfo( + id="no-start-model", + name="No Start Model", + capabilities=ModelCapabilities( + supports=ModelSupports(vision=False, reasoning_effort=False), + limits=ModelLimits(max_context_window_tokens=128000), + ), + ) + ] + + handler_calls = [] + + def handler(): + handler_calls.append(1) + return custom_models + + client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + models = await client.list_models() + assert len(handler_calls) == 1 + assert models == custom_models + + class TestSessionConfigForwarding: @pytest.mark.asyncio async def test_create_session_forwards_client_name(self): From 5c38b90d2eecf596ee96e621ddfa2d39c673a46f Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 9 Mar 2026 09:43:50 +0000 Subject: [PATCH 13/61] In C# codegen, represent optional RPC params as optional C# method params, not just nullable values (#733) --- dotnet/src/Generated/Rpc.cs | 4 ++-- scripts/codegen/csharp.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 85e55e4b80..9cee420976 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -775,7 +775,7 @@ internal FleetApi(JsonRpc rpc, string sessionId) } /// Calls "session.fleet.start". - public async Task StartAsync(string? prompt, CancellationToken cancellationToken = default) + public async Task StartAsync(string? prompt = null, CancellationToken cancellationToken = default) { var request = new SessionFleetStartRequest { SessionId = _sessionId, Prompt = prompt }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.fleet.start", [request], cancellationToken); @@ -853,7 +853,7 @@ internal ToolsApi(JsonRpc rpc, string sessionId) } /// Calls "session.tools.handlePendingToolCall". - public async Task HandlePendingToolCallAsync(string requestId, object? result, string? error, CancellationToken cancellationToken = default) + public async Task HandlePendingToolCallAsync(string requestId, object? result = null, string? error = null, CancellationToken cancellationToken = default) { var request = new SessionToolsHandlePendingToolCallRequest { SessionId = _sessionId, RequestId = requestId, Result = result, Error = error }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.tools.handlePendingToolCall", [request], cancellationToken); diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 463d856c89..0956e11b2f 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -699,6 +699,13 @@ function emitSessionApiClass(className: string, node: Record, c const paramEntries = (method.params?.properties ? Object.entries(method.params.properties) : []).filter(([k]) => k !== "sessionId"); const requiredSet = new Set(method.params?.required || []); + // Sort so required params come before optional (C# requires defaults at end) + paramEntries.sort((a, b) => { + const aReq = requiredSet.has(a[0]) ? 0 : 1; + const bReq = requiredSet.has(b[0]) ? 0 : 1; + return aReq - bReq; + }); + const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; if (method.params) { const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); @@ -711,8 +718,9 @@ function emitSessionApiClass(className: string, node: Record, c for (const [pName, pSchema] of paramEntries) { if (typeof pSchema !== "object") continue; - const csType = resolveRpcType(pSchema as JSONSchema7, requiredSet.has(pName), requestClassName, toPascalCase(pName), classes); - sigParams.push(`${csType} ${pName}`); + const isReq = requiredSet.has(pName); + const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); + sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); } sigParams.push("CancellationToken cancellationToken = default"); From ad51d74bf91d9a12b412e24949644202aa566583 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 9 Mar 2026 12:38:17 +0000 Subject: [PATCH 14/61] Fix codegen for discriminated unions nested within other types (#736) * Fix C# codegen for discriminated unions nested within other types. Result is strongly-typed APIs for permission requests * Fix permissions scenario to use strongly-typed PermissionRequest variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/SessionEvents.cs | 199 +++++++++++++++++- dotnet/src/Session.cs | 18 +- dotnet/src/Types.cs | 33 --- dotnet/test/PermissionTests.cs | 2 +- dotnet/test/ToolsTests.cs | 6 +- scripts/codegen/csharp.ts | 48 ++++- .../callbacks/permissions/csharp/Program.cs | 12 +- 7 files changed, 253 insertions(+), 65 deletions(-) diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index c497038c6a..f87ab32d4c 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1528,7 +1528,7 @@ public partial class PermissionRequestedData public required string RequestId { get; set; } [JsonPropertyName("permissionRequest")] - public required object PermissionRequest { get; set; } + public required PermissionRequest PermissionRequest { get; set; } } public partial class PermissionCompletedData @@ -2095,6 +2095,193 @@ public partial class SystemMessageDataMetadata public Dictionary? Variables { get; set; } } +public partial class PermissionRequestShellCommandsItem +{ + [JsonPropertyName("identifier")] + public required string Identifier { get; set; } + + [JsonPropertyName("readOnly")] + public required bool ReadOnly { get; set; } +} + +public partial class PermissionRequestShellPossibleUrlsItem +{ + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +public partial class PermissionRequestShell : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "shell"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("fullCommandText")] + public required string FullCommandText { get; set; } + + [JsonPropertyName("intention")] + public required string Intention { get; set; } + + [JsonPropertyName("commands")] + public required PermissionRequestShellCommandsItem[] Commands { get; set; } + + [JsonPropertyName("possiblePaths")] + public required string[] PossiblePaths { get; set; } + + [JsonPropertyName("possibleUrls")] + public required PermissionRequestShellPossibleUrlsItem[] PossibleUrls { get; set; } + + [JsonPropertyName("hasWriteFileRedirection")] + public required bool HasWriteFileRedirection { get; set; } + + [JsonPropertyName("canOfferSessionApproval")] + public required bool CanOfferSessionApproval { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("warning")] + public string? Warning { get; set; } +} + +public partial class PermissionRequestWrite : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "write"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("intention")] + public required string Intention { get; set; } + + [JsonPropertyName("fileName")] + public required string FileName { get; set; } + + [JsonPropertyName("diff")] + public required string Diff { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("newFileContents")] + public string? NewFileContents { get; set; } +} + +public partial class PermissionRequestRead : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "read"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("intention")] + public required string Intention { get; set; } + + [JsonPropertyName("path")] + public required string Path { get; set; } +} + +public partial class PermissionRequestMcp : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "mcp"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } + + [JsonPropertyName("toolTitle")] + public required string ToolTitle { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("args")] + public object? Args { get; set; } + + [JsonPropertyName("readOnly")] + public required bool ReadOnly { get; set; } +} + +public partial class PermissionRequestUrl : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "url"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("intention")] + public required string Intention { get; set; } + + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +public partial class PermissionRequestMemory : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "memory"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("subject")] + public required string Subject { get; set; } + + [JsonPropertyName("fact")] + public required string Fact { get; set; } + + [JsonPropertyName("citations")] + public required string Citations { get; set; } +} + +public partial class PermissionRequestCustomTool : PermissionRequest +{ + [JsonIgnore] + public override string Kind => "custom-tool"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } + + [JsonPropertyName("toolDescription")] + public required string ToolDescription { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("args")] + public object? Args { get; set; } +} + +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionRequestShell), "shell")] +[JsonDerivedType(typeof(PermissionRequestWrite), "write")] +[JsonDerivedType(typeof(PermissionRequestRead), "read")] +[JsonDerivedType(typeof(PermissionRequestMcp), "mcp")] +[JsonDerivedType(typeof(PermissionRequestUrl), "url")] +[JsonDerivedType(typeof(PermissionRequestMemory), "memory")] +[JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")] +public partial class PermissionRequest +{ + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + public partial class PermissionCompletedDataResult { [JsonPropertyName("kind")] @@ -2273,6 +2460,16 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(PermissionCompletedData))] [JsonSerializable(typeof(PermissionCompletedDataResult))] [JsonSerializable(typeof(PermissionCompletedEvent))] +[JsonSerializable(typeof(PermissionRequest))] +[JsonSerializable(typeof(PermissionRequestCustomTool))] +[JsonSerializable(typeof(PermissionRequestMcp))] +[JsonSerializable(typeof(PermissionRequestMemory))] +[JsonSerializable(typeof(PermissionRequestRead))] +[JsonSerializable(typeof(PermissionRequestShell))] +[JsonSerializable(typeof(PermissionRequestShellCommandsItem))] +[JsonSerializable(typeof(PermissionRequestShellPossibleUrlsItem))] +[JsonSerializable(typeof(PermissionRequestUrl))] +[JsonSerializable(typeof(PermissionRequestWrite))] [JsonSerializable(typeof(PermissionRequestedData))] [JsonSerializable(typeof(PermissionRequestedEvent))] [JsonSerializable(typeof(SessionCompactionCompleteData))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 397eae0fa2..282fc50d5f 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -339,7 +339,7 @@ internal async Task HandlePermissionRequestAsync(JsonEl }; } - var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionJsonContext.Default.PermissionRequest) + var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionEventsJsonContext.Default.PermissionRequest) ?? throw new InvalidOperationException("Failed to deserialize permission request"); var invocation = new PermissionInvocation @@ -457,27 +457,16 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, /// /// Executes a permission handler and sends the result back via the HandlePendingPermissionRequest RPC. /// - private async Task ExecutePermissionAndRespondAsync(string requestId, object permissionRequestData, PermissionRequestHandler handler) + private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, PermissionRequestHandler handler) { try { - // PermissionRequestedData.PermissionRequest is typed as `object` in generated code, - // but StreamJsonRpc deserializes it as a JsonElement. - if (permissionRequestData is not JsonElement permJsonElement) - { - throw new InvalidOperationException( - $"Permission request data must be a {nameof(JsonElement)}; received {permissionRequestData.GetType().Name}"); - } - - var request = JsonSerializer.Deserialize(permJsonElement.GetRawText(), SessionJsonContext.Default.PermissionRequest) - ?? throw new InvalidOperationException("Failed to deserialize permission request"); - var invocation = new PermissionInvocation { SessionId = SessionId }; - var result = await handler(request, invocation); + var result = await handler(permissionRequest, invocation); await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result); } catch (Exception) @@ -780,7 +769,6 @@ internal record SessionDestroyRequest DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(GetMessagesRequest))] [JsonSerializable(typeof(GetMessagesResponse))] - [JsonSerializable(typeof(PermissionRequest))] [JsonSerializable(typeof(SendMessageRequest))] [JsonSerializable(typeof(SendMessageResponse))] [JsonSerializable(typeof(SessionAbortRequest))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index a132e48181..4d268434ea 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -266,38 +266,6 @@ public class ToolInvocation /// public delegate Task ToolHandler(ToolInvocation invocation); -/// -/// Represents a permission request from the server for a tool operation. -/// -public class PermissionRequest -{ - /// - /// Kind of permission being requested. - /// - /// "shell" — execute a shell command. - /// "write" — write to a file. - /// "read" — read a file. - /// "mcp" — invoke an MCP server tool. - /// "url" — access a URL. - /// "custom-tool" — invoke a custom tool. - /// - /// - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; - - /// - /// Identifier of the tool call that triggered the permission request. - /// - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } - - /// - /// Additional properties not explicitly modeled. - /// - [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } -} - /// Describes the kind of a permission request result. [JsonConverter(typeof(PermissionRequestResultKind.Converter))] [DebuggerDisplay("{Value,nq}")] @@ -2005,7 +1973,6 @@ public class SetForegroundSessionResponse [JsonSerializable(typeof(ModelPolicy))] [JsonSerializable(typeof(ModelSupports))] [JsonSerializable(typeof(ModelVisionLimits))] -[JsonSerializable(typeof(PermissionRequest))] [JsonSerializable(typeof(PermissionRequestResult))] [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResponse))] diff --git a/dotnet/test/PermissionTests.cs b/dotnet/test/PermissionTests.cs index 59a3cb4dd5..3ab36dad19 100644 --- a/dotnet/test/PermissionTests.cs +++ b/dotnet/test/PermissionTests.cs @@ -231,7 +231,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests() { OnPermissionRequest = (request, invocation) => { - if (!string.IsNullOrEmpty(request.ToolCallId)) + if (request is PermissionRequestShell shell && !string.IsNullOrEmpty(shell.ToolCallId)) { receivedToolCallId = true; } diff --git a/dotnet/test/ToolsTests.cs b/dotnet/test/ToolsTests.cs index b31ef1f93d..0956598895 100644 --- a/dotnet/test/ToolsTests.cs +++ b/dotnet/test/ToolsTests.cs @@ -237,11 +237,9 @@ await session.SendAsync(new MessageOptions Assert.Contains("HELLO", assistantMessage!.Data.Content ?? string.Empty); // Should have received a custom-tool permission request with the correct tool name - var customToolRequest = permissionRequests.FirstOrDefault(r => r.Kind == "custom-tool"); + var customToolRequest = permissionRequests.OfType().FirstOrDefault(); Assert.NotNull(customToolRequest); - Assert.True(customToolRequest!.ExtensionData?.ContainsKey("toolName") ?? false); - var toolName = ((JsonElement)customToolRequest.ExtensionData!["toolName"]).GetString(); - Assert.Equal("encrypt_string", toolName); + Assert.Equal("encrypt_string", customToolRequest!.ToolName); [Description("Encrypts a string")] static string EncryptStringForPermission([Description("String to encrypt")] string input) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 0956e11b2f..af5fb78a67 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -24,11 +24,29 @@ import { const execFileAsync = promisify(execFile); +// ── C# type rename overrides ──────────────────────────────────────────────── +// Map generated class names to shorter public-facing names. +// Applied to base classes AND their derived variants (e.g., FooBar → Bar, FooBazShell → BarShell). +const TYPE_RENAMES: Record = { + PermissionRequestedDataPermissionRequest: "PermissionRequest", +}; + +/** Apply rename to a generated class name, checking both exact match and prefix replacement for derived types. */ +function applyTypeRename(className: string): string { + if (TYPE_RENAMES[className]) return TYPE_RENAMES[className]; + for (const [from, to] of Object.entries(TYPE_RENAMES)) { + if (className.startsWith(from)) { + return to + className.slice(from.length); + } + } + return className; +} + // ── C# utilities ──────────────────────────────────────────────────────────── function toPascalCase(name: string): string { - if (name.includes("_")) { - return name.split("_").map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); + if (name.includes("_") || name.includes("-")) { + return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); } return name.charAt(0).toUpperCase() + name.slice(1); } @@ -208,17 +226,18 @@ function generatePolymorphicClasses( ): string { const lines: string[] = []; const discriminatorInfo = findDiscriminator(variants)!; + const renamedBase = applyTypeRename(baseClassName); lines.push(`[JsonPolymorphic(`); lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`); lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`); for (const [constValue] of discriminatorInfo.mapping) { - const derivedClassName = `${baseClassName}${toPascalCase(constValue)}`; + const derivedClassName = applyTypeRename(`${baseClassName}${toPascalCase(constValue)}`); lines.push(`[JsonDerivedType(typeof(${derivedClassName}), "${constValue}")]`); } - lines.push(`public partial class ${baseClassName}`); + lines.push(`public partial class ${renamedBase}`); lines.push(`{`); lines.push(` [JsonPropertyName("${discriminatorProperty}")]`); lines.push(` public virtual string ${toPascalCase(discriminatorProperty)} { get; set; } = string.Empty;`); @@ -226,8 +245,8 @@ function generatePolymorphicClasses( lines.push(""); for (const [constValue, variant] of discriminatorInfo.mapping) { - const derivedClassName = `${baseClassName}${toPascalCase(constValue)}`; - const derivedCode = generateDerivedClass(derivedClassName, baseClassName, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput); + const derivedClassName = applyTypeRename(`${baseClassName}${toPascalCase(constValue)}`); + const derivedCode = generateDerivedClass(derivedClassName, renamedBase, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput); nestedClasses.set(derivedClassName, derivedCode); } @@ -319,6 +338,18 @@ function resolveSessionPropertyType( if (nonNull.length === 1) { return resolveSessionPropertyType(nonNull[0] as JSONSchema7, parentClassName, propName, isRequired && !hasNull, knownTypes, nestedClasses, enumOutput); } + // Discriminated union: anyOf with multiple object variants sharing a const discriminator + if (nonNull.length > 1) { + const variants = nonNull as JSONSchema7[]; + const discriminatorInfo = findDiscriminator(variants); + if (discriminatorInfo) { + const baseClassName = `${parentClassName}${propName}`; + const renamedBase = applyTypeRename(baseClassName); + const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput); + nestedClasses.set(renamedBase, polymorphicCode); + return isRequired && !hasNull ? renamedBase : `${renamedBase}?`; + } + } return hasNull || !isRequired ? "object?" : "object"; } if (propSchema.enum && Array.isArray(propSchema.enum)) { @@ -338,9 +369,10 @@ function resolveSessionPropertyType( const discriminatorInfo = findDiscriminator(variants); if (discriminatorInfo) { const baseClassName = `${parentClassName}${propName}Item`; + const renamedBase = applyTypeRename(baseClassName); const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput); - nestedClasses.set(baseClassName, polymorphicCode); - return isRequired ? `${baseClassName}[]` : `${baseClassName}[]?`; + nestedClasses.set(renamedBase, polymorphicCode); + return isRequired ? `${renamedBase}[]` : `${renamedBase}[]?`; } } if (items.type === "object" && items.properties) { diff --git a/test/scenarios/callbacks/permissions/csharp/Program.cs b/test/scenarios/callbacks/permissions/csharp/Program.cs index 0000ed575d..889eeaff1a 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -17,9 +17,15 @@ Model = "claude-haiku-4.5", OnPermissionRequest = (request, invocation) => { - var toolName = request.ExtensionData?.TryGetValue("toolName", out var value) == true - ? value?.ToString() ?? "unknown" - : "unknown"; + var toolName = request switch + { + PermissionRequestCustomTool ct => ct.ToolName, + PermissionRequestShell sh => "shell", + PermissionRequestWrite wr => wr.FileName ?? "write", + PermissionRequestRead rd => rd.Path ?? "read", + PermissionRequestMcp mcp => mcp.ToolName ?? "mcp", + _ => request.Kind, + }; permissionLog.Add($"approved:{toolName}"); return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); }, From 11dde6e4de841bc8437b24f49e37591858dea914 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 9 Mar 2026 16:08:09 +0000 Subject: [PATCH 15/61] Update to match runtime changes (#737) * Run codegen to produce session.log and other updates from runtime * E2E test for TypeScript session.log * Session log wrappers and E2E tests for C#, Go, Python * Docs for extension author guide * Avoid incorrect use of console.log as that would break on the stdio transport * Improve extension API via joinSession * Use latest codegenerator after rebase * Fix how we reference vscode-jsonrpc * Update dependency * Formatting --- dotnet/src/Generated/Rpc.cs | 64 +- dotnet/src/Generated/SessionEvents.cs | 111 ++++ dotnet/src/Session.cs | 24 +- dotnet/test/SessionTests.cs | 48 ++ go/generated_session_events.go | 45 ++ go/internal/e2e/session_test.go | 104 ++++ go/rpc/generated_rpc.go | 62 +- go/session.go | 46 ++ nodejs/docs/agent-author.md | 265 ++++++++ nodejs/docs/examples.md | 681 +++++++++++++++++++++ nodejs/docs/extensions.md | 61 ++ nodejs/package-lock.json | 56 +- nodejs/package.json | 3 +- nodejs/src/client.ts | 41 +- nodejs/src/extension.ts | 34 +- nodejs/src/generated/rpc.ts | 29 + nodejs/src/generated/session-events.ts | 80 +++ nodejs/src/session.ts | 27 +- nodejs/test/e2e/session.test.ts | 53 +- python/copilot/generated/rpc.py | 92 ++- python/copilot/generated/session_events.py | 92 ++- python/copilot/session.py | 36 ++ python/e2e/test_session.py | 43 ++ scripts/codegen/csharp.ts | 88 +-- 24 files changed, 2087 insertions(+), 98 deletions(-) create mode 100644 nodejs/docs/agent-author.md create mode 100644 nodejs/docs/examples.md create mode 100644 nodejs/docs/extensions.md diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 9cee420976..01911d5896 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -192,6 +192,28 @@ public class AccountGetQuotaResult public Dictionary QuotaSnapshots { get; set; } = []; } +public class SessionLogResult +{ + /// The unique identifier of the emitted session event + [JsonPropertyName("eventId")] + public Guid EventId { get; set; } +} + +internal class SessionLogRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("level")] + public SessionLogRequestLevel? Level { get; set; } + + [JsonPropertyName("ephemeral")] + public bool? Ephemeral { get; set; } +} + public class SessionModelGetCurrentResult { [JsonPropertyName("modelId")] @@ -217,6 +239,9 @@ internal class SessionModelSwitchToRequest [JsonPropertyName("modelId")] public string ModelId { get; set; } = string.Empty; + + [JsonPropertyName("reasoningEffort")] + public SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; } } public class SessionModeGetResult @@ -511,6 +536,32 @@ internal class SessionPermissionsHandlePendingPermissionRequestRequest public object Result { get; set; } = null!; } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionLogRequestLevel +{ + [JsonStringEnumMemberName("info")] + Info, + [JsonStringEnumMemberName("warning")] + Warning, + [JsonStringEnumMemberName("error")] + Error, +} + + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionModelSwitchToRequestReasoningEffort +{ + [JsonStringEnumMemberName("low")] + Low, + [JsonStringEnumMemberName("medium")] + Medium, + [JsonStringEnumMemberName("high")] + High, + [JsonStringEnumMemberName("xhigh")] + Xhigh, +} + + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode { @@ -643,6 +694,13 @@ internal SessionRpc(JsonRpc rpc, string sessionId) public ToolsApi Tools { get; } public PermissionsApi Permissions { get; } + + /// Calls "session.log". + public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) + { + var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.log", [request], cancellationToken); + } } public class ModelApi @@ -664,9 +722,9 @@ public async Task GetCurrentAsync(CancellationToke } /// Calls "session.model.switchTo". - public async Task SwitchToAsync(string modelId, CancellationToken cancellationToken = default) + public async Task SwitchToAsync(string modelId, SessionModelSwitchToRequestReasoningEffort? reasoningEffort = null, CancellationToken cancellationToken = default) { - var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId }; + var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId, ReasoningEffort = reasoningEffort }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.model.switchTo", [request], cancellationToken); } } @@ -909,6 +967,8 @@ public async Task Handle [JsonSerializable(typeof(SessionCompactionCompactResult))] [JsonSerializable(typeof(SessionFleetStartRequest))] [JsonSerializable(typeof(SessionFleetStartResult))] +[JsonSerializable(typeof(SessionLogRequest))] +[JsonSerializable(typeof(SessionLogResult))] [JsonSerializable(typeof(SessionModeGetRequest))] [JsonSerializable(typeof(SessionModeGetResult))] [JsonSerializable(typeof(SessionModeSetRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index f87ab32d4c..5bdf50df00 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -69,6 +69,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SubagentSelectedEvent), "subagent.selected")] [JsonDerivedType(typeof(SubagentStartedEvent), "subagent.started")] [JsonDerivedType(typeof(SystemMessageEvent), "system.message")] +[JsonDerivedType(typeof(SystemNotificationEvent), "system.notification")] [JsonDerivedType(typeof(ToolExecutionCompleteEvent), "tool.execution_complete")] [JsonDerivedType(typeof(ToolExecutionPartialResultEvent), "tool.execution_partial_result")] [JsonDerivedType(typeof(ToolExecutionProgressEvent), "tool.execution_progress")] @@ -657,6 +658,18 @@ public partial class SystemMessageEvent : SessionEvent public required SystemMessageData Data { get; set; } } +/// +/// Event: system.notification +/// +public partial class SystemNotificationEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "system.notification"; + + [JsonPropertyName("data")] + public required SystemNotificationData Data { get; set; } +} + /// /// Event: permission.requested /// @@ -825,6 +838,10 @@ public partial class SessionStartData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionStartDataContext? Context { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("alreadyInUse")] + public bool? AlreadyInUse { get; set; } } public partial class SessionResumeData @@ -838,6 +855,10 @@ public partial class SessionResumeData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionResumeDataContext? Context { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("alreadyInUse")] + public bool? AlreadyInUse { get; set; } } public partial class SessionErrorData @@ -1522,6 +1543,15 @@ public partial class SystemMessageData public SystemMessageDataMetadata? Metadata { get; set; } } +public partial class SystemNotificationData +{ + [JsonPropertyName("content")] + public required string Content { get; set; } + + [JsonPropertyName("kind")] + public required SystemNotificationDataKind Kind { get; set; } +} + public partial class PermissionRequestedData { [JsonPropertyName("requestId")] @@ -2095,6 +2125,72 @@ public partial class SystemMessageDataMetadata public Dictionary? Variables { get; set; } } +public partial class SystemNotificationDataKindAgentCompleted : SystemNotificationDataKind +{ + [JsonIgnore] + public override string Type => "agent_completed"; + + [JsonPropertyName("agentId")] + public required string AgentId { get; set; } + + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } + + [JsonPropertyName("status")] + public required SystemNotificationDataKindAgentCompletedStatus Status { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("prompt")] + public string? Prompt { get; set; } +} + +public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind +{ + [JsonIgnore] + public override string Type => "shell_completed"; + + [JsonPropertyName("shellId")] + public required string ShellId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public double? ExitCode { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNotificationDataKind +{ + [JsonIgnore] + public override string Type => "shell_detached_completed"; + + [JsonPropertyName("shellId")] + public required string ShellId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SystemNotificationDataKindAgentCompleted), "agent_completed")] +[JsonDerivedType(typeof(SystemNotificationDataKindShellCompleted), "shell_completed")] +[JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")] +public partial class SystemNotificationDataKind +{ + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} + + public partial class PermissionRequestShellCommandsItem { [JsonPropertyName("identifier")] @@ -2390,6 +2486,15 @@ public enum SystemMessageDataRole Developer, } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SystemNotificationDataKindAgentCompletedStatus +{ + [JsonStringEnumMemberName("completed")] + Completed, + [JsonStringEnumMemberName("failed")] + Failed, +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum PermissionCompletedDataResultKind { @@ -2536,6 +2641,12 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SystemMessageData))] [JsonSerializable(typeof(SystemMessageDataMetadata))] [JsonSerializable(typeof(SystemMessageEvent))] +[JsonSerializable(typeof(SystemNotificationData))] +[JsonSerializable(typeof(SystemNotificationDataKind))] +[JsonSerializable(typeof(SystemNotificationDataKindAgentCompleted))] +[JsonSerializable(typeof(SystemNotificationDataKindShellCompleted))] +[JsonSerializable(typeof(SystemNotificationDataKindShellDetachedCompleted))] +[JsonSerializable(typeof(SystemNotificationEvent))] [JsonSerializable(typeof(ToolExecutionCompleteData))] [JsonSerializable(typeof(ToolExecutionCompleteDataError))] [JsonSerializable(typeof(ToolExecutionCompleteDataResult))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 282fc50d5f..b9d70a2ab6 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -671,7 +671,29 @@ await InvokeRpcAsync( /// public async Task SetModelAsync(string model, CancellationToken cancellationToken = default) { - await Rpc.Model.SwitchToAsync(model, cancellationToken); + await Rpc.Model.SwitchToAsync(model, cancellationToken: cancellationToken); + } + + /// + /// Log a message to the session timeline. + /// The message appears in the session event stream and is visible to SDK consumers + /// and (for non-ephemeral messages) persisted to the session event log on disk. + /// + /// The message to log. + /// Log level (default: info). + /// When true, the message is not persisted to disk. + /// Optional cancellation token. + /// + /// + /// await session.LogAsync("Build completed successfully"); + /// await session.LogAsync("Disk space low", level: SessionLogRequestLevel.Warning); + /// await session.LogAsync("Connection failed", level: SessionLogRequestLevel.Error); + /// await session.LogAsync("Temporary status", ephemeral: true); + /// + /// + public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) + { + await Rpc.LogAsync(message, level, ephemeral, cancellationToken); } /// diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index e710835dc6..20d6f3ac50 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ using GitHub.Copilot.SDK.Test.Harness; +using GitHub.Copilot.SDK.Rpc; using Microsoft.Extensions.AI; using System.ComponentModel; using Xunit; @@ -404,4 +405,51 @@ public async Task Should_Set_Model_On_Existing_Session() var modelChanged = await modelChangedTask; Assert.Equal("gpt-4.1", modelChanged.Data.NewModel); } + + [Fact] + public async Task Should_Log_Messages_At_Various_Levels() + { + var session = await CreateSessionAsync(); + var events = new List(); + session.On(evt => events.Add(evt)); + + await session.LogAsync("Info message"); + await session.LogAsync("Warning message", level: SessionLogRequestLevel.Warning); + await session.LogAsync("Error message", level: SessionLogRequestLevel.Error); + await session.LogAsync("Ephemeral message", ephemeral: true); + + // Poll until all 4 notification events arrive + await WaitForAsync(() => + { + var notifications = events.Where(e => + e is SessionInfoEvent info && info.Data.InfoType == "notification" || + e is SessionWarningEvent warn && warn.Data.WarningType == "notification" || + e is SessionErrorEvent err && err.Data.ErrorType == "notification" + ).ToList(); + return notifications.Count >= 4; + }, timeout: TimeSpan.FromSeconds(10)); + + var infoEvent = events.OfType().First(e => e.Data.Message == "Info message"); + Assert.Equal("notification", infoEvent.Data.InfoType); + + var warningEvent = events.OfType().First(e => e.Data.Message == "Warning message"); + Assert.Equal("notification", warningEvent.Data.WarningType); + + var errorEvent = events.OfType().First(e => e.Data.Message == "Error message"); + Assert.Equal("notification", errorEvent.Data.ErrorType); + + var ephemeralEvent = events.OfType().First(e => e.Data.Message == "Ephemeral message"); + Assert.Equal("notification", ephemeralEvent.Data.InfoType); + } + + private static async Task WaitForAsync(Func condition, TimeSpan timeout) + { + var deadline = DateTime.UtcNow + timeout; + while (!condition()) + { + if (DateTime.UtcNow > deadline) + throw new TimeoutException($"Condition not met within {timeout}"); + await Task.Delay(100); + } + } } diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 86f5066f75..72e428d164 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -56,6 +56,7 @@ type SessionEvent struct { // Empty payload; the event signals that the custom agent was deselected, returning to the // default agent type Data struct { + AlreadyInUse *bool `json:"alreadyInUse,omitempty"` // Working directory and git context at session start // // Updated working directory and git context at resume time @@ -267,6 +268,8 @@ type Data struct { // Full content of the skill file, injected into the conversation for the model // // The system or developer prompt text + // + // The notification text, typically wrapped in XML tags Content *string `json:"content,omitempty"` // CAPI interaction ID for correlating this user message with its turn // @@ -426,6 +429,8 @@ type Data struct { Metadata *Metadata `json:"metadata,omitempty"` // Message role: "system" for system prompts, "developer" for developer-injected instructions Role *Role `json:"role,omitempty"` + // Structured metadata identifying what triggered this notification + Kind *KindClass `json:"kind,omitempty"` // Details of the permission being requested PermissionRequest *PermissionRequest `json:"permissionRequest,omitempty"` // Whether the user can provide a free-form text response in addition to predefined choices @@ -594,6 +599,29 @@ type ErrorClass struct { Stack *string `json:"stack,omitempty"` } +// Structured metadata identifying what triggered this notification +type KindClass struct { + // Unique identifier of the background agent + AgentID *string `json:"agentId,omitempty"` + // Type of the agent (e.g., explore, task, general-purpose) + AgentType *string `json:"agentType,omitempty"` + // Human-readable description of the agent task + // + // Human-readable description of the command + Description *string `json:"description,omitempty"` + // The full prompt given to the background agent + Prompt *string `json:"prompt,omitempty"` + // Whether the agent completed successfully or failed + Status *Status `json:"status,omitempty"` + Type KindType `json:"type"` + // Exit code of the shell command, if available + ExitCode *float64 `json:"exitCode,omitempty"` + // Unique identifier of the shell session + // + // Unique identifier of the detached shell session + ShellID *string `json:"shellId,omitempty"` +} + // Metadata about the prompt template and its construction type Metadata struct { // Version identifier of the prompt template used @@ -860,6 +888,22 @@ const ( Selection AttachmentType = "selection" ) +// Whether the agent completed successfully or failed +type Status string + +const ( + Completed Status = "completed" + Failed Status = "failed" +) + +type KindType string + +const ( + AgentCompleted KindType = "agent_completed" + ShellCompleted KindType = "shell_completed" + ShellDetachedCompleted KindType = "shell_detached_completed" +) + type Mode string const ( @@ -1011,6 +1055,7 @@ const ( SubagentSelected SessionEventType = "subagent.selected" SubagentStarted SessionEventType = "subagent.started" SystemMessage SessionEventType = "system.message" + SystemNotification SessionEventType = "system.notification" ToolExecutionComplete SessionEventType = "tool.execution_complete" ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" ToolExecutionProgress SessionEventType = "tool.execution_progress" diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index d1902311f3..8da66cdd2f 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -3,11 +3,13 @@ package e2e import ( "regexp" "strings" + "sync" "testing" "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestSession(t *testing.T) { @@ -889,3 +891,105 @@ func contains(slice []string, item string) bool { } return false } + +func TestSessionLog(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + // Collect events + var events []copilot.SessionEvent + var mu sync.Mutex + unsubscribe := session.On(func(event copilot.SessionEvent) { + mu.Lock() + defer mu.Unlock() + events = append(events, event) + }) + defer unsubscribe() + + t.Run("should log info message (default level)", func(t *testing.T) { + if err := session.Log(t.Context(), "Info message", nil); err != nil { + t.Fatalf("Log failed: %v", err) + } + + evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Info message", 5*time.Second) + if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { + t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) + } + if evt.Data.Message == nil || *evt.Data.Message != "Info message" { + t.Errorf("Expected message 'Info message', got %v", evt.Data.Message) + } + }) + + t.Run("should log warning message", func(t *testing.T) { + if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.Warning}); err != nil { + t.Fatalf("Log failed: %v", err) + } + + evt := waitForEvent(t, &mu, &events, copilot.SessionWarning, "Warning message", 5*time.Second) + if evt.Data.WarningType == nil || *evt.Data.WarningType != "notification" { + t.Errorf("Expected warningType 'notification', got %v", evt.Data.WarningType) + } + if evt.Data.Message == nil || *evt.Data.Message != "Warning message" { + t.Errorf("Expected message 'Warning message', got %v", evt.Data.Message) + } + }) + + t.Run("should log error message", func(t *testing.T) { + if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.Error}); err != nil { + t.Fatalf("Log failed: %v", err) + } + + evt := waitForEvent(t, &mu, &events, copilot.SessionError, "Error message", 5*time.Second) + if evt.Data.ErrorType == nil || *evt.Data.ErrorType != "notification" { + t.Errorf("Expected errorType 'notification', got %v", evt.Data.ErrorType) + } + if evt.Data.Message == nil || *evt.Data.Message != "Error message" { + t.Errorf("Expected message 'Error message', got %v", evt.Data.Message) + } + }) + + t.Run("should log ephemeral message", func(t *testing.T) { + if err := session.Log(t.Context(), "Ephemeral message", &copilot.LogOptions{Ephemeral: true}); err != nil { + t.Fatalf("Log failed: %v", err) + } + + evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Ephemeral message", 5*time.Second) + if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { + t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) + } + if evt.Data.Message == nil || *evt.Data.Message != "Ephemeral message" { + t.Errorf("Expected message 'Ephemeral message', got %v", evt.Data.Message) + } + }) +} + +// waitForEvent polls the collected events for a matching event type and message. +func waitForEvent(t *testing.T, mu *sync.Mutex, events *[]copilot.SessionEvent, eventType copilot.SessionEventType, message string, timeout time.Duration) copilot.SessionEvent { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + mu.Lock() + for _, evt := range *events { + if evt.Type == eventType && evt.Data.Message != nil && *evt.Data.Message == message { + mu.Unlock() + return evt + } + } + mu.Unlock() + time.Sleep(50 * time.Millisecond) + } + t.Fatalf("Timed out waiting for %s event with message %q", eventType, message) + return copilot.SessionEvent{} // unreachable +} diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 67a3542029..0e4b96e4f3 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -129,7 +129,8 @@ type SessionModelSwitchToResult struct { } type SessionModelSwitchToParams struct { - ModelID string `json:"modelId"` + ModelID string `json:"modelId"` + ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` } type SessionModeGetResult struct { @@ -296,6 +297,30 @@ type SessionPermissionsHandlePendingPermissionRequestParamsResult struct { Path *string `json:"path,omitempty"` } +type SessionLogResult struct { + // The unique identifier of the emitted session event + EventID string `json:"eventId"` +} + +type SessionLogParams struct { + // When true, the message is transient and not persisted to the session event log on disk + Ephemeral *bool `json:"ephemeral,omitempty"` + // Log severity level. Determines how the message is displayed in the timeline. Defaults to + // "info". + Level *Level `json:"level,omitempty"` + // Human-readable message + Message string `json:"message"` +} + +type ReasoningEffort string + +const ( + High ReasoningEffort = "high" + Low ReasoningEffort = "low" + Medium ReasoningEffort = "medium" + Xhigh ReasoningEffort = "xhigh" +) + // The current agent mode. // // The agent mode after switching. @@ -319,6 +344,16 @@ const ( DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" ) +// Log severity level. Determines how the message is displayed in the timeline. Defaults to +// "info". +type Level string + +const ( + Error Level = "error" + Info Level = "info" + Warning Level = "warning" +) + type ResultUnion struct { ResultResult *ResultResult String *string @@ -416,6 +451,9 @@ func (a *ModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchTo req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["modelId"] = params.ModelID + if params.ReasoningEffort != nil { + req["reasoningEffort"] = *params.ReasoningEffort + } } raw, err := a.client.Request("session.model.switchTo", req) if err != nil { @@ -725,6 +763,28 @@ type SessionRpc struct { Permissions *PermissionsRpcApi } +func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*SessionLogResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["message"] = params.Message + if params.Level != nil { + req["level"] = *params.Level + } + if params.Ephemeral != nil { + req["ephemeral"] = *params.Ephemeral + } + } + raw, err := a.client.Request("session.log", req) + if err != nil { + return nil, err + } + var result SessionLogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { return &SessionRpc{client: client, sessionID: sessionID, Model: &ModelRpcApi{client: client, sessionID: sessionID}, diff --git a/go/session.go b/go/session.go index c06a8e1ec3..74529c5235 100644 --- a/go/session.go +++ b/go/session.go @@ -701,3 +701,49 @@ func (s *Session) SetModel(ctx context.Context, model string) error { return nil } + +// LogOptions configures optional parameters for [Session.Log]. +type LogOptions struct { + // Level sets the log severity. Valid values are [rpc.Info] (default), + // [rpc.Warning], and [rpc.Error]. + Level rpc.Level + // Ephemeral marks the message as transient so it is not persisted + // to the session event log on disk. + Ephemeral bool +} + +// Log sends a log message to the session timeline. +// The message appears in the session event stream and is visible to SDK consumers +// and (for non-ephemeral messages) persisted to the session event log on disk. +// +// Pass nil for opts to use defaults (info level, non-ephemeral). +// +// Example: +// +// // Simple info message +// session.Log(ctx, "Processing started") +// +// // Warning with options +// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.Warning}) +// +// // Ephemeral message (not persisted) +// session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: true}) +func (s *Session) Log(ctx context.Context, message string, opts *LogOptions) error { + params := &rpc.SessionLogParams{Message: message} + + if opts != nil { + if opts.Level != "" { + params.Level = &opts.Level + } + if opts.Ephemeral { + params.Ephemeral = &opts.Ephemeral + } + } + + _, err := s.RPC.Log(ctx, params) + if err != nil { + return fmt.Errorf("failed to log message: %w", err) + } + + return nil +} diff --git a/nodejs/docs/agent-author.md b/nodejs/docs/agent-author.md new file mode 100644 index 0000000000..4c1e32f69f --- /dev/null +++ b/nodejs/docs/agent-author.md @@ -0,0 +1,265 @@ +# Agent Extension Authoring Guide + +A precise, step-by-step reference for agents writing Copilot CLI extensions programmatically. + +## Workflow + +### Step 1: Scaffold the extension + +Use the `extensions_manage` tool with `operation: "scaffold"`: + +``` +extensions_manage({ operation: "scaffold", name: "my-extension" }) +``` + +This creates `.github/extensions/my-extension/extension.mjs` with a working skeleton. +For user-scoped extensions (persist across all repos), add `location: "user"`. + +### Step 2: Edit the extension file + +Modify the generated `extension.mjs` using `edit` or `create` tools. The file must: +- Be named `extension.mjs` (only `.mjs` is supported) +- Use ES module syntax (`import`/`export`) +- Call `joinSession({ ... })` + +### Step 3: Reload extensions + +``` +extensions_reload({}) +``` + +This stops all running extensions and re-discovers/re-launches them. New tools are available immediately in the same turn (mid-turn refresh). + +### Step 4: Verify + +``` +extensions_manage({ operation: "list" }) +extensions_manage({ operation: "inspect", name: "my-extension" }) +``` + +Check that the extension loaded successfully and isn't marked as "failed". + +--- + +## File Structure + +``` +.github/extensions//extension.mjs +``` + +Discovery rules: +- The CLI scans `.github/extensions/` relative to the git root +- It also scans the user's copilot config extensions directory +- Only immediate subdirectories are checked (not recursive) +- Each subdirectory must contain a file named `extension.mjs` +- Project extensions shadow user extensions on name collision + +--- + +## Minimal Skeleton + +```js +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +await joinSession({ + onPermissionRequest: approveAll, // Required — handle permission requests + tools: [], // Optional — custom tools + hooks: {}, // Optional — lifecycle hooks +}); +``` + +--- + +## Registering Tools + +```js +tools: [ + { + name: "tool_name", // Required. Must be globally unique across all extensions. + description: "What it does", // Required. Shown to the agent in tool descriptions. + parameters: { // Optional. JSON Schema for the arguments. + type: "object", + properties: { + arg1: { type: "string", description: "..." }, + }, + required: ["arg1"], + }, + handler: async (args, invocation) => { + // args: parsed arguments matching the schema + // invocation.sessionId: current session ID + // invocation.toolCallId: unique call ID + // invocation.toolName: this tool's name + // + // Return value: string or ToolResultObject + // string → treated as success + // { textResultForLlm, resultType } → structured result + // resultType: "success" | "failure" | "rejected" | "denied" + return `Result: ${args.arg1}`; + }, + }, +] +``` + +**Constraints:** +- Tool names must be unique across ALL loaded extensions. Collisions cause the second extension to fail to load. +- Handler must return a string or `{ textResultForLlm: string, resultType?: string }`. +- Handler receives `(args, invocation)` — the second argument has `sessionId`, `toolCallId`, `toolName`. +- Use `session.log()` to surface messages to the user. Don't use `console.log()` (stdout is reserved for JSON-RPC). + +--- + +## Registering Hooks + +```js +hooks: { + onUserPromptSubmitted: async (input, invocation) => { ... }, + onPreToolUse: async (input, invocation) => { ... }, + onPostToolUse: async (input, invocation) => { ... }, + onSessionStart: async (input, invocation) => { ... }, + onSessionEnd: async (input, invocation) => { ... }, + onErrorOccurred: async (input, invocation) => { ... }, +} +``` + +All hook inputs include `timestamp` (unix ms) and `cwd` (working directory). +All handlers receive `invocation: { sessionId: string }` as the second argument. +All handlers may return `void`/`undefined` (no-op) or an output object. + +### onUserPromptSubmitted + +**Input:** `{ prompt: string, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `modifiedPrompt` | `string` | Replaces the user's prompt | +| `additionalContext` | `string` | Appended as hidden context the agent sees | + +### onPreToolUse + +**Input:** `{ toolName: string, toolArgs: unknown, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `permissionDecision` | `"allow" \| "deny" \| "ask"` | Override the permission check | +| `permissionDecisionReason` | `string` | Shown to user if denied | +| `modifiedArgs` | `unknown` | Replaces the tool arguments | +| `additionalContext` | `string` | Injected into the conversation | + +### onPostToolUse + +**Input:** `{ toolName: string, toolArgs: unknown, toolResult: ToolResultObject, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `modifiedResult` | `ToolResultObject` | Replaces the tool result | +| `additionalContext` | `string` | Injected into the conversation | + +### onSessionStart + +**Input:** `{ source: "startup" \| "resume" \| "new", initialPrompt?: string, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `additionalContext` | `string` | Injected as initial context | + +### onSessionEnd + +**Input:** `{ reason: "complete" \| "error" \| "abort" \| "timeout" \| "user_exit", finalMessage?: string, error?: string, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `sessionSummary` | `string` | Summary for session persistence | +| `cleanupActions` | `string[]` | Cleanup descriptions | + +### onErrorOccurred + +**Input:** `{ error: string, errorContext: "model_call" \| "tool_execution" \| "system" \| "user_input", recoverable: boolean, timestamp, cwd }` + +**Output (all fields optional):** +| Field | Type | Effect | +|-------|------|--------| +| `errorHandling` | `"retry" \| "skip" \| "abort"` | How to handle the error | +| `retryCount` | `number` | Max retries (when errorHandling is "retry") | +| `userNotification` | `string` | Message shown to the user | + +--- + +## Session Object + +After `joinSession()`, the returned `session` provides: + +### session.send(options) + +Send a message programmatically: +```js +await session.send({ prompt: "Analyze the test results." }); +await session.send({ + prompt: "Review this file", + attachments: [{ type: "file", path: "./src/index.ts" }], +}); +``` + +### session.sendAndWait(options, timeout?) + +Send and block until the agent finishes (resolves on `session.idle`): +```js +const response = await session.sendAndWait({ prompt: "What is 2+2?" }); +// response?.data.content contains the agent's reply +``` + +### session.log(message, options?) + +Log to the CLI timeline: +```js +await session.log("Extension ready"); +await session.log("Rate limit approaching", { level: "warning" }); +await session.log("Connection failed", { level: "error" }); +await session.log("Processing...", { ephemeral: true }); // transient, not persisted +``` + +### session.on(eventType, handler) + +Subscribe to session events. Returns an unsubscribe function. +```js +const unsub = session.on("tool.execution_complete", (event) => { + // event.data.toolName, event.data.success, event.data.result +}); +``` + +### Key Event Types + +| Event | Key Data Fields | +|-------|----------------| +| `assistant.message` | `content`, `messageId` | +| `tool.execution_start` | `toolCallId`, `toolName`, `arguments` | +| `tool.execution_complete` | `toolCallId`, `toolName`, `success`, `result`, `error` | +| `user.message` | `content`, `attachments`, `source` | +| `session.idle` | `backgroundTasks` | +| `session.error` | `errorType`, `message`, `stack` | +| `permission.requested` | `requestId`, `permissionRequest.kind` | +| `session.shutdown` | `shutdownType`, `totalPremiumRequests` | + +### session.workspacePath + +Path to the session workspace directory (checkpoints, plan.md, files/). `undefined` if infinite sessions disabled. + +### session.rpc + +Low-level typed RPC access to all session APIs (model, mode, plan, workspace, etc.). + +--- + +## Gotchas + +- **stdout is reserved for JSON-RPC.** Don't use `console.log()` — it will corrupt the protocol. Use `session.log()` to surface messages to the user. +- **Tool name collisions are fatal.** If two extensions register the same tool name, the second extension fails to initialize. +- **Don't call `session.send()` synchronously from `onUserPromptSubmitted`.** Use `setTimeout(() => session.send(...), 0)` to avoid infinite loops. +- **Extensions are reloaded on `/clear`.** Any in-memory state is lost between sessions. +- **Only `.mjs` is supported.** TypeScript (`.ts`) is not yet supported. +- **The handler's return value is the tool result.** Returning `undefined` sends an empty success. Throwing sends a failure with the error message. diff --git a/nodejs/docs/examples.md b/nodejs/docs/examples.md new file mode 100644 index 0000000000..a5b03f87ea --- /dev/null +++ b/nodejs/docs/examples.md @@ -0,0 +1,681 @@ +# Copilot CLI Extension Examples + +A practical guide to writing extensions using the `@github/copilot-sdk` extension API. + +## Extension Skeleton + +Every extension starts with the same boilerplate: + +```js +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +const session = await joinSession({ + onPermissionRequest: approveAll, + hooks: { /* ... */ }, + tools: [ /* ... */ ], +}); +``` + +`joinSession` returns a `CopilotSession` object you can use to send messages and subscribe to events. + +> **Platform notes (Windows vs macOS/Linux):** +> - Use `process.platform === "win32"` to detect Windows at runtime. +> - Clipboard: `pbcopy` on macOS, `clip` on Windows. +> - Use `exec()` instead of `execFile()` for `.cmd` scripts like `code`, `npx`, `npm` on Windows. +> - PowerShell stderr redirection uses `*>&1` instead of `2>&1`. + +--- + +## Logging to the Timeline + +Use `session.log()` to surface messages to the user in the CLI timeline: + +```js +const session = await joinSession({ + onPermissionRequest: approveAll, + hooks: { + onSessionStart: async () => { + await session.log("My extension loaded"); + }, + onPreToolUse: async (input) => { + if (input.toolName === "bash") { + await session.log(`Running: ${input.toolArgs?.command}`, { ephemeral: true }); + } + }, + }, + tools: [], +}); +``` + +Levels: `"info"` (default), `"warning"`, `"error"`. Set `ephemeral: true` for transient messages that aren't persisted. + +--- + +## Registering Custom Tools + +Tools are functions the agent can call. Define them with a name, description, JSON Schema parameters, and a handler. + +### Basic tool + +```js +tools: [ + { + name: "my_tool", + description: "Does something useful", + parameters: { + type: "object", + properties: { + input: { type: "string", description: "The input value" }, + }, + required: ["input"], + }, + handler: async (args) => { + return `Processed: ${args.input}`; + }, + }, +] +``` + +### Tool that invokes an external shell command + +```js +import { execFile } from "node:child_process"; + +{ + name: "run_command", + description: "Runs a shell command and returns its output", + parameters: { + type: "object", + properties: { + command: { type: "string", description: "The command to run" }, + }, + required: ["command"], + }, + handler: async (args) => { + const isWindows = process.platform === "win32"; + const shell = isWindows ? "powershell" : "bash"; + const shellArgs = isWindows + ? ["-NoProfile", "-Command", args.command] + : ["-c", args.command]; + return new Promise((resolve) => { + execFile(shell, shellArgs, (err, stdout, stderr) => { + if (err) resolve(`Error: ${stderr || err.message}`); + else resolve(stdout); + }); + }); + }, +} +``` + +### Tool that calls an external API + +```js +{ + name: "fetch_data", + description: "Fetches data from an API endpoint", + parameters: { + type: "object", + properties: { + url: { type: "string", description: "The URL to fetch" }, + }, + required: ["url"], + }, + handler: async (args) => { + const res = await fetch(args.url); + if (!res.ok) return `Error: HTTP ${res.status}`; + return await res.text(); + }, +} +``` + +### Tool handler invocation context + +The handler receives a second argument with invocation metadata: + +```js +handler: async (args, invocation) => { + // invocation.sessionId — current session ID + // invocation.toolCallId — unique ID for this tool call + // invocation.toolName — name of the tool being called + return "done"; +} +``` + +--- + +## Hooks + +Hooks intercept and modify behavior at key lifecycle points. Register them in the `hooks` option. + +### Available Hooks + +| Hook | Fires When | Can Modify | +|------|-----------|------------| +| `onUserPromptSubmitted` | User sends a message | The prompt text, add context | +| `onPreToolUse` | Before a tool executes | Tool args, permission decision, add context | +| `onPostToolUse` | After a tool executes | Tool result, add context | +| `onSessionStart` | Session starts or resumes | Add context, modify config | +| `onSessionEnd` | Session ends | Cleanup actions, summary | +| `onErrorOccurred` | An error occurs | Error handling strategy (retry/skip/abort) | + +All hook inputs include `timestamp` (unix ms) and `cwd` (working directory). + +### Modifying the user's message + +Use `onUserPromptSubmitted` to rewrite or augment what the user typed before the agent sees it. + +```js +hooks: { + onUserPromptSubmitted: async (input) => { + // Rewrite the prompt + return { modifiedPrompt: input.prompt.toUpperCase() }; + }, +} +``` + +### Injecting additional context into every message + +Return `additionalContext` to silently append instructions the agent will follow. + +```js +hooks: { + onUserPromptSubmitted: async (input) => { + return { + additionalContext: "Always respond in bullet points. Follow our team coding standards.", + }; + }, +} +``` + +### Sending a follow-up message based on a keyword + +Use `session.send()` to programmatically inject a new user message. + +```js +hooks: { + onUserPromptSubmitted: async (input) => { + if (/\\burgent\\b/i.test(input.prompt)) { + // Fire-and-forget a follow-up message + setTimeout(() => session.send({ prompt: "Please prioritize this." }), 0); + } + }, +} +``` + +> **Tip:** Guard against infinite loops if your follow-up message could re-trigger the same hook. + +### Blocking dangerous tool calls + +Use `onPreToolUse` to inspect and optionally deny tool execution. + +```js +hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "bash") { + const cmd = String(input.toolArgs?.command || ""); + if (/rm\\s+-rf/i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { + return { + permissionDecision: "deny", + permissionDecisionReason: "Destructive commands are not allowed.", + }; + } + } + // Allow everything else + return { permissionDecision: "allow" }; + }, +} +``` + +### Modifying tool arguments before execution + +```js +hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "bash") { + const redirect = process.platform === "win32" ? "*>&1" : "2>&1"; + return { + modifiedArgs: { + ...input.toolArgs, + command: `${input.toolArgs.command} ${redirect}`, + }, + }; + } + }, +} +``` + +### Reacting when the agent creates or edits a file + +Use `onPostToolUse` to run side effects after a tool completes. + +```js +import { exec } from "node:child_process"; + +hooks: { + onPostToolUse: async (input) => { + if (input.toolName === "create" || input.toolName === "edit") { + const filePath = input.toolArgs?.path; + if (filePath) { + // Open the file in VS Code + exec(`code "${filePath}"`, () => {}); + } + } + }, +} +``` + +### Augmenting tool results with extra context + +```js +hooks: { + onPostToolUse: async (input) => { + if (input.toolName === "bash" && input.toolResult?.resultType === "failure") { + return { + additionalContext: "The command failed. Try a different approach.", + }; + } + }, +} +``` + +### Running a linter after every file edit + +```js +import { exec } from "node:child_process"; + +hooks: { + onPostToolUse: async (input) => { + if (input.toolName === "edit") { + const filePath = input.toolArgs?.path; + if (filePath?.endsWith(".ts")) { + const result = await new Promise((resolve) => { + exec(`npx eslint "${filePath}"`, (err, stdout) => { + resolve(err ? stdout : "No lint errors."); + }); + }); + return { additionalContext: `Lint result: ${result}` }; + } + } + }, +} +``` + +### Handling errors with retry logic + +```js +hooks: { + onErrorOccurred: async (input) => { + if (input.recoverable && input.errorContext === "model_call") { + return { errorHandling: "retry", retryCount: 2 }; + } + return { + errorHandling: "abort", + userNotification: `An error occurred: ${input.error}`, + }; + }, +} +``` + +### Session lifecycle hooks + +```js +hooks: { + onSessionStart: async (input) => { + // input.source is "startup", "resume", or "new" + return { additionalContext: "Remember to write tests for all changes." }; + }, + onSessionEnd: async (input) => { + // input.reason is "complete", "error", "abort", "timeout", or "user_exit" + }, +} +``` + +--- + +## Session Events + +After calling `joinSession`, use `session.on()` to react to events in real time. + +### Listening to a specific event type + +```js +session.on("assistant.message", (event) => { + // event.data.content has the agent's response text +}); +``` + +### Listening to all events + +```js +session.on((event) => { + // event.type and event.data are available for all events +}); +``` + +### Unsubscribing from events + +`session.on()` returns an unsubscribe function: + +```js +const unsubscribe = session.on("tool.execution_complete", (event) => { + // event.data.toolName, event.data.success, event.data.result, event.data.error +}); + +// Later, stop listening +unsubscribe(); +``` + +### Example: Auto-copy agent responses to clipboard + +Combine a hook (to detect a keyword) with a session event (to capture the response): + +```js +import { execFile } from "node:child_process"; + +let copyNextResponse = false; + +function copyToClipboard(text) { + const cmd = process.platform === "win32" ? "clip" : "pbcopy"; + const proc = execFile(cmd, [], () => {}); + proc.stdin.write(text); + proc.stdin.end(); +} + +const session = await joinSession({ + onPermissionRequest: approveAll, + hooks: { + onUserPromptSubmitted: async (input) => { + if (/\\bcopy\\b/i.test(input.prompt)) { + copyNextResponse = true; + } + }, + }, + tools: [], +}); + +session.on("assistant.message", (event) => { + if (copyNextResponse) { + copyNextResponse = false; + copyToClipboard(event.data.content); + } +}); +``` + +### Top 10 Most Useful Event Types + +| Event Type | Description | Key Data Fields | +|-----------|-------------|-----------------| +| `assistant.message` | Agent's final response | `content`, `messageId`, `toolRequests` | +| `assistant.streaming_delta` | Token-by-token streaming (ephemeral) | `totalResponseSizeBytes` | +| `tool.execution_start` | A tool is about to run | `toolCallId`, `toolName`, `arguments` | +| `tool.execution_complete` | A tool finished running | `toolCallId`, `toolName`, `success`, `result`, `error` | +| `user.message` | User sent a message | `content`, `attachments`, `source` | +| `session.idle` | Session finished processing a turn | `backgroundTasks` | +| `session.error` | An error occurred | `errorType`, `message`, `stack` | +| `permission.requested` | Agent needs permission (shell, file write, etc.) | `requestId`, `permissionRequest.kind` | +| `session.shutdown` | Session is ending | `shutdownType`, `totalPremiumRequests`, `codeChanges` | +| `assistant.turn_start` | Agent begins a new thinking/response cycle | `turnId` | + +### Example: Detecting when the plan file is created or edited + +Use `session.workspacePath` to locate the session's `plan.md`, then `fs.watchFile` to detect changes. +Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCallId` to distinguish agent edits from user edits. + +```js +import { existsSync, watchFile, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +const agentEdits = new Set(); // toolCallIds for in-flight agent edits +const recentAgentPaths = new Set(); // paths recently written by the agent + +const session = await joinSession({ + onPermissionRequest: approveAll, +}); + +const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/ +if (workspace) { + const planPath = join(workspace, "plan.md"); + let lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; + + // Track agent edits to suppress false triggers + session.on("tool.execution_start", (event) => { + if ((event.data.toolName === "edit" || event.data.toolName === "create") + && String(event.data.arguments?.path || "").endsWith("plan.md")) { + agentEdits.add(event.data.toolCallId); + recentAgentPaths.add(planPath); + } + }); + session.on("tool.execution_complete", (event) => { + if (agentEdits.delete(event.data.toolCallId)) { + setTimeout(() => { + recentAgentPaths.delete(planPath); + lastContent = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; + }, 2000); + } + }); + + watchFile(planPath, { interval: 1000 }, () => { + if (recentAgentPaths.has(planPath) || agentEdits.size > 0) return; + const content = existsSync(planPath) ? readFileSync(planPath, "utf-8") : null; + if (content === lastContent) return; + const wasCreated = lastContent === null && content !== null; + lastContent = content; + if (content !== null) { + session.send({ + prompt: `The plan was ${wasCreated ? "created" : "edited"} by the user.`, + }); + } + }); +} +``` + +### Example: Reacting when the user manually edits any file in the repo + +Use `fs.watch` with `recursive: true` on `process.cwd()` to detect file changes. +Filter out agent edits by tracking `tool.execution_start` / `tool.execution_complete` events. + +```js +import { watch, readFileSync, statSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +const agentEditPaths = new Set(); + +const session = await joinSession({ + onPermissionRequest: approveAll, +}); + +const cwd = process.cwd(); +const IGNORE = new Set(["node_modules", ".git", "dist"]); + +// Track agent file edits +session.on("tool.execution_start", (event) => { + if (event.data.toolName === "edit" || event.data.toolName === "create") { + const p = String(event.data.arguments?.path || ""); + if (p) agentEditPaths.add(resolve(p)); + } +}); +session.on("tool.execution_complete", (event) => { + // Clear after a delay to avoid race with fs.watch + const p = [...agentEditPaths].find((x) => x); // any tracked path + setTimeout(() => agentEditPaths.clear(), 3000); +}); + +const debounce = new Map(); + +watch(cwd, { recursive: true }, (eventType, filename) => { + if (!filename || eventType !== "change") return; + if (filename.split(/[\\\\\\/]/).some((p) => IGNORE.has(p))) return; + + if (debounce.has(filename)) clearTimeout(debounce.get(filename)); + debounce.set(filename, setTimeout(() => { + debounce.delete(filename); + const fullPath = join(cwd, filename); + if (agentEditPaths.has(resolve(fullPath))) return; + + try { if (!statSync(fullPath).isFile()) return; } catch { return; } + const relPath = relative(cwd, fullPath); + session.send({ + prompt: `The user edited \\`${relPath}\\`.`, + attachments: [{ type: "file", path: fullPath }], + }); + }, 500)); +}); +``` + +--- + +## Sending Messages Programmatically + +### Fire-and-forget + +```js +await session.send({ prompt: "Analyze the test results." }); +``` + +### Send and wait for the response + +```js +const response = await session.sendAndWait({ prompt: "What is 2 + 2?" }); +// response?.data.content contains the agent's reply +``` + +### Send with file attachments + +```js +await session.send({ + prompt: "Review this file", + attachments: [ + { type: "file", path: "./src/index.ts" }, + ], +}); +``` + +--- + +## Permission and User Input Handlers + +### Custom permission logic + +```js +const session = await joinSession({ + onPermissionRequest: async (request) => { + if (request.kind === "shell") { + // request.fullCommandText has the shell command + return { kind: "approved" }; + } + if (request.kind === "write") { + return { kind: "approved" }; + } + return { kind: "denied-by-rules" }; + }, +}); +``` + +### Handling agent questions (ask_user) + +Register `onUserInputRequest` to enable the agent's `ask_user` tool: + +```js +const session = await joinSession({ + onPermissionRequest: approveAll, + onUserInputRequest: async (request) => { + // request.question has the agent's question + // request.choices has the options (if multiple choice) + return { answer: "yes", wasFreeform: false }; + }, +}); +``` + +--- + +## Complete Example: Multi-Feature Extension + +An extension that combines tools, hooks, and events. + +```js +import { execFile, exec } from "node:child_process"; +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +const isWindows = process.platform === "win32"; +let copyNextResponse = false; + +function copyToClipboard(text) { + const proc = execFile(isWindows ? "clip" : "pbcopy", [], () => {}); + proc.stdin.write(text); + proc.stdin.end(); +} + +function openInEditor(filePath) { + if (isWindows) exec(`code "${filePath}"`, () => {}); + else execFile("code", [filePath], () => {}); +} + +const session = await joinSession({ + onPermissionRequest: approveAll, + hooks: { + onUserPromptSubmitted: async (input) => { + if (/\\bcopy this\\b/i.test(input.prompt)) { + copyNextResponse = true; + } + return { + additionalContext: "Follow our team style guide. Use 4-space indentation.", + }; + }, + onPreToolUse: async (input) => { + if (input.toolName === "bash") { + const cmd = String(input.toolArgs?.command || ""); + if (/rm\\s+-rf\\s+\\//i.test(cmd) || /Remove-Item\\s+.*-Recurse/i.test(cmd)) { + return { permissionDecision: "deny" }; + } + } + }, + onPostToolUse: async (input) => { + if (input.toolName === "create" || input.toolName === "edit") { + const filePath = input.toolArgs?.path; + if (filePath) openInEditor(filePath); + } + }, + }, + tools: [ + { + name: "copy_to_clipboard", + description: "Copies text to the system clipboard.", + parameters: { + type: "object", + properties: { + text: { type: "string", description: "Text to copy" }, + }, + required: ["text"], + }, + handler: async (args) => { + return new Promise((resolve) => { + const proc = execFile(isWindows ? "clip" : "pbcopy", [], (err) => { + if (err) resolve(`Error: ${err.message}`); + else resolve("Copied to clipboard."); + }); + proc.stdin.write(args.text); + proc.stdin.end(); + }); + }, + }, + ], +}); + +session.on("assistant.message", (event) => { + if (copyNextResponse) { + copyNextResponse = false; + copyToClipboard(event.data.content); + } +}); + +session.on("tool.execution_complete", (event) => { + // event.data.success, event.data.toolName, event.data.result +}); +``` + diff --git a/nodejs/docs/extensions.md b/nodejs/docs/extensions.md new file mode 100644 index 0000000000..5eff9135b7 --- /dev/null +++ b/nodejs/docs/extensions.md @@ -0,0 +1,61 @@ +# Copilot CLI Extensions + +Extensions add custom tools, hooks, and behaviors to the Copilot CLI. They run as separate Node.js processes that communicate with the CLI over JSON-RPC via stdio. + +## How Extensions Work + +``` +┌─────────────────────┐ JSON-RPC / stdio ┌──────────────────────┐ +│ Copilot CLI │ ◄──────────────────────────────────► │ Extension Process │ +│ (parent process) │ tool calls, events, hooks │ (forked child) │ +│ │ │ │ +│ • Discovers exts │ │ • Registers tools │ +│ • Forks processes │ │ • Registers hooks │ +│ • Routes tool calls │ │ • Listens to events │ +│ • Manages lifecycle │ │ • Uses SDK APIs │ +└─────────────────────┘ └──────────────────────┘ +``` + +1. **Discovery**: The CLI scans `.github/extensions/` (project) and the user's copilot config extensions directory for subdirectories containing `extension.mjs`. +2. **Launch**: Each extension is forked as a child process with `@github/copilot-sdk` available via an automatic module resolver. +3. **Connection**: The extension calls `joinSession()` which establishes a JSON-RPC connection over stdio to the CLI and attaches to the user's current foreground session. +4. **Registration**: Tools and hooks declared in the session options are registered with the CLI and become available to the agent. +5. **Lifecycle**: Extensions are reloaded on `/clear` (or if the foreground session is replaced) and stopped on CLI exit (SIGTERM, then SIGKILL after 5s). + +## File Structure + +``` +.github/extensions/ + my-extension/ + extension.mjs ← Entry point (required, must be .mjs) +``` + +- Only `.mjs` files are supported (ES modules). The file must be named `extension.mjs`. +- Each extension lives in its own subdirectory. +- The `@github/copilot-sdk` import is resolved automatically — you don't install it. + +## The SDK + +Extensions use `@github/copilot-sdk` for all interactions with the CLI: + +```js +import { approveAll } from "@github/copilot-sdk"; +import { joinSession } from "@github/copilot-sdk/extension"; + +const session = await joinSession({ + onPermissionRequest: approveAll, + tools: [ + /* ... */ + ], + hooks: { + /* ... */ + }, +}); +``` + +The `session` object provides methods for sending messages, logging to the timeline, listening to events, and accessing the RPC API. See the `.d.ts` files in the SDK package for full type information. + +## Further Reading + +- `examples.md` — Practical code examples for tools, hooks, events, and complete extensions +- `agent-author.md` — Step-by-step workflow for agents authoring extensions programmatically diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 78aacd1c0b..a07746bfdc 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.2", + "@github/copilot": "^1.0.3-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.2.tgz", - "integrity": "sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.3-0.tgz", + "integrity": "sha512-wvd3FwQUgf4Bm3dwRBNXdjE60eGi+4cK0Shn9Ky8GSuusHtClIanTL65ft5HdOlZ1H+ieyWrrGgu7rO1Sip/yQ==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.2", - "@github/copilot-darwin-x64": "1.0.2", - "@github/copilot-linux-arm64": "1.0.2", - "@github/copilot-linux-x64": "1.0.2", - "@github/copilot-win32-arm64": "1.0.2", - "@github/copilot-win32-x64": "1.0.2" + "@github/copilot-darwin-arm64": "1.0.3-0", + "@github/copilot-darwin-x64": "1.0.3-0", + "@github/copilot-linux-arm64": "1.0.3-0", + "@github/copilot-linux-x64": "1.0.3-0", + "@github/copilot-win32-arm64": "1.0.3-0", + "@github/copilot-win32-x64": "1.0.3-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.3-0.tgz", + "integrity": "sha512-9bpouod3i4S5TbO9zMb6e47O2l8tussndaQu8D2nD7dBVUO/p+k7r9N1agAZ9/h3zrIqWo+JpJ57iUYb8tbCSw==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.2.tgz", - "integrity": "sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.3-0.tgz", + "integrity": "sha512-L4/OJLcnSnPIUIPaTZR6K7+mjXDPkHFNixioefJZQvJerOZdo9LTML6zkc2j21dWleSHiOVaLAfUdoLMyWzaVg==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.2.tgz", - "integrity": "sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.3-0.tgz", + "integrity": "sha512-3zGP9UuQAh7goXo7Ae2jm1SPpHWmNJw3iW6oEIhTocYm+xUecYdny7AbDAQs491fZcVGYea22Jqyynlcj1lH/g==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.2.tgz", - "integrity": "sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.3-0.tgz", + "integrity": "sha512-cdxGofsF7LHjw5mO0uvmsK4wl1QnW3cd2rhwc14XgWMXbenlgyBTmwamGbVdlYtZRIAYgKNQAo3PpZSsyPXw8A==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.2.tgz", - "integrity": "sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.3-0.tgz", + "integrity": "sha512-ZjUDdE7IOi6EeUEb8hJvRu5RqPrY5kuPzdqMAiIqwDervBdNJwy9AkCNtg0jJ2fPamoQgKSFcAX7QaUX4kMx3A==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.2.tgz", - "integrity": "sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ==", + "version": "1.0.3-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.3-0.tgz", + "integrity": "sha512-mNoeF4hwbxXxDtGZPWe78jEfAwdQbG1Zeyztme7Z19NjZF4bUI/iDaifKUfn+fMzGHZyykoaPl9mLrTSYr77Cw==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index ccd63582a4..4b4071270b 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -44,7 +44,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.2", + "@github/copilot": "^1.0.3-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -70,6 +70,7 @@ }, "files": [ "dist/**/*", + "docs/**/*", "README.md" ] } diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 8cc79bf56e..b94c0a5a63 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -77,6 +77,26 @@ function toJsonSchema(parameters: Tool["parameters"]): Record | return parameters; } +function getNodeExecPath(): string { + if (process.versions.bun) { + return "node"; + } + return process.execPath; +} + +/** + * Gets the path to the bundled CLI from the @github/copilot package. + * Uses index.js directly rather than npm-loader.js (which spawns the native binary). + */ +function getBundledCliPath(): string { + // Find the actual location of the @github/copilot package by resolving its sdk export + const sdkUrl = import.meta.resolve("@github/copilot/sdk"); + const sdkPath = fileURLToPath(sdkUrl); + // sdkPath is like .../node_modules/@github/copilot/sdk/index.js + // Go up two levels to get the package root, then append index.js + return join(dirname(dirname(sdkPath)), "index.js"); +} + /** * Main client for interacting with the Copilot CLI. * @@ -110,27 +130,6 @@ function toJsonSchema(parameters: Tool["parameters"]): Record | * await client.stop(); * ``` */ - -function getNodeExecPath(): string { - if (process.versions.bun) { - return "node"; - } - return process.execPath; -} - -/** - * Gets the path to the bundled CLI from the @github/copilot package. - * Uses index.js directly rather than npm-loader.js (which spawns the native binary). - */ -function getBundledCliPath(): string { - // Find the actual location of the @github/copilot package by resolving its sdk export - const sdkUrl = import.meta.resolve("@github/copilot/sdk"); - const sdkPath = fileURLToPath(sdkUrl); - // sdkPath is like .../node_modules/@github/copilot/sdk/index.js - // Go up two levels to get the package root, then append index.js - return join(dirname(dirname(sdkPath)), "index.js"); -} - export class CopilotClient { private cliProcess: ChildProcess | null = null; private connection: MessageConnection | null = null; diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index b84fb2b6fb..0a9b7b05d6 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -3,5 +3,37 @@ *--------------------------------------------------------------------------------------------*/ import { CopilotClient } from "./client.js"; +import type { CopilotSession } from "./session.js"; +import type { ResumeSessionConfig } from "./types.js"; -export const extension = new CopilotClient({ isChildProcess: true }); +/** + * Joins the current foreground session. + * + * @param config - Configuration to add to the session + * @returns A promise that resolves with the joined session + * + * @example + * ```typescript + * import { approveAll } from "@github/copilot-sdk"; + * import { joinSession } from "@github/copilot-sdk/extension"; + * + * const session = await joinSession({ + * onPermissionRequest: approveAll, + * tools: [myTool], + * }); + * ``` + */ +export async function joinSession(config: ResumeSessionConfig): Promise { + const sessionId = process.env.SESSION_ID; + if (!sessionId) { + throw new Error( + "joinSession() is intended for extensions running as child processes of the Copilot CLI." + ); + } + + const client = new CopilotClient({ isChildProcess: true }); + return client.resumeSession(sessionId, { + ...config, + disableResume: config.disableResume ?? true, + }); +} diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index c230348e09..ec40bfa695 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -173,6 +173,7 @@ export interface SessionModelSwitchToParams { */ sessionId: string; modelId: string; + reasoningEffort?: "low" | "medium" | "high" | "xhigh"; } export interface SessionModeGetResult { @@ -489,6 +490,32 @@ export interface SessionPermissionsHandlePendingPermissionRequestParams { }; } +export interface SessionLogResult { + /** + * The unique identifier of the emitted session event + */ + eventId: string; +} + +export interface SessionLogParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Human-readable message + */ + message: string; + /** + * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". + */ + level?: "info" | "warning" | "error"; + /** + * When true, the message is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -566,5 +593,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin handlePendingPermissionRequest: async (params: Omit): Promise => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), }, + log: async (params: Omit): Promise => + connection.sendRequest("session.log", { sessionId, ...params }), }; } diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index cf87e10251..f5329cc88c 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -68,6 +68,7 @@ export type SessionEvent = */ branch?: string; }; + alreadyInUse?: boolean; }; } | { @@ -118,6 +119,7 @@ export type SessionEvent = */ branch?: string; }; + alreadyInUse?: boolean; }; } | { @@ -2152,6 +2154,84 @@ export type SessionEvent = }; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + type: "system.notification"; + data: { + /** + * The notification text, typically wrapped in XML tags + */ + content: string; + /** + * Structured metadata identifying what triggered this notification + */ + kind: + | { + type: "agent_completed"; + /** + * Unique identifier of the background agent + */ + agentId: string; + /** + * Type of the agent (e.g., explore, task, general-purpose) + */ + agentType: string; + /** + * Whether the agent completed successfully or failed + */ + status: "completed" | "failed"; + /** + * Human-readable description of the agent task + */ + description?: string; + /** + * The full prompt given to the background agent + */ + prompt?: string; + } + | { + type: "shell_completed"; + /** + * Unique identifier of the shell session + */ + shellId: string; + /** + * Exit code of the shell command, if available + */ + exitCode?: number; + /** + * Human-readable description of the command + */ + description?: string; + } + | { + type: "shell_detached_completed"; + /** + * Unique identifier of the detached shell session + */ + shellId: string; + /** + * Human-readable description of the command + */ + description?: string; + }; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 181d1a9615..c8c88d2cdf 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -7,8 +7,8 @@ * @module session */ -import type { MessageConnection } from "vscode-jsonrpc/node"; -import { ConnectionError, ResponseError } from "vscode-jsonrpc/node"; +import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js"; import { createSessionRpc } from "./generated/rpc.js"; import type { MessageOptions, @@ -693,4 +693,27 @@ export class CopilotSession { async setModel(model: string): Promise { await this.rpc.model.switchTo({ modelId: model }); } + + /** + * Log a message to the session timeline. + * The message appears in the session event stream and is visible to SDK consumers + * and (for non-ephemeral messages) persisted to the session event log on disk. + * + * @param message - Human-readable message text + * @param options - Optional log level and ephemeral flag + * + * @example + * ```typescript + * await session.log("Processing started"); + * await session.log("Disk usage high", { level: "warning" }); + * await session.log("Connection failed", { level: "error" }); + * await session.log("Debug info", { ephemeral: true }); + * ``` + */ + async log( + message: string, + options?: { level?: "info" | "warning" | "error"; ephemeral?: boolean } + ): Promise { + await this.rpc.log({ message, ...options }); + } } diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index e988e62c81..7cd781bc2d 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -1,5 +1,5 @@ import { rm } from "fs/promises"; -import { describe, expect, it, onTestFinished } from "vitest"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; import { CopilotClient, approveAll } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; @@ -334,6 +334,57 @@ describe("Sessions", async () => { const assistantMessage = await getFinalAssistantMessage(session); expect(assistantMessage.data.content).toContain("2"); }); + + it("should log messages at all levels and emit matching session events", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const events: Array<{ type: string; id?: string; data?: Record }> = []; + session.on((event) => { + events.push(event as (typeof events)[number]); + }); + + await session.log("Info message"); + await session.log("Warning message", { level: "warning" }); + await session.log("Error message", { level: "error" }); + await session.log("Ephemeral message", { ephemeral: true }); + + await vi.waitFor( + () => { + const notifications = events.filter( + (e) => + e.data && + ("infoType" in e.data || "warningType" in e.data || "errorType" in e.data) + ); + expect(notifications).toHaveLength(4); + }, + { timeout: 10_000 } + ); + + const byMessage = (msg: string) => events.find((e) => e.data?.message === msg)!; + expect(byMessage("Info message").type).toBe("session.info"); + expect(byMessage("Info message").data).toEqual({ + infoType: "notification", + message: "Info message", + }); + + expect(byMessage("Warning message").type).toBe("session.warning"); + expect(byMessage("Warning message").data).toEqual({ + warningType: "notification", + message: "Warning message", + }); + + expect(byMessage("Error message").type).toBe("session.error"); + expect(byMessage("Error message").data).toEqual({ + errorType: "notification", + message: "Error message", + }); + + expect(byMessage("Ephemeral message").type).toBe("session.info"); + expect(byMessage("Ephemeral message").data).toEqual({ + infoType: "notification", + message: "Ephemeral message", + }); + }); }); function getSystemMessage(exchange: ParsedHttpExchange): string | undefined { diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ef188b0955..d5fa7b73b1 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -13,6 +13,7 @@ from typing import Any, TypeVar, cast from collections.abc import Callable from enum import Enum +from uuid import UUID T = TypeVar("T") @@ -465,19 +466,30 @@ def to_dict(self) -> dict: return result +class ReasoningEffort(Enum): + HIGH = "high" + LOW = "low" + MEDIUM = "medium" + XHIGH = "xhigh" + + @dataclass class SessionModelSwitchToParams: model_id: str + reasoning_effort: ReasoningEffort | None = None @staticmethod def from_dict(obj: Any) -> 'SessionModelSwitchToParams': assert isinstance(obj, dict) model_id = from_str(obj.get("modelId")) - return SessionModelSwitchToParams(model_id) + reasoning_effort = from_union([ReasoningEffort, from_none], obj.get("reasoningEffort")) + return SessionModelSwitchToParams(model_id, reasoning_effort) def to_dict(self) -> dict: result: dict = {} result["modelId"] = from_str(self.model_id) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([lambda x: to_enum(ReasoningEffort, x), from_none], self.reasoning_effort) return result @@ -1065,6 +1077,63 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionLogResult: + event_id: UUID + """The unique identifier of the emitted session event""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogResult': + assert isinstance(obj, dict) + event_id = UUID(obj.get("eventId")) + return SessionLogResult(event_id) + + def to_dict(self) -> dict: + result: dict = {} + result["eventId"] = str(self.event_id) + return result + + +class Level(Enum): + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + ERROR = "error" + INFO = "info" + WARNING = "warning" + + +@dataclass +class SessionLogParams: + message: str + """Human-readable message""" + + ephemeral: bool | None = None + """When true, the message is transient and not persisted to the session event log on disk""" + + level: Level | None = None + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogParams': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + ephemeral = from_union([from_bool, from_none], obj.get("ephemeral")) + level = from_union([Level, from_none], obj.get("level")) + return SessionLogParams(message, ephemeral, level) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.ephemeral is not None: + result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral) + if self.level is not None: + result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level) + return result + + def ping_result_from_dict(s: Any) -> PingResult: return PingResult.from_dict(s) @@ -1329,6 +1398,22 @@ def session_permissions_handle_pending_permission_request_params_to_dict(x: Sess return to_class(SessionPermissionsHandlePendingPermissionRequestParams, x) +def session_log_result_from_dict(s: Any) -> SessionLogResult: + return SessionLogResult.from_dict(s) + + +def session_log_result_to_dict(x: SessionLogResult) -> Any: + return to_class(SessionLogResult, x) + + +def session_log_params_from_dict(s: Any) -> SessionLogParams: + return SessionLogParams.from_dict(s) + + +def session_log_params_to_dict(x: SessionLogParams) -> Any: + return to_class(SessionLogParams, x) + + def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" if timeout is not None: @@ -1515,3 +1600,8 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.tools = ToolsApi(client, session_id) self.permissions = PermissionsApi(client, session_id) + async def log(self, params: SessionLogParams, *, timeout: float | None = None) -> SessionLogResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionLogResult.from_dict(await self._client.request("session.log", params_dict, **_timeout_kwargs(timeout))) + diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 1b442530d4..69d07f77b9 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -538,6 +538,83 @@ def to_dict(self) -> dict: return result +class Status(Enum): + """Whether the agent completed successfully or failed""" + + COMPLETED = "completed" + FAILED = "failed" + + +class KindType(Enum): + AGENT_COMPLETED = "agent_completed" + SHELL_COMPLETED = "shell_completed" + SHELL_DETACHED_COMPLETED = "shell_detached_completed" + + +@dataclass +class KindClass: + """Structured metadata identifying what triggered this notification""" + + type: KindType + agent_id: str | None = None + """Unique identifier of the background agent""" + + agent_type: str | None = None + """Type of the agent (e.g., explore, task, general-purpose)""" + + description: str | None = None + """Human-readable description of the agent task + + Human-readable description of the command + """ + prompt: str | None = None + """The full prompt given to the background agent""" + + status: Status | None = None + """Whether the agent completed successfully or failed""" + + exit_code: float | None = None + """Exit code of the shell command, if available""" + + shell_id: str | None = None + """Unique identifier of the shell session + + Unique identifier of the detached shell session + """ + + @staticmethod + def from_dict(obj: Any) -> 'KindClass': + assert isinstance(obj, dict) + type = KindType(obj.get("type")) + agent_id = from_union([from_str, from_none], obj.get("agentId")) + agent_type = from_union([from_str, from_none], obj.get("agentType")) + description = from_union([from_str, from_none], obj.get("description")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + status = from_union([Status, from_none], obj.get("status")) + exit_code = from_union([from_float, from_none], obj.get("exitCode")) + shell_id = from_union([from_str, from_none], obj.get("shellId")) + return KindClass(type, agent_id, agent_type, description, prompt, status, exit_code, shell_id) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(KindType, self.type) + if self.agent_id is not None: + result["agentId"] = from_union([from_str, from_none], self.agent_id) + if self.agent_type is not None: + result["agentType"] = from_union([from_str, from_none], self.agent_type) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(Status, x), from_none], self.status) + if self.exit_code is not None: + result["exitCode"] = from_union([to_float, from_none], self.exit_code) + if self.shell_id is not None: + result["shellId"] = from_union([from_str, from_none], self.shell_id) + return result + + @dataclass class Metadata: """Metadata about the prompt template and its construction""" @@ -1305,6 +1382,7 @@ class Data: Empty payload; the event signals that the custom agent was deselected, returning to the default agent """ + already_in_use: bool | None = None context: ContextClass | str | None = None """Working directory and git context at session start @@ -1583,6 +1661,8 @@ class Data: Full content of the skill file, injected into the conversation for the model The system or developer prompt text + + The notification text, typically wrapped in XML tags """ interaction_id: str | None = None """CAPI interaction ID for correlating this user message with its turn @@ -1793,6 +1873,9 @@ class Data: role: Role | None = None """Message role: "system" for system prompts, "developer" for developer-injected instructions""" + kind: KindClass | None = None + """Structured metadata identifying what triggered this notification""" + permission_request: PermissionRequest | None = None """Details of the permission being requested""" @@ -1826,6 +1909,7 @@ class Data: @staticmethod def from_dict(obj: Any) -> 'Data': assert isinstance(obj, dict) + already_in_use = from_union([from_bool, from_none], obj.get("alreadyInUse")) context = from_union([ContextClass.from_dict, from_str, from_none], obj.get("context")) copilot_version = from_union([from_str, from_none], obj.get("copilotVersion")) producer = from_union([from_str, from_none], obj.get("producer")) @@ -1944,6 +2028,7 @@ def from_dict(obj: Any) -> 'Data': output = obj.get("output") metadata = from_union([Metadata.from_dict, from_none], obj.get("metadata")) role = from_union([Role, from_none], obj.get("role")) + kind = from_union([KindClass.from_dict, from_none], obj.get("kind")) permission_request = from_union([PermissionRequest.from_dict, from_none], obj.get("permissionRequest")) allow_freeform = from_union([from_bool, from_none], obj.get("allowFreeform")) choices = from_union([lambda x: from_list(from_str, x), from_none], obj.get("choices")) @@ -1954,10 +2039,12 @@ def from_dict(obj: Any) -> 'Data': actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions")) plan_content = from_union([from_str, from_none], obj.get("planContent")) recommended_action = from_union([from_str, from_none], obj.get("recommendedAction")) - return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, permission_request, allow_freeform, choices, question, mode, requested_schema, command, actions, plan_content, recommended_action) + return Data(already_in_use, context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, mode, requested_schema, command, actions, plan_content, recommended_action) def to_dict(self) -> dict: result: dict = {} + if self.already_in_use is not None: + result["alreadyInUse"] = from_union([from_bool, from_none], self.already_in_use) if self.context is not None: result["context"] = from_union([lambda x: to_class(ContextClass, x), from_str, from_none], self.context) if self.copilot_version is not None: @@ -2194,6 +2281,8 @@ def to_dict(self) -> dict: result["metadata"] = from_union([lambda x: to_class(Metadata, x), from_none], self.metadata) if self.role is not None: result["role"] = from_union([lambda x: to_enum(Role, x), from_none], self.role) + if self.kind is not None: + result["kind"] = from_union([lambda x: to_class(KindClass, x), from_none], self.kind) if self.permission_request is not None: result["permissionRequest"] = from_union([lambda x: to_class(PermissionRequest, x), from_none], self.permission_request) if self.allow_freeform is not None: @@ -2268,6 +2357,7 @@ class SessionEventType(Enum): SUBAGENT_SELECTED = "subagent.selected" SUBAGENT_STARTED = "subagent.started" SYSTEM_MESSAGE = "system.message" + SYSTEM_NOTIFICATION = "system.notification" TOOL_EXECUTION_COMPLETE = "tool.execution_complete" TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result" TOOL_EXECUTION_PROGRESS = "tool.execution_progress" diff --git a/python/copilot/session.py b/python/copilot/session.py index e0e72fc68d..ee46cbd7b5 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -13,7 +13,9 @@ from .generated.rpc import ( Kind, + Level, ResultResult, + SessionLogParams, SessionModelSwitchToParams, SessionPermissionsHandlePendingPermissionRequestParams, SessionPermissionsHandlePendingPermissionRequestParamsResult, @@ -733,3 +735,37 @@ async def set_model(self, model: str) -> None: >>> await session.set_model("gpt-4.1") """ await self.rpc.model.switch_to(SessionModelSwitchToParams(model_id=model)) + + async def log( + self, + message: str, + *, + level: str | None = None, + ephemeral: bool | None = None, + ) -> None: + """ + Log a message to the session timeline. + + The message appears in the session event stream and is visible to SDK consumers + and (for non-ephemeral messages) persisted to the session event log on disk. + + Args: + message: The human-readable message to log. + level: Log severity level ("info", "warning", "error"). Defaults to "info". + ephemeral: When True, the message is transient and not persisted to disk. + + Raises: + Exception: If the session has been destroyed or the connection fails. + + Example: + >>> await session.log("Processing started") + >>> await session.log("Something looks off", level="warning") + >>> await session.log("Operation failed", level="error") + >>> await session.log("Temporary status update", ephemeral=True) + """ + params = SessionLogParams( + message=message, + level=Level(level) if level is not None else None, + ephemeral=ephemeral, + ) + await self.rpc.log(params) diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 60cb7c875b..aa93ed42df 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -501,6 +501,49 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo assistant_message = await get_final_assistant_message(session) assert "2" in assistant_message.data.content + async def test_session_log_emits_events_at_all_levels(self, ctx: E2ETestContext): + import asyncio + + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + received_events = [] + + def on_event(event): + if event.type.value in ("session.info", "session.warning", "session.error"): + received_events.append(event) + + session.on(on_event) + + await session.log("Info message") + await session.log("Warning message", level="warning") + await session.log("Error message", level="error") + await session.log("Ephemeral message", ephemeral=True) + + # Poll until all 4 notification events arrive + deadline = asyncio.get_event_loop().time() + 10 + while len(received_events) < 4: + if asyncio.get_event_loop().time() > deadline: + pytest.fail( + f"Timed out waiting for 4 notification events, got {len(received_events)}" + ) + await asyncio.sleep(0.1) + + by_message = {e.data.message: e for e in received_events} + + assert by_message["Info message"].type.value == "session.info" + assert by_message["Info message"].data.info_type == "notification" + + assert by_message["Warning message"].type.value == "session.warning" + assert by_message["Warning message"].data.warning_type == "notification" + + assert by_message["Error message"].type.value == "session.error" + assert by_message["Error message"].data.error_type == "notification" + + assert by_message["Ephemeral message"].type.value == "session.info" + assert by_message["Ephemeral message"].data.info_type == "notification" + def _get_system_message(exchange: dict) -> str: messages = exchange.get("request", {}).get("messages", []) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index af5fb78a67..c72eb06df5 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -701,12 +701,21 @@ function emitServerInstanceMethod( function emitSessionRpcClasses(node: Record, classes: string[]): string[] { const result: string[] = []; const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); + const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); const srLines = [`/// Typed session-scoped RPC methods.`, `public class SessionRpc`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; srLines.push(` internal SessionRpc(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`); for (const [groupName] of groups) srLines.push(` ${toPascalCase(groupName)} = new ${toPascalCase(groupName)}Api(rpc, sessionId);`); srLines.push(` }`); for (const [groupName] of groups) srLines.push("", ` public ${toPascalCase(groupName)}Api ${toPascalCase(groupName)} { get; }`); + + // Emit top-level session RPC methods directly on the SessionRpc class + const topLevelLines: string[] = []; + for (const [key, value] of topLevelMethods) { + emitSessionMethod(key, value as RpcMethod, topLevelLines, classes, " "); + } + srLines.push(...topLevelLines); + srLines.push(`}`); result.push(srLines.join("\n")); @@ -716,50 +725,53 @@ function emitSessionRpcClasses(node: Record, classes: string[]) return result; } +function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string): void { + const methodName = toPascalCase(key); + const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; + const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); + if (resultClass) classes.push(resultClass); + + const paramEntries = (method.params?.properties ? Object.entries(method.params.properties) : []).filter(([k]) => k !== "sessionId"); + const requiredSet = new Set(method.params?.required || []); + + // Sort so required params come before optional (C# requires defaults at end) + paramEntries.sort((a, b) => { + const aReq = requiredSet.has(a[0]) ? 0 : 1; + const bReq = requiredSet.has(b[0]) ? 0 : 1; + return aReq - bReq; + }); + + const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; + if (method.params) { + const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); + if (reqClass) classes.push(reqClass); + } + + lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); + const sigParams: string[] = []; + const bodyAssignments = [`SessionId = _sessionId`]; + + for (const [pName, pSchema] of paramEntries) { + if (typeof pSchema !== "object") continue; + const isReq = requiredSet.has(pName); + const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); + sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); + bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); + } + sigParams.push("CancellationToken cancellationToken = default"); + + lines.push(`${indent}public async Task<${resultClassName}> ${methodName}Async(${sigParams.join(", ")})`); + lines.push(`${indent}{`, `${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`); + lines.push(`${indent} return await CopilotClient.InvokeRpcAsync<${resultClassName}>(_rpc, "${method.rpcMethod}", [request], cancellationToken);`, `${indent}}`); +} + function emitSessionApiClass(className: string, node: Record, classes: string[]): string { const lines = [`public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; lines.push(` internal ${className}(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`, ` }`); for (const [key, value] of Object.entries(node)) { if (!isRpcMethod(value)) continue; - const method = value; - const methodName = toPascalCase(key); - const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; - const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); - if (resultClass) classes.push(resultClass); - - const paramEntries = (method.params?.properties ? Object.entries(method.params.properties) : []).filter(([k]) => k !== "sessionId"); - const requiredSet = new Set(method.params?.required || []); - - // Sort so required params come before optional (C# requires defaults at end) - paramEntries.sort((a, b) => { - const aReq = requiredSet.has(a[0]) ? 0 : 1; - const bReq = requiredSet.has(b[0]) ? 0 : 1; - return aReq - bReq; - }); - - const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; - if (method.params) { - const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); - if (reqClass) classes.push(reqClass); - } - - lines.push("", ` /// Calls "${method.rpcMethod}".`); - const sigParams: string[] = []; - const bodyAssignments = [`SessionId = _sessionId`]; - - for (const [pName, pSchema] of paramEntries) { - if (typeof pSchema !== "object") continue; - const isReq = requiredSet.has(pName); - const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); - sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); - } - sigParams.push("CancellationToken cancellationToken = default"); - - lines.push(` public async Task<${resultClassName}> ${methodName}Async(${sigParams.join(", ")})`); - lines.push(` {`, ` var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`); - lines.push(` return await CopilotClient.InvokeRpcAsync<${resultClassName}>(_rpc, "${method.rpcMethod}", [request], cancellationToken);`, ` }`); + emitSessionMethod(key, value, lines, classes, " "); } lines.push(`}`); return lines.join("\n"); From 87968ce44dbb7ffe0b67719528405c1c97b5c561 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Mon, 9 Mar 2026 20:27:36 -0700 Subject: [PATCH 16/61] Fix link in docs (#774) #741 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b4770ed0be..37513bde10 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Yes, the GitHub Copilot SDK allows you to define custom agents, skills, and tool ### Are there instructions for Copilot to speed up development with the SDK? -Yes, check out the custom instructions at [`github/awesome-copilot`](https://github.com/github/awesome-copilot/blob/main/collections/copilot-sdk.md). +Yes, check out the custom instructions at [`github/awesome-copilot`](https://github.com/github/awesome-copilot/tree/main/instructions). ### What models are supported? From 0bb8a8138c6c8e1916bdf595f6015d23b20b3b98 Mon Sep 17 00:00:00 2001 From: kirankashyap <46650420+kirankashyap@users.noreply.github.com> Date: Tue, 10 Mar 2026 08:58:33 +0530 Subject: [PATCH 17/61] docs: add per-language custom instruction links to FAQ (#740) Add links to the language-specific Copilot custom instructions for Node.js, Python, .NET, and Go under the FAQ section. Also removes a broken collection link. Co-authored-by: Kiran Kashyap Co-authored-by: Patrick Nikoletich --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37513bde10..d4899588a6 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,12 @@ Yes, the GitHub Copilot SDK allows you to define custom agents, skills, and tool ### Are there instructions for Copilot to speed up development with the SDK? -Yes, check out the custom instructions at [`github/awesome-copilot`](https://github.com/github/awesome-copilot/tree/main/instructions). +Yes, check out the custom instructions for each SDK: + +- **[Node.js / TypeScript](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-nodejs.instructions.md)** +- **[Python](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-python.instructions.md)** +- **[.NET](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-csharp.instructions.md)** +- **[Go](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-go.instructions.md)** ### What models are supported? From 2b3337a63afa683d756530bc493e4262770b7c18 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 9 Mar 2026 23:29:06 -0400 Subject: [PATCH 18/61] Emit XML doc comments from schema descriptions in C# codegen (#724) Update the C# code generator (csharp.ts) to emit /// XML doc comments on all generated types and members, sourced from JSON Schema description annotations. Changes to scripts/codegen/csharp.ts: - Add escapeXml(), ensureTrailingPunctuation(), xmlDocComment(), rawXmlDocSummary(), and xmlDocCommentWithFallback() helpers - Emit on all data classes, nested classes, polymorphic base and derived classes, enum types, and enum members - Emit with event name on SessionEvent-derived classes when a real schema description is present - Emit on all override string Type properties - Emit cross-references in data class fallback summaries - Use tags for event names and discriminator values - Ensure all comments end with sentence-ending punctuation - Add synthetic fallback summaries for types without schema descriptions - Apply XML escaping to schema-sourced text; skip escaping for codegen-controlled XML tags Regenerated output (from published @github/copilot v1.0.2 schemas): - SessionEvents.cs: ~525 + 69 comments - Rpc.cs: class-level and property-level docs with fallbacks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 248 +++++-- dotnet/src/Generated/SessionEvents.cs | 899 ++++++++++++++++++++------ scripts/codegen/csharp.ts | 151 ++++- 3 files changed, 1030 insertions(+), 268 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 01911d5896..7cc6bdacaf 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5,245 +5,281 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json -// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. -#pragma warning disable CS1591 - using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; namespace GitHub.Copilot.SDK.Rpc; +/// RPC data type for Ping operations. public class PingResult { - /// Echoed message (or default greeting) + /// Echoed message (or default greeting). [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; - /// Server timestamp in milliseconds + /// Server timestamp in milliseconds. [JsonPropertyName("timestamp")] public double Timestamp { get; set; } - /// Server protocol version number + /// Server protocol version number. [JsonPropertyName("protocolVersion")] public double ProtocolVersion { get; set; } } +/// RPC data type for Ping operations. internal class PingRequest { + /// Optional message to echo back. [JsonPropertyName("message")] public string? Message { get; set; } } +/// RPC data type for ModelCapabilitiesSupports operations. public class ModelCapabilitiesSupports { + /// Gets or sets the vision value. [JsonPropertyName("vision")] public bool? Vision { get; set; } - /// Whether this model supports reasoning effort configuration + /// Whether this model supports reasoning effort configuration. [JsonPropertyName("reasoningEffort")] public bool? ReasoningEffort { get; set; } } +/// RPC data type for ModelCapabilitiesLimits operations. public class ModelCapabilitiesLimits { + /// Gets or sets the max_prompt_tokens value. [JsonPropertyName("max_prompt_tokens")] public double? MaxPromptTokens { get; set; } + /// Gets or sets the max_output_tokens value. [JsonPropertyName("max_output_tokens")] public double? MaxOutputTokens { get; set; } + /// Gets or sets the max_context_window_tokens value. [JsonPropertyName("max_context_window_tokens")] public double MaxContextWindowTokens { get; set; } } -/// Model capabilities and limits +/// Model capabilities and limits. public class ModelCapabilities { + /// Gets or sets the supports value. [JsonPropertyName("supports")] public ModelCapabilitiesSupports Supports { get; set; } = new(); + /// Gets or sets the limits value. [JsonPropertyName("limits")] public ModelCapabilitiesLimits Limits { get; set; } = new(); } -/// Policy state (if applicable) +/// Policy state (if applicable). public class ModelPolicy { + /// Gets or sets the state value. [JsonPropertyName("state")] public string State { get; set; } = string.Empty; + /// Gets or sets the terms value. [JsonPropertyName("terms")] public string Terms { get; set; } = string.Empty; } -/// Billing information +/// Billing information. public class ModelBilling { + /// Gets or sets the multiplier value. [JsonPropertyName("multiplier")] public double Multiplier { get; set; } } +/// RPC data type for Model operations. public class Model { - /// Model identifier (e.g., "claude-sonnet-4.5") + /// Model identifier (e.g., "claude-sonnet-4.5"). [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Display name + /// Display name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Model capabilities and limits + /// Model capabilities and limits. [JsonPropertyName("capabilities")] public ModelCapabilities Capabilities { get; set; } = new(); - /// Policy state (if applicable) + /// Policy state (if applicable). [JsonPropertyName("policy")] public ModelPolicy? Policy { get; set; } - /// Billing information + /// Billing information. [JsonPropertyName("billing")] public ModelBilling? Billing { get; set; } - /// Supported reasoning effort levels (only present if model supports reasoning effort) + /// Supported reasoning effort levels (only present if model supports reasoning effort). [JsonPropertyName("supportedReasoningEfforts")] public List? SupportedReasoningEfforts { get; set; } - /// Default reasoning effort level (only present if model supports reasoning effort) + /// Default reasoning effort level (only present if model supports reasoning effort). [JsonPropertyName("defaultReasoningEffort")] public string? DefaultReasoningEffort { get; set; } } +/// RPC data type for ModelsList operations. public class ModelsListResult { - /// List of available models with full metadata + /// List of available models with full metadata. [JsonPropertyName("models")] public List Models { get; set; } = []; } +/// RPC data type for Tool operations. public class Tool { - /// Tool identifier (e.g., "bash", "grep", "str_replace_editor") + /// Tool identifier (e.g., "bash", "grep", "str_replace_editor"). [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP tools) + /// Optional namespaced name for declarative filtering (e.g., "playwright/navigate" for MCP tools). [JsonPropertyName("namespacedName")] public string? NamespacedName { get; set; } - /// Description of what the tool does + /// Description of what the tool does. [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; - /// JSON Schema for the tool's input parameters + /// JSON Schema for the tool's input parameters. [JsonPropertyName("parameters")] public Dictionary? Parameters { get; set; } - /// Optional instructions for how to use this tool effectively + /// Optional instructions for how to use this tool effectively. [JsonPropertyName("instructions")] public string? Instructions { get; set; } } +/// RPC data type for ToolsList operations. public class ToolsListResult { - /// List of available built-in tools with metadata + /// List of available built-in tools with metadata. [JsonPropertyName("tools")] public List Tools { get; set; } = []; } +/// RPC data type for ToolsList operations. internal class ToolsListRequest { + /// Optional model ID — when provided, the returned tool list reflects model-specific overrides. [JsonPropertyName("model")] public string? Model { get; set; } } +/// RPC data type for AccountGetQuotaResultQuotaSnapshotsValue operations. public class AccountGetQuotaResultQuotaSnapshotsValue { - /// Number of requests included in the entitlement + /// Number of requests included in the entitlement. [JsonPropertyName("entitlementRequests")] public double EntitlementRequests { get; set; } - /// Number of requests used so far this period + /// Number of requests used so far this period. [JsonPropertyName("usedRequests")] public double UsedRequests { get; set; } - /// Percentage of entitlement remaining + /// Percentage of entitlement remaining. [JsonPropertyName("remainingPercentage")] public double RemainingPercentage { get; set; } - /// Number of overage requests made this period + /// Number of overage requests made this period. [JsonPropertyName("overage")] public double Overage { get; set; } - /// Whether pay-per-request usage is allowed when quota is exhausted + /// Whether pay-per-request usage is allowed when quota is exhausted. [JsonPropertyName("overageAllowedWithExhaustedQuota")] public bool OverageAllowedWithExhaustedQuota { get; set; } - /// Date when the quota resets (ISO 8601) + /// Date when the quota resets (ISO 8601). [JsonPropertyName("resetDate")] public string? ResetDate { get; set; } } +/// RPC data type for AccountGetQuota operations. public class AccountGetQuotaResult { - /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) + /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions). [JsonPropertyName("quotaSnapshots")] public Dictionary QuotaSnapshots { get; set; } = []; } +/// RPC data type for SessionLog operations. public class SessionLogResult { - /// The unique identifier of the emitted session event + /// The unique identifier of the emitted session event. [JsonPropertyName("eventId")] public Guid EventId { get; set; } } +/// RPC data type for SessionLog operations. internal class SessionLogRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Human-readable message. [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; + /// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". [JsonPropertyName("level")] public SessionLogRequestLevel? Level { get; set; } + /// When true, the message is transient and not persisted to the session event log on disk. [JsonPropertyName("ephemeral")] public bool? Ephemeral { get; set; } } +/// RPC data type for SessionModelGetCurrent operations. public class SessionModelGetCurrentResult { + /// Gets or sets the modelId value. [JsonPropertyName("modelId")] public string? ModelId { get; set; } } +/// RPC data type for SessionModelGetCurrent operations. internal class SessionModelGetCurrentRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionModelSwitchTo operations. public class SessionModelSwitchToResult { + /// Gets or sets the modelId value. [JsonPropertyName("modelId")] public string? ModelId { get; set; } } +/// RPC data type for SessionModelSwitchTo operations. internal class SessionModelSwitchToRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Gets or sets the modelId value. [JsonPropertyName("modelId")] public string ModelId { get; set; } = string.Empty; + /// Gets or sets the reasoningEffort value. [JsonPropertyName("reasoningEffort")] public SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; } } +/// RPC data type for SessionModeGet operations. public class SessionModeGetResult { /// The current agent mode. @@ -251,12 +287,15 @@ public class SessionModeGetResult public SessionModeGetResultMode Mode { get; set; } } +/// RPC data type for SessionModeGet operations. internal class SessionModeGetRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionModeSet operations. public class SessionModeSetResult { /// The agent mode after switching. @@ -264,317 +303,390 @@ public class SessionModeSetResult public SessionModeGetResultMode Mode { get; set; } } +/// RPC data type for SessionModeSet operations. internal class SessionModeSetRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// The mode to switch to. Valid values: "interactive", "plan", "autopilot". [JsonPropertyName("mode")] public SessionModeGetResultMode Mode { get; set; } } +/// RPC data type for SessionPlanRead operations. public class SessionPlanReadResult { - /// Whether the plan file exists in the workspace + /// Whether the plan file exists in the workspace. [JsonPropertyName("exists")] public bool Exists { get; set; } - /// The content of the plan file, or null if it does not exist + /// The content of the plan file, or null if it does not exist. [JsonPropertyName("content")] public string? Content { get; set; } - /// Absolute file path of the plan file, or null if workspace is not enabled + /// Absolute file path of the plan file, or null if workspace is not enabled. [JsonPropertyName("path")] public string? Path { get; set; } } +/// RPC data type for SessionPlanRead operations. internal class SessionPlanReadRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionPlanUpdate operations. public class SessionPlanUpdateResult { } +/// RPC data type for SessionPlanUpdate operations. internal class SessionPlanUpdateRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// The new content for the plan file. [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; } +/// RPC data type for SessionPlanDelete operations. public class SessionPlanDeleteResult { } +/// RPC data type for SessionPlanDelete operations. internal class SessionPlanDeleteRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionWorkspaceListFiles operations. public class SessionWorkspaceListFilesResult { - /// Relative file paths in the workspace files directory + /// Relative file paths in the workspace files directory. [JsonPropertyName("files")] public List Files { get; set; } = []; } +/// RPC data type for SessionWorkspaceListFiles operations. internal class SessionWorkspaceListFilesRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionWorkspaceReadFile operations. public class SessionWorkspaceReadFileResult { - /// File content as a UTF-8 string + /// File content as a UTF-8 string. [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; } +/// RPC data type for SessionWorkspaceReadFile operations. internal class SessionWorkspaceReadFileRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Relative path within the workspace files directory. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; } +/// RPC data type for SessionWorkspaceCreateFile operations. public class SessionWorkspaceCreateFileResult { } +/// RPC data type for SessionWorkspaceCreateFile operations. internal class SessionWorkspaceCreateFileRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Relative path within the workspace files directory. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; + /// File content to write as a UTF-8 string. [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; } +/// RPC data type for SessionFleetStart operations. public class SessionFleetStartResult { - /// Whether fleet mode was successfully activated + /// Whether fleet mode was successfully activated. [JsonPropertyName("started")] public bool Started { get; set; } } +/// RPC data type for SessionFleetStart operations. internal class SessionFleetStartRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Optional user prompt to combine with fleet instructions. [JsonPropertyName("prompt")] public string? Prompt { get; set; } } +/// RPC data type for Agent operations. public class Agent { - /// Unique identifier of the custom agent + /// Unique identifier of the custom agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Human-readable display name + /// Human-readable display name. [JsonPropertyName("displayName")] public string DisplayName { get; set; } = string.Empty; - /// Description of the agent's purpose + /// Description of the agent's purpose. [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; } +/// RPC data type for SessionAgentList operations. public class SessionAgentListResult { - /// Available custom agents + /// Available custom agents. [JsonPropertyName("agents")] public List Agents { get; set; } = []; } +/// RPC data type for SessionAgentList operations. internal class SessionAgentListRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionAgentGetCurrentResultAgent operations. public class SessionAgentGetCurrentResultAgent { - /// Unique identifier of the custom agent + /// Unique identifier of the custom agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Human-readable display name + /// Human-readable display name. [JsonPropertyName("displayName")] public string DisplayName { get; set; } = string.Empty; - /// Description of the agent's purpose + /// Description of the agent's purpose. [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; } +/// RPC data type for SessionAgentGetCurrent operations. public class SessionAgentGetCurrentResult { - /// Currently selected custom agent, or null if using the default agent + /// Currently selected custom agent, or null if using the default agent. [JsonPropertyName("agent")] public SessionAgentGetCurrentResultAgent? Agent { get; set; } } +/// RPC data type for SessionAgentGetCurrent operations. internal class SessionAgentGetCurrentRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The newly selected custom agent +/// The newly selected custom agent. public class SessionAgentSelectResultAgent { - /// Unique identifier of the custom agent + /// Unique identifier of the custom agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Human-readable display name + /// Human-readable display name. [JsonPropertyName("displayName")] public string DisplayName { get; set; } = string.Empty; - /// Description of the agent's purpose + /// Description of the agent's purpose. [JsonPropertyName("description")] public string Description { get; set; } = string.Empty; } +/// RPC data type for SessionAgentSelect operations. public class SessionAgentSelectResult { - /// The newly selected custom agent + /// The newly selected custom agent. [JsonPropertyName("agent")] public SessionAgentSelectResultAgent Agent { get; set; } = new(); } +/// RPC data type for SessionAgentSelect operations. internal class SessionAgentSelectRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Name of the custom agent to select. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; } +/// RPC data type for SessionAgentDeselect operations. public class SessionAgentDeselectResult { } +/// RPC data type for SessionAgentDeselect operations. internal class SessionAgentDeselectRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionCompactionCompact operations. public class SessionCompactionCompactResult { - /// Whether compaction completed successfully + /// Whether compaction completed successfully. [JsonPropertyName("success")] public bool Success { get; set; } - /// Number of tokens freed by compaction + /// Number of tokens freed by compaction. [JsonPropertyName("tokensRemoved")] public double TokensRemoved { get; set; } - /// Number of messages removed during compaction + /// Number of messages removed during compaction. [JsonPropertyName("messagesRemoved")] public double MessagesRemoved { get; set; } } +/// RPC data type for SessionCompactionCompact operations. internal class SessionCompactionCompactRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionToolsHandlePendingToolCall operations. public class SessionToolsHandlePendingToolCallResult { + /// Gets or sets the success value. [JsonPropertyName("success")] public bool Success { get; set; } } +/// RPC data type for SessionToolsHandlePendingToolCall operations. internal class SessionToolsHandlePendingToolCallRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Gets or sets the requestId value. [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; + /// Gets or sets the result value. [JsonPropertyName("result")] public object? Result { get; set; } + /// Gets or sets the error value. [JsonPropertyName("error")] public string? Error { get; set; } } +/// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations. public class SessionPermissionsHandlePendingPermissionRequestResult { + /// Gets or sets the success value. [JsonPropertyName("success")] public bool Success { get; set; } } +/// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations. internal class SessionPermissionsHandlePendingPermissionRequestRequest { + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; + /// Gets or sets the requestId value. [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; + /// Gets or sets the result value. [JsonPropertyName("result")] public object Result { get; set; } = null!; } +/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionLogRequestLevel { + /// The info variant. [JsonStringEnumMemberName("info")] Info, + /// The warning variant. [JsonStringEnumMemberName("warning")] Warning, + /// The error variant. [JsonStringEnumMemberName("error")] Error, } +/// Defines the allowed values. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModelSwitchToRequestReasoningEffort { + /// The low variant. [JsonStringEnumMemberName("low")] Low, + /// The medium variant. [JsonStringEnumMemberName("medium")] Medium, + /// The high variant. [JsonStringEnumMemberName("high")] High, + /// The xhigh variant. [JsonStringEnumMemberName("xhigh")] Xhigh, } +/// The current agent mode. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode { + /// The interactive variant. [JsonStringEnumMemberName("interactive")] Interactive, + /// The plan variant. [JsonStringEnumMemberName("plan")] Plan, + /// The autopilot variant. [JsonStringEnumMemberName("autopilot")] Autopilot, } -/// Typed server-scoped RPC methods (no session required). +/// Provides server-scoped RPC methods (no session required). public class ServerRpc { private readonly JsonRpc _rpc; @@ -604,7 +716,7 @@ public async Task PingAsync(string? message = null, CancellationToke public ServerAccountApi Account { get; } } -/// Server-scoped Models APIs. +/// Provides server-scoped Models APIs. public class ServerModelsApi { private readonly JsonRpc _rpc; @@ -621,7 +733,7 @@ public async Task ListAsync(CancellationToken cancellationToke } } -/// Server-scoped Tools APIs. +/// Provides server-scoped Tools APIs. public class ServerToolsApi { private readonly JsonRpc _rpc; @@ -639,7 +751,7 @@ public async Task ListAsync(string? model = null, CancellationT } } -/// Server-scoped Account APIs. +/// Provides server-scoped Account APIs. public class ServerAccountApi { private readonly JsonRpc _rpc; @@ -656,7 +768,7 @@ public async Task GetQuotaAsync(CancellationToken cancell } } -/// Typed session-scoped RPC methods. +/// Provides typed session-scoped RPC methods. public class SessionRpc { private readonly JsonRpc _rpc; @@ -677,22 +789,31 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Permissions = new PermissionsApi(rpc, sessionId); } + /// Model APIs. public ModelApi Model { get; } + /// Mode APIs. public ModeApi Mode { get; } + /// Plan APIs. public PlanApi Plan { get; } + /// Workspace APIs. public WorkspaceApi Workspace { get; } + /// Fleet APIs. public FleetApi Fleet { get; } + /// Agent APIs. public AgentApi Agent { get; } + /// Compaction APIs. public CompactionApi Compaction { get; } + /// Tools APIs. public ToolsApi Tools { get; } + /// Permissions APIs. public PermissionsApi Permissions { get; } /// Calls "session.log". @@ -703,6 +824,7 @@ public async Task LogAsync(string message, SessionLogRequestLe } } +/// Provides session-scoped Model APIs. public class ModelApi { private readonly JsonRpc _rpc; @@ -729,6 +851,7 @@ public async Task SwitchToAsync(string modelId, Sess } } +/// Provides session-scoped Mode APIs. public class ModeApi { private readonly JsonRpc _rpc; @@ -755,6 +878,7 @@ public async Task SetAsync(SessionModeGetResultMode mode, } } +/// Provides session-scoped Plan APIs. public class PlanApi { private readonly JsonRpc _rpc; @@ -788,6 +912,7 @@ public async Task DeleteAsync(CancellationToken cancell } } +/// Provides session-scoped Workspace APIs. public class WorkspaceApi { private readonly JsonRpc _rpc; @@ -821,6 +946,7 @@ public async Task CreateFileAsync(string path, } } +/// Provides session-scoped Fleet APIs. public class FleetApi { private readonly JsonRpc _rpc; @@ -840,6 +966,7 @@ public async Task StartAsync(string? prompt = null, Can } } +/// Provides session-scoped Agent APIs. public class AgentApi { private readonly JsonRpc _rpc; @@ -880,6 +1007,7 @@ public async Task DeselectAsync(CancellationToken ca } } +/// Provides session-scoped Compaction APIs. public class CompactionApi { private readonly JsonRpc _rpc; @@ -899,6 +1027,7 @@ public async Task CompactAsync(CancellationToken } } +/// Provides session-scoped Tools APIs. public class ToolsApi { private readonly JsonRpc _rpc; @@ -918,6 +1047,7 @@ public async Task HandlePendingToolCall } } +/// Provides session-scoped Permissions APIs. public class PermissionsApi { private readonly JsonRpc _rpc; diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 5bdf50df00..6648bd189b 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -5,16 +5,13 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json -// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. -#pragma warning disable CS1591 - using System.Text.Json; using System.Text.Json.Serialization; namespace GitHub.Copilot.SDK; /// -/// Base class for all session events with polymorphic JSON serialization. +/// Provides the base class from which all session events derive. /// [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", @@ -80,15 +77,19 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(UserMessageEvent), "user.message")] public abstract partial class SessionEvent { + /// Unique event identifier (UUID v4), generated when the event is emitted. [JsonPropertyName("id")] public Guid Id { get; set; } + /// ISO 8601 timestamp when the event was created. [JsonPropertyName("timestamp")] public DateTimeOffset Timestamp { get; set; } + /// ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. [JsonPropertyName("parentId")] public Guid? ParentId { get; set; } + /// When true, the event is transient and not persisted to the session event log on disk. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("ephemeral")] public bool? Ephemeral { get; set; } @@ -99,1807 +100,2153 @@ public abstract partial class SessionEvent [JsonIgnore] public abstract string Type { get; } + /// Deserializes a JSON string into a . public static SessionEvent FromJson(string json) => JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!; + /// Serializes this event to a JSON string. public string ToJson() => JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent); } -/// -/// Event: session.start -/// +/// Represents the session.start event. public partial class SessionStartEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.start"; + /// The session.start event payload. [JsonPropertyName("data")] public required SessionStartData Data { get; set; } } -/// -/// Event: session.resume -/// +/// Represents the session.resume event. public partial class SessionResumeEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.resume"; + /// The session.resume event payload. [JsonPropertyName("data")] public required SessionResumeData Data { get; set; } } -/// -/// Event: session.error -/// +/// Represents the session.error event. public partial class SessionErrorEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.error"; + /// The session.error event payload. [JsonPropertyName("data")] public required SessionErrorData Data { get; set; } } -/// -/// Event: session.idle -/// +/// Payload indicating the agent is idle; includes any background tasks still in flight. +/// Represents the session.idle event. public partial class SessionIdleEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.idle"; + /// The session.idle event payload. [JsonPropertyName("data")] public required SessionIdleData Data { get; set; } } -/// -/// Event: session.title_changed -/// +/// Represents the session.title_changed event. public partial class SessionTitleChangedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.title_changed"; + /// The session.title_changed event payload. [JsonPropertyName("data")] public required SessionTitleChangedData Data { get; set; } } -/// -/// Event: session.info -/// +/// Represents the session.info event. public partial class SessionInfoEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.info"; + /// The session.info event payload. [JsonPropertyName("data")] public required SessionInfoData Data { get; set; } } -/// -/// Event: session.warning -/// +/// Represents the session.warning event. public partial class SessionWarningEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.warning"; + /// The session.warning event payload. [JsonPropertyName("data")] public required SessionWarningData Data { get; set; } } -/// -/// Event: session.model_change -/// +/// Represents the session.model_change event. public partial class SessionModelChangeEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.model_change"; + /// The session.model_change event payload. [JsonPropertyName("data")] public required SessionModelChangeData Data { get; set; } } -/// -/// Event: session.mode_changed -/// +/// Represents the session.mode_changed event. public partial class SessionModeChangedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.mode_changed"; + /// The session.mode_changed event payload. [JsonPropertyName("data")] public required SessionModeChangedData Data { get; set; } } -/// -/// Event: session.plan_changed -/// +/// Represents the session.plan_changed event. public partial class SessionPlanChangedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.plan_changed"; + /// The session.plan_changed event payload. [JsonPropertyName("data")] public required SessionPlanChangedData Data { get; set; } } -/// -/// Event: session.workspace_file_changed -/// +/// Represents the session.workspace_file_changed event. public partial class SessionWorkspaceFileChangedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.workspace_file_changed"; + /// The session.workspace_file_changed event payload. [JsonPropertyName("data")] public required SessionWorkspaceFileChangedData Data { get; set; } } -/// -/// Event: session.handoff -/// +/// Represents the session.handoff event. public partial class SessionHandoffEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.handoff"; + /// The session.handoff event payload. [JsonPropertyName("data")] public required SessionHandoffData Data { get; set; } } -/// -/// Event: session.truncation -/// +/// Represents the session.truncation event. public partial class SessionTruncationEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.truncation"; + /// The session.truncation event payload. [JsonPropertyName("data")] public required SessionTruncationData Data { get; set; } } -/// -/// Event: session.snapshot_rewind -/// +/// Represents the session.snapshot_rewind event. public partial class SessionSnapshotRewindEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.snapshot_rewind"; + /// The session.snapshot_rewind event payload. [JsonPropertyName("data")] public required SessionSnapshotRewindData Data { get; set; } } -/// -/// Event: session.shutdown -/// +/// Represents the session.shutdown event. public partial class SessionShutdownEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.shutdown"; + /// The session.shutdown event payload. [JsonPropertyName("data")] public required SessionShutdownData Data { get; set; } } -/// -/// Event: session.context_changed -/// +/// Represents the session.context_changed event. public partial class SessionContextChangedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.context_changed"; + /// The session.context_changed event payload. [JsonPropertyName("data")] public required SessionContextChangedData Data { get; set; } } -/// -/// Event: session.usage_info -/// +/// Represents the session.usage_info event. public partial class SessionUsageInfoEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.usage_info"; + /// The session.usage_info event payload. [JsonPropertyName("data")] public required SessionUsageInfoData Data { get; set; } } -/// -/// Event: session.compaction_start -/// +/// Empty payload; the event signals that LLM-powered conversation compaction has begun. +/// Represents the session.compaction_start event. public partial class SessionCompactionStartEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.compaction_start"; + /// The session.compaction_start event payload. [JsonPropertyName("data")] public required SessionCompactionStartData Data { get; set; } } -/// -/// Event: session.compaction_complete -/// +/// Represents the session.compaction_complete event. public partial class SessionCompactionCompleteEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.compaction_complete"; + /// The session.compaction_complete event payload. [JsonPropertyName("data")] public required SessionCompactionCompleteData Data { get; set; } } -/// -/// Event: session.task_complete -/// +/// Represents the session.task_complete event. public partial class SessionTaskCompleteEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "session.task_complete"; + /// The session.task_complete event payload. [JsonPropertyName("data")] public required SessionTaskCompleteData Data { get; set; } } -/// -/// Event: user.message -/// +/// Represents the user.message event. public partial class UserMessageEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "user.message"; + /// The user.message event payload. [JsonPropertyName("data")] public required UserMessageData Data { get; set; } } -/// -/// Event: pending_messages.modified -/// +/// Empty payload; the event signals that the pending message queue has changed. +/// Represents the pending_messages.modified event. public partial class PendingMessagesModifiedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "pending_messages.modified"; + /// The pending_messages.modified event payload. [JsonPropertyName("data")] public required PendingMessagesModifiedData Data { get; set; } } -/// -/// Event: assistant.turn_start -/// +/// Represents the assistant.turn_start event. public partial class AssistantTurnStartEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.turn_start"; + /// The assistant.turn_start event payload. [JsonPropertyName("data")] public required AssistantTurnStartData Data { get; set; } } -/// -/// Event: assistant.intent -/// +/// Represents the assistant.intent event. public partial class AssistantIntentEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.intent"; + /// The assistant.intent event payload. [JsonPropertyName("data")] public required AssistantIntentData Data { get; set; } } -/// -/// Event: assistant.reasoning -/// +/// Represents the assistant.reasoning event. public partial class AssistantReasoningEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.reasoning"; + /// The assistant.reasoning event payload. [JsonPropertyName("data")] public required AssistantReasoningData Data { get; set; } } -/// -/// Event: assistant.reasoning_delta -/// +/// Represents the assistant.reasoning_delta event. public partial class AssistantReasoningDeltaEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.reasoning_delta"; + /// The assistant.reasoning_delta event payload. [JsonPropertyName("data")] public required AssistantReasoningDeltaData Data { get; set; } } -/// -/// Event: assistant.streaming_delta -/// +/// Represents the assistant.streaming_delta event. public partial class AssistantStreamingDeltaEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.streaming_delta"; + /// The assistant.streaming_delta event payload. [JsonPropertyName("data")] public required AssistantStreamingDeltaData Data { get; set; } } -/// -/// Event: assistant.message -/// +/// Represents the assistant.message event. public partial class AssistantMessageEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.message"; + /// The assistant.message event payload. [JsonPropertyName("data")] public required AssistantMessageData Data { get; set; } } -/// -/// Event: assistant.message_delta -/// +/// Represents the assistant.message_delta event. public partial class AssistantMessageDeltaEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.message_delta"; + /// The assistant.message_delta event payload. [JsonPropertyName("data")] public required AssistantMessageDeltaData Data { get; set; } } -/// -/// Event: assistant.turn_end -/// +/// Represents the assistant.turn_end event. public partial class AssistantTurnEndEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.turn_end"; + /// The assistant.turn_end event payload. [JsonPropertyName("data")] public required AssistantTurnEndData Data { get; set; } } -/// -/// Event: assistant.usage -/// +/// Represents the assistant.usage event. public partial class AssistantUsageEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "assistant.usage"; + /// The assistant.usage event payload. [JsonPropertyName("data")] public required AssistantUsageData Data { get; set; } } -/// -/// Event: abort -/// +/// Represents the abort event. public partial class AbortEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "abort"; + /// The abort event payload. [JsonPropertyName("data")] public required AbortData Data { get; set; } } -/// -/// Event: tool.user_requested -/// +/// Represents the tool.user_requested event. public partial class ToolUserRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "tool.user_requested"; + /// The tool.user_requested event payload. [JsonPropertyName("data")] public required ToolUserRequestedData Data { get; set; } } -/// -/// Event: tool.execution_start -/// +/// Represents the tool.execution_start event. public partial class ToolExecutionStartEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "tool.execution_start"; + /// The tool.execution_start event payload. [JsonPropertyName("data")] public required ToolExecutionStartData Data { get; set; } } -/// -/// Event: tool.execution_partial_result -/// +/// Represents the tool.execution_partial_result event. public partial class ToolExecutionPartialResultEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "tool.execution_partial_result"; + /// The tool.execution_partial_result event payload. [JsonPropertyName("data")] public required ToolExecutionPartialResultData Data { get; set; } } -/// -/// Event: tool.execution_progress -/// +/// Represents the tool.execution_progress event. public partial class ToolExecutionProgressEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "tool.execution_progress"; + /// The tool.execution_progress event payload. [JsonPropertyName("data")] public required ToolExecutionProgressData Data { get; set; } } -/// -/// Event: tool.execution_complete -/// +/// Represents the tool.execution_complete event. public partial class ToolExecutionCompleteEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "tool.execution_complete"; + /// The tool.execution_complete event payload. [JsonPropertyName("data")] public required ToolExecutionCompleteData Data { get; set; } } -/// -/// Event: skill.invoked -/// +/// Represents the skill.invoked event. public partial class SkillInvokedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "skill.invoked"; + /// The skill.invoked event payload. [JsonPropertyName("data")] public required SkillInvokedData Data { get; set; } } -/// -/// Event: subagent.started -/// +/// Represents the subagent.started event. public partial class SubagentStartedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "subagent.started"; + /// The subagent.started event payload. [JsonPropertyName("data")] public required SubagentStartedData Data { get; set; } } -/// -/// Event: subagent.completed -/// +/// Represents the subagent.completed event. public partial class SubagentCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "subagent.completed"; + /// The subagent.completed event payload. [JsonPropertyName("data")] public required SubagentCompletedData Data { get; set; } } -/// -/// Event: subagent.failed -/// +/// Represents the subagent.failed event. public partial class SubagentFailedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "subagent.failed"; + /// The subagent.failed event payload. [JsonPropertyName("data")] public required SubagentFailedData Data { get; set; } } -/// -/// Event: subagent.selected -/// +/// Represents the subagent.selected event. public partial class SubagentSelectedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "subagent.selected"; + /// The subagent.selected event payload. [JsonPropertyName("data")] public required SubagentSelectedData Data { get; set; } } -/// -/// Event: subagent.deselected -/// +/// Empty payload; the event signals that the custom agent was deselected, returning to the default agent. +/// Represents the subagent.deselected event. public partial class SubagentDeselectedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "subagent.deselected"; + /// The subagent.deselected event payload. [JsonPropertyName("data")] public required SubagentDeselectedData Data { get; set; } } -/// -/// Event: hook.start -/// +/// Represents the hook.start event. public partial class HookStartEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "hook.start"; + /// The hook.start event payload. [JsonPropertyName("data")] public required HookStartData Data { get; set; } } -/// -/// Event: hook.end -/// +/// Represents the hook.end event. public partial class HookEndEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "hook.end"; + /// The hook.end event payload. [JsonPropertyName("data")] public required HookEndData Data { get; set; } } -/// -/// Event: system.message -/// +/// Represents the system.message event. public partial class SystemMessageEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "system.message"; + /// The system.message event payload. [JsonPropertyName("data")] public required SystemMessageData Data { get; set; } } -/// -/// Event: system.notification -/// +/// Represents the system.notification event. public partial class SystemNotificationEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "system.notification"; + /// The system.notification event payload. [JsonPropertyName("data")] public required SystemNotificationData Data { get; set; } } -/// -/// Event: permission.requested -/// +/// Represents the permission.requested event. public partial class PermissionRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "permission.requested"; + /// The permission.requested event payload. [JsonPropertyName("data")] public required PermissionRequestedData Data { get; set; } } -/// -/// Event: permission.completed -/// +/// Represents the permission.completed event. public partial class PermissionCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "permission.completed"; + /// The permission.completed event payload. [JsonPropertyName("data")] public required PermissionCompletedData Data { get; set; } } -/// -/// Event: user_input.requested -/// +/// Represents the user_input.requested event. public partial class UserInputRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "user_input.requested"; + /// The user_input.requested event payload. [JsonPropertyName("data")] public required UserInputRequestedData Data { get; set; } } -/// -/// Event: user_input.completed -/// +/// Represents the user_input.completed event. public partial class UserInputCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "user_input.completed"; + /// The user_input.completed event payload. [JsonPropertyName("data")] public required UserInputCompletedData Data { get; set; } } -/// -/// Event: elicitation.requested -/// +/// Represents the elicitation.requested event. public partial class ElicitationRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "elicitation.requested"; + /// The elicitation.requested event payload. [JsonPropertyName("data")] public required ElicitationRequestedData Data { get; set; } } -/// -/// Event: elicitation.completed -/// +/// Represents the elicitation.completed event. public partial class ElicitationCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "elicitation.completed"; + /// The elicitation.completed event payload. [JsonPropertyName("data")] public required ElicitationCompletedData Data { get; set; } } -/// -/// Event: external_tool.requested -/// +/// Represents the external_tool.requested event. public partial class ExternalToolRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "external_tool.requested"; + /// The external_tool.requested event payload. [JsonPropertyName("data")] public required ExternalToolRequestedData Data { get; set; } } -/// -/// Event: external_tool.completed -/// +/// Represents the external_tool.completed event. public partial class ExternalToolCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "external_tool.completed"; + /// The external_tool.completed event payload. [JsonPropertyName("data")] public required ExternalToolCompletedData Data { get; set; } } -/// -/// Event: command.queued -/// +/// Represents the command.queued event. public partial class CommandQueuedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "command.queued"; + /// The command.queued event payload. [JsonPropertyName("data")] public required CommandQueuedData Data { get; set; } } -/// -/// Event: command.completed -/// +/// Represents the command.completed event. public partial class CommandCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "command.completed"; + /// The command.completed event payload. [JsonPropertyName("data")] public required CommandCompletedData Data { get; set; } } -/// -/// Event: exit_plan_mode.requested -/// +/// Represents the exit_plan_mode.requested event. public partial class ExitPlanModeRequestedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "exit_plan_mode.requested"; + /// The exit_plan_mode.requested event payload. [JsonPropertyName("data")] public required ExitPlanModeRequestedData Data { get; set; } } -/// -/// Event: exit_plan_mode.completed -/// +/// Represents the exit_plan_mode.completed event. public partial class ExitPlanModeCompletedEvent : SessionEvent { + /// [JsonIgnore] public override string Type => "exit_plan_mode.completed"; + /// The exit_plan_mode.completed event payload. [JsonPropertyName("data")] public required ExitPlanModeCompletedData Data { get; set; } } +/// Event payload for . public partial class SessionStartData { + /// Unique identifier for the session. [JsonPropertyName("sessionId")] public required string SessionId { get; set; } + /// Schema version number for the session event format. [JsonPropertyName("version")] public required double Version { get; set; } + /// Identifier of the software producing the events (e.g., "copilot-agent"). [JsonPropertyName("producer")] public required string Producer { get; set; } + /// Version string of the Copilot application. [JsonPropertyName("copilotVersion")] public required string CopilotVersion { get; set; } + /// ISO 8601 timestamp when the session was created. [JsonPropertyName("startTime")] public required DateTimeOffset StartTime { get; set; } + /// Model selected at session creation time, if any. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("selectedModel")] public string? SelectedModel { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionStartDataContext? Context { get; set; } + /// Gets or sets the alreadyInUse value. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } } +/// Event payload for . public partial class SessionResumeData { + /// ISO 8601 timestamp when the session was resumed. [JsonPropertyName("resumeTime")] public required DateTimeOffset ResumeTime { get; set; } + /// Total number of persisted events in the session at the time of resume. [JsonPropertyName("eventCount")] public required double EventCount { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionResumeDataContext? Context { get; set; } + /// Gets or sets the alreadyInUse value. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } } +/// Event payload for . public partial class SessionErrorData { + /// Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query"). [JsonPropertyName("errorType")] public required string ErrorType { get; set; } + /// Human-readable error message. [JsonPropertyName("message")] public required string Message { get; set; } + /// Error stack trace, when available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("stack")] public string? Stack { get; set; } + /// HTTP status code from the upstream request, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] public double? StatusCode { get; set; } + /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } } +/// Payload indicating the agent is idle; includes any background tasks still in flight. public partial class SessionIdleData { + /// Background tasks still running when the agent became idle. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("backgroundTasks")] public SessionIdleDataBackgroundTasks? BackgroundTasks { get; set; } } +/// Event payload for . public partial class SessionTitleChangedData { + /// The new display title for the session. [JsonPropertyName("title")] public required string Title { get; set; } } +/// Event payload for . public partial class SessionInfoData { + /// Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model"). [JsonPropertyName("infoType")] public required string InfoType { get; set; } + /// Human-readable informational message for display in the timeline. [JsonPropertyName("message")] public required string Message { get; set; } } +/// Event payload for . public partial class SessionWarningData { + /// Category of warning (e.g., "subscription", "policy", "mcp"). [JsonPropertyName("warningType")] public required string WarningType { get; set; } + /// Human-readable warning message for display in the timeline. [JsonPropertyName("message")] public required string Message { get; set; } } +/// Event payload for . public partial class SessionModelChangeData { + /// Model that was previously selected, if any. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("previousModel")] public string? PreviousModel { get; set; } + /// Newly selected model identifier. [JsonPropertyName("newModel")] public required string NewModel { get; set; } } +/// Event payload for . public partial class SessionModeChangedData { + /// Agent mode before the change (e.g., "interactive", "plan", "autopilot"). [JsonPropertyName("previousMode")] public required string PreviousMode { get; set; } + /// Agent mode after the change (e.g., "interactive", "plan", "autopilot"). [JsonPropertyName("newMode")] public required string NewMode { get; set; } } +/// Event payload for . public partial class SessionPlanChangedData { + /// The type of operation performed on the plan file. [JsonPropertyName("operation")] public required SessionPlanChangedDataOperation Operation { get; set; } } +/// Event payload for . public partial class SessionWorkspaceFileChangedData { + /// Relative path within the session workspace files directory. [JsonPropertyName("path")] public required string Path { get; set; } + /// Whether the file was newly created or updated. [JsonPropertyName("operation")] public required SessionWorkspaceFileChangedDataOperation Operation { get; set; } } +/// Event payload for . public partial class SessionHandoffData { + /// ISO 8601 timestamp when the handoff occurred. [JsonPropertyName("handoffTime")] public required DateTimeOffset HandoffTime { get; set; } + /// Origin type of the session being handed off. [JsonPropertyName("sourceType")] public required SessionHandoffDataSourceType SourceType { get; set; } + /// Repository context for the handed-off session. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public SessionHandoffDataRepository? Repository { get; set; } + /// Additional context information for the handoff. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public string? Context { get; set; } + /// Summary of the work done in the source session. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("summary")] public string? Summary { get; set; } + /// Session ID of the remote session being handed off. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("remoteSessionId")] public string? RemoteSessionId { get; set; } } +/// Event payload for . public partial class SessionTruncationData { + /// Maximum token count for the model's context window. [JsonPropertyName("tokenLimit")] public required double TokenLimit { get; set; } + /// Total tokens in conversation messages before truncation. [JsonPropertyName("preTruncationTokensInMessages")] public required double PreTruncationTokensInMessages { get; set; } + /// Number of conversation messages before truncation. [JsonPropertyName("preTruncationMessagesLength")] public required double PreTruncationMessagesLength { get; set; } + /// Total tokens in conversation messages after truncation. [JsonPropertyName("postTruncationTokensInMessages")] public required double PostTruncationTokensInMessages { get; set; } + /// Number of conversation messages after truncation. [JsonPropertyName("postTruncationMessagesLength")] public required double PostTruncationMessagesLength { get; set; } + /// Number of tokens removed by truncation. [JsonPropertyName("tokensRemovedDuringTruncation")] public required double TokensRemovedDuringTruncation { get; set; } + /// Number of messages removed by truncation. [JsonPropertyName("messagesRemovedDuringTruncation")] public required double MessagesRemovedDuringTruncation { get; set; } + /// Identifier of the component that performed truncation (e.g., "BasicTruncator"). [JsonPropertyName("performedBy")] public required string PerformedBy { get; set; } } +/// Event payload for . public partial class SessionSnapshotRewindData { + /// Event ID that was rewound to; all events after this one were removed. [JsonPropertyName("upToEventId")] public required string UpToEventId { get; set; } + /// Number of events that were removed by the rewind. [JsonPropertyName("eventsRemoved")] public required double EventsRemoved { get; set; } } +/// Event payload for . public partial class SessionShutdownData { + /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error"). [JsonPropertyName("shutdownType")] public required SessionShutdownDataShutdownType ShutdownType { get; set; } + /// Error description when shutdownType is "error". [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("errorReason")] public string? ErrorReason { get; set; } + /// Total number of premium API requests used during the session. [JsonPropertyName("totalPremiumRequests")] public required double TotalPremiumRequests { get; set; } + /// Cumulative time spent in API calls during the session, in milliseconds. [JsonPropertyName("totalApiDurationMs")] public required double TotalApiDurationMs { get; set; } + /// Unix timestamp (milliseconds) when the session started. [JsonPropertyName("sessionStartTime")] public required double SessionStartTime { get; set; } + /// Aggregate code change metrics for the session. [JsonPropertyName("codeChanges")] public required SessionShutdownDataCodeChanges CodeChanges { get; set; } + /// Per-model usage breakdown, keyed by model identifier. [JsonPropertyName("modelMetrics")] public required Dictionary ModelMetrics { get; set; } + /// Model that was selected at the time of shutdown. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("currentModel")] public string? CurrentModel { get; set; } } +/// Event payload for . public partial class SessionContextChangedData { + /// Current working directory path. [JsonPropertyName("cwd")] public required string Cwd { get; set; } + /// Root directory of the git repository, resolved via git rev-parse. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } + /// Repository identifier in "owner/name" format, derived from the git remote URL. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } } +/// Event payload for . public partial class SessionUsageInfoData { + /// Maximum token count for the model's context window. [JsonPropertyName("tokenLimit")] public required double TokenLimit { get; set; } + /// Current number of tokens in the context window. [JsonPropertyName("currentTokens")] public required double CurrentTokens { get; set; } + /// Current number of messages in the conversation. [JsonPropertyName("messagesLength")] public required double MessagesLength { get; set; } } +/// Empty payload; the event signals that LLM-powered conversation compaction has begun. public partial class SessionCompactionStartData { } +/// Event payload for . public partial class SessionCompactionCompleteData { + /// Whether compaction completed successfully. [JsonPropertyName("success")] public required bool Success { get; set; } + /// Error message if compaction failed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("error")] public string? Error { get; set; } + /// Total tokens in conversation before compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("preCompactionTokens")] public double? PreCompactionTokens { get; set; } + /// Total tokens in conversation after compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("postCompactionTokens")] public double? PostCompactionTokens { get; set; } + /// Number of messages before compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("preCompactionMessagesLength")] public double? PreCompactionMessagesLength { get; set; } + /// Number of messages removed during compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("messagesRemoved")] public double? MessagesRemoved { get; set; } + /// Number of tokens removed during compaction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("tokensRemoved")] public double? TokensRemoved { get; set; } + /// LLM-generated summary of the compacted conversation history. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("summaryContent")] public string? SummaryContent { get; set; } + /// Checkpoint snapshot number created for recovery. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("checkpointNumber")] public double? CheckpointNumber { get; set; } + /// File path where the checkpoint was stored. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("checkpointPath")] public string? CheckpointPath { get; set; } + /// Token usage breakdown for the compaction LLM call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("compactionTokensUsed")] public SessionCompactionCompleteDataCompactionTokensUsed? CompactionTokensUsed { get; set; } + /// GitHub request tracing ID (x-github-request-id header) for the compaction LLM call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestId")] public string? RequestId { get; set; } } +/// Event payload for . public partial class SessionTaskCompleteData { + /// Optional summary of the completed task, provided by the agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("summary")] public string? Summary { get; set; } } +/// Event payload for . public partial class UserMessageData { + /// The user's message text as displayed in the timeline. [JsonPropertyName("content")] public required string Content { get; set; } + /// Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("transformedContent")] public string? TransformedContent { get; set; } + /// Files, selections, or GitHub references attached to the message. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("attachments")] public UserMessageDataAttachmentsItem[]? Attachments { get; set; } + /// Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } + /// The agent mode that was active when this message was sent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("agentMode")] public UserMessageDataAgentMode? AgentMode { get; set; } + /// CAPI interaction ID for correlating this user message with its turn. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] public string? InteractionId { get; set; } } +/// Empty payload; the event signals that the pending message queue has changed. public partial class PendingMessagesModifiedData { } +/// Event payload for . public partial class AssistantTurnStartData { + /// Identifier for this turn within the agentic loop, typically a stringified turn number. [JsonPropertyName("turnId")] public required string TurnId { get; set; } + /// CAPI interaction ID for correlating this turn with upstream telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] public string? InteractionId { get; set; } } +/// Event payload for . public partial class AssistantIntentData { + /// Short description of what the agent is currently doing or planning to do. [JsonPropertyName("intent")] public required string Intent { get; set; } } +/// Event payload for . public partial class AssistantReasoningData { + /// Unique identifier for this reasoning block. [JsonPropertyName("reasoningId")] public required string ReasoningId { get; set; } + /// The complete extended thinking text from the model. [JsonPropertyName("content")] public required string Content { get; set; } } +/// Event payload for . public partial class AssistantReasoningDeltaData { + /// Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event. [JsonPropertyName("reasoningId")] public required string ReasoningId { get; set; } + /// Incremental text chunk to append to the reasoning content. [JsonPropertyName("deltaContent")] public required string DeltaContent { get; set; } } +/// Event payload for . public partial class AssistantStreamingDeltaData { + /// Cumulative total bytes received from the streaming response so far. [JsonPropertyName("totalResponseSizeBytes")] public required double TotalResponseSizeBytes { get; set; } } +/// Event payload for . public partial class AssistantMessageData { + /// Unique identifier for this assistant message. [JsonPropertyName("messageId")] public required string MessageId { get; set; } + /// The assistant's text response content. [JsonPropertyName("content")] public required string Content { get; set; } + /// Tool invocations requested by the assistant in this message. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolRequests")] public AssistantMessageDataToolRequestsItem[]? ToolRequests { get; set; } + /// Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningOpaque")] public string? ReasoningOpaque { get; set; } + /// Readable reasoning text from the model's extended thinking. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("reasoningText")] public string? ReasoningText { get; set; } + /// Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("encryptedContent")] public string? EncryptedContent { get; set; } + /// Generation phase for phased-output models (e.g., thinking vs. response phases). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("phase")] public string? Phase { get; set; } + /// Actual output token count from the API response (completion_tokens), used for accurate token accounting. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputTokens")] public double? OutputTokens { get; set; } + /// CAPI interaction ID for correlating this message with upstream telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] public string? InteractionId { get; set; } + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } } +/// Event payload for . public partial class AssistantMessageDeltaData { + /// Message ID this delta belongs to, matching the corresponding assistant.message event. [JsonPropertyName("messageId")] public required string MessageId { get; set; } + /// Incremental text chunk to append to the message content. [JsonPropertyName("deltaContent")] public required string DeltaContent { get; set; } + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } } +/// Event payload for . public partial class AssistantTurnEndData { + /// Identifier of the turn that has ended, matching the corresponding assistant.turn_start event. [JsonPropertyName("turnId")] public required string TurnId { get; set; } } +/// Event payload for . public partial class AssistantUsageData { + /// Model identifier used for this API call. [JsonPropertyName("model")] public required string Model { get; set; } + /// Number of input tokens consumed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("inputTokens")] public double? InputTokens { get; set; } + /// Number of output tokens produced. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("outputTokens")] public double? OutputTokens { get; set; } + /// Number of tokens read from prompt cache. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cacheReadTokens")] public double? CacheReadTokens { get; set; } + /// Number of tokens written to prompt cache. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cacheWriteTokens")] public double? CacheWriteTokens { get; set; } + /// Model multiplier cost for billing purposes. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cost")] public double? Cost { get; set; } + /// Duration of the API call in milliseconds. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("duration")] public double? Duration { get; set; } + /// What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("initiator")] public string? Initiator { get; set; } + /// Completion ID from the model provider (e.g., chatcmpl-abc123). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("apiCallId")] public string? ApiCallId { get; set; } + /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + /// Parent tool call ID when this usage originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } + /// Per-quota resource usage snapshots, keyed by quota identifier. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("quotaSnapshots")] public Dictionary? QuotaSnapshots { get; set; } + /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("copilotUsage")] public AssistantUsageDataCopilotUsage? CopilotUsage { get; set; } } +/// Event payload for . public partial class AbortData { + /// Reason the current turn was aborted (e.g., "user initiated"). [JsonPropertyName("reason")] public required string Reason { get; set; } } +/// Event payload for . public partial class ToolUserRequestedData { + /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Name of the tool the user wants to invoke. [JsonPropertyName("toolName")] public required string ToolName { get; set; } + /// Arguments for the tool invocation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public object? Arguments { get; set; } } +/// Event payload for . public partial class ToolExecutionStartData { + /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Name of the tool being executed. [JsonPropertyName("toolName")] public required string ToolName { get; set; } + /// Arguments passed to the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public object? Arguments { get; set; } + /// Name of the MCP server hosting this tool, when the tool is an MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mcpServerName")] public string? McpServerName { get; set; } + /// Original tool name on the MCP server, when the tool is an MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mcpToolName")] public string? McpToolName { get; set; } + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } } +/// Event payload for . public partial class ToolExecutionPartialResultData { + /// Tool call ID this partial result belongs to. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Incremental output chunk from the running tool. [JsonPropertyName("partialOutput")] public required string PartialOutput { get; set; } } +/// Event payload for . public partial class ToolExecutionProgressData { + /// Tool call ID this progress notification belongs to. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Human-readable progress status message (e.g., from an MCP server). [JsonPropertyName("progressMessage")] public required string ProgressMessage { get; set; } } +/// Event payload for . public partial class ToolExecutionCompleteData { + /// Unique identifier for the completed tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Whether the tool execution completed successfully. [JsonPropertyName("success")] public required bool Success { get; set; } + /// Model identifier that generated this tool call. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] public string? Model { get; set; } + /// CAPI interaction ID for correlating this tool execution with upstream telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] public string? InteractionId { get; set; } + /// Whether this tool call was explicitly requested by the user rather than the assistant. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("isUserRequested")] public bool? IsUserRequested { get; set; } + /// Tool execution result on success. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("result")] public ToolExecutionCompleteDataResult? Result { get; set; } + /// Error details when the tool execution failed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("error")] public ToolExecutionCompleteDataError? Error { get; set; } + /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolTelemetry")] public Dictionary? ToolTelemetry { get; set; } + /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } } +/// Event payload for . public partial class SkillInvokedData { + /// Name of the invoked skill. [JsonPropertyName("name")] public required string Name { get; set; } + /// File path to the SKILL.md definition. [JsonPropertyName("path")] public required string Path { get; set; } + /// Full content of the skill file, injected into the conversation for the model. [JsonPropertyName("content")] public required string Content { get; set; } + /// Tool names that should be auto-approved when this skill is active. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("allowedTools")] public string[]? AllowedTools { get; set; } + /// Name of the plugin this skill originated from, when applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pluginName")] public string? PluginName { get; set; } + /// Version of the plugin this skill originated from, when applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } } +/// Event payload for . public partial class SubagentStartedData { + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Internal name of the sub-agent. [JsonPropertyName("agentName")] public required string AgentName { get; set; } + /// Human-readable display name of the sub-agent. [JsonPropertyName("agentDisplayName")] public required string AgentDisplayName { get; set; } + /// Description of what the sub-agent does. [JsonPropertyName("agentDescription")] public required string AgentDescription { get; set; } } +/// Event payload for . public partial class SubagentCompletedData { + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Internal name of the sub-agent. [JsonPropertyName("agentName")] public required string AgentName { get; set; } + /// Human-readable display name of the sub-agent. [JsonPropertyName("agentDisplayName")] public required string AgentDisplayName { get; set; } } +/// Event payload for . public partial class SubagentFailedData { + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Internal name of the sub-agent. [JsonPropertyName("agentName")] public required string AgentName { get; set; } + /// Human-readable display name of the sub-agent. [JsonPropertyName("agentDisplayName")] public required string AgentDisplayName { get; set; } + /// Error message describing why the sub-agent failed. [JsonPropertyName("error")] public required string Error { get; set; } } +/// Event payload for . public partial class SubagentSelectedData { + /// Internal name of the selected custom agent. [JsonPropertyName("agentName")] public required string AgentName { get; set; } + /// Human-readable display name of the selected custom agent. [JsonPropertyName("agentDisplayName")] public required string AgentDisplayName { get; set; } + /// List of tool names available to this agent, or null for all tools. [JsonPropertyName("tools")] public string[]? Tools { get; set; } } +/// Empty payload; the event signals that the custom agent was deselected, returning to the default agent. public partial class SubagentDeselectedData { } +/// Event payload for . public partial class HookStartData { + /// Unique identifier for this hook invocation. [JsonPropertyName("hookInvocationId")] public required string HookInvocationId { get; set; } + /// Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart"). [JsonPropertyName("hookType")] public required string HookType { get; set; } + /// Input data passed to the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] public object? Input { get; set; } } +/// Event payload for . public partial class HookEndData { + /// Identifier matching the corresponding hook.start event. [JsonPropertyName("hookInvocationId")] public required string HookInvocationId { get; set; } + /// Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart"). [JsonPropertyName("hookType")] public required string HookType { get; set; } + /// Output data produced by the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("output")] public object? Output { get; set; } + /// Whether the hook completed successfully. [JsonPropertyName("success")] public required bool Success { get; set; } + /// Error details when the hook failed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("error")] public HookEndDataError? Error { get; set; } } +/// Event payload for . public partial class SystemMessageData { + /// The system or developer prompt text. [JsonPropertyName("content")] public required string Content { get; set; } + /// Message role: "system" for system prompts, "developer" for developer-injected instructions. [JsonPropertyName("role")] public required SystemMessageDataRole Role { get; set; } + /// Optional name identifier for the message source. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("name")] public string? Name { get; set; } + /// Metadata about the prompt template and its construction. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("metadata")] public SystemMessageDataMetadata? Metadata { get; set; } } +/// Event payload for . public partial class SystemNotificationData { + /// The notification text, typically wrapped in <system_notification> XML tags. [JsonPropertyName("content")] public required string Content { get; set; } + /// Structured metadata identifying what triggered this notification. [JsonPropertyName("kind")] public required SystemNotificationDataKind Kind { get; set; } } +/// Event payload for . public partial class PermissionRequestedData { + /// Unique identifier for this permission request; used to respond via session.respondToPermission(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// Details of the permission being requested. [JsonPropertyName("permissionRequest")] public required PermissionRequest PermissionRequest { get; set; } } +/// Event payload for . public partial class PermissionCompletedData { + /// Request ID of the resolved permission request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// The result of the permission request. [JsonPropertyName("result")] public required PermissionCompletedDataResult Result { get; set; } } +/// Event payload for . public partial class UserInputRequestedData { + /// Unique identifier for this input request; used to respond via session.respondToUserInput(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// The question or prompt to present to the user. [JsonPropertyName("question")] public required string Question { get; set; } + /// Predefined choices for the user to select from, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("choices")] public string[]? Choices { get; set; } + /// Whether the user can provide a free-form text response in addition to predefined choices. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("allowFreeform")] public bool? AllowFreeform { get; set; } } +/// Event payload for . public partial class UserInputCompletedData { + /// Request ID of the resolved user input request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } } +/// Event payload for . public partial class ElicitationRequestedData { + /// Unique identifier for this elicitation request; used to respond via session.respondToElicitation(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// Message describing what information is needed from the user. [JsonPropertyName("message")] public required string Message { get; set; } + /// Elicitation mode; currently only "form" is supported. Defaults to "form" when absent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mode")] public string? Mode { get; set; } + /// JSON Schema describing the form fields to present to the user. [JsonPropertyName("requestedSchema")] public required ElicitationRequestedDataRequestedSchema RequestedSchema { get; set; } } +/// Event payload for . public partial class ElicitationCompletedData { + /// Request ID of the resolved elicitation request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } } +/// Event payload for . public partial class ExternalToolRequestedData { + /// Unique identifier for this request; used to respond via session.respondToExternalTool(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// Session ID that this external tool request belongs to. [JsonPropertyName("sessionId")] public required string SessionId { get; set; } + /// Tool call ID assigned to this external tool invocation. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Name of the external tool to invoke. [JsonPropertyName("toolName")] public required string ToolName { get; set; } + /// Arguments to pass to the external tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public object? Arguments { get; set; } } +/// Event payload for . public partial class ExternalToolCompletedData { + /// Request ID of the resolved external tool request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } } +/// Event payload for . public partial class CommandQueuedData { + /// Unique identifier for this request; used to respond via session.respondToQueuedCommand(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// The slash command text to be executed (e.g., /help, /clear). [JsonPropertyName("command")] public required string Command { get; set; } } +/// Event payload for . public partial class CommandCompletedData { + /// Request ID of the resolved command request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } } +/// Event payload for . public partial class ExitPlanModeRequestedData { + /// Unique identifier for this request; used to respond via session.respondToExitPlanMode(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// Summary of the plan that was created. [JsonPropertyName("summary")] public required string Summary { get; set; } + /// Full content of the plan file. [JsonPropertyName("planContent")] public required string PlanContent { get; set; } + /// Available actions the user can take (e.g., approve, edit, reject). [JsonPropertyName("actions")] public required string[] Actions { get; set; } + /// The recommended action for the user to take. [JsonPropertyName("recommendedAction")] public required string RecommendedAction { get; set; } } +/// Event payload for . public partial class ExitPlanModeCompletedData { + /// Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] public required string RequestId { get; set; } } +/// Working directory and git context at session start. +/// Nested data type for SessionStartDataContext. public partial class SessionStartDataContext { + /// Current working directory path. [JsonPropertyName("cwd")] public required string Cwd { get; set; } + /// Root directory of the git repository, resolved via git rev-parse. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } + /// Repository identifier in "owner/name" format, derived from the git remote URL. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } } +/// Updated working directory and git context at resume time. +/// Nested data type for SessionResumeDataContext. public partial class SessionResumeDataContext { + /// Current working directory path. [JsonPropertyName("cwd")] public required string Cwd { get; set; } + /// Root directory of the git repository, resolved via git rev-parse. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } + /// Repository identifier in "owner/name" format, derived from the git remote URL. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } } +/// Nested data type for SessionIdleDataBackgroundTasksAgentsItem. public partial class SessionIdleDataBackgroundTasksAgentsItem { + /// Unique identifier of the background agent. [JsonPropertyName("agentId")] public required string AgentId { get; set; } + /// Type of the background agent. [JsonPropertyName("agentType")] public required string AgentType { get; set; } + /// Human-readable description of the agent task. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } } +/// Nested data type for SessionIdleDataBackgroundTasksShellsItem. public partial class SessionIdleDataBackgroundTasksShellsItem { + /// Unique identifier of the background shell. [JsonPropertyName("shellId")] public required string ShellId { get; set; } + /// Human-readable description of the shell command. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } } +/// Background tasks still running when the agent became idle. +/// Nested data type for SessionIdleDataBackgroundTasks. public partial class SessionIdleDataBackgroundTasks { + /// Currently running background agents. [JsonPropertyName("agents")] public required SessionIdleDataBackgroundTasksAgentsItem[] Agents { get; set; } + /// Currently running background shell commands. [JsonPropertyName("shells")] public required SessionIdleDataBackgroundTasksShellsItem[] Shells { get; set; } } +/// Repository context for the handed-off session. +/// Nested data type for SessionHandoffDataRepository. public partial class SessionHandoffDataRepository { + /// Repository owner (user or organization). [JsonPropertyName("owner")] public required string Owner { get; set; } + /// Repository name. [JsonPropertyName("name")] public required string Name { get; set; } + /// Git branch name, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } } +/// Aggregate code change metrics for the session. +/// Nested data type for SessionShutdownDataCodeChanges. public partial class SessionShutdownDataCodeChanges { + /// Total number of lines added during the session. [JsonPropertyName("linesAdded")] public required double LinesAdded { get; set; } + /// Total number of lines removed during the session. [JsonPropertyName("linesRemoved")] public required double LinesRemoved { get; set; } + /// List of file paths that were modified during the session. [JsonPropertyName("filesModified")] public required string[] FilesModified { get; set; } } +/// Token usage breakdown for the compaction LLM call. +/// Nested data type for SessionCompactionCompleteDataCompactionTokensUsed. public partial class SessionCompactionCompleteDataCompactionTokensUsed { + /// Input tokens consumed by the compaction LLM call. [JsonPropertyName("input")] public required double Input { get; set; } + /// Output tokens produced by the compaction LLM call. [JsonPropertyName("output")] public required double Output { get; set; } + /// Cached input tokens reused in the compaction LLM call. [JsonPropertyName("cachedInput")] public required double CachedInput { get; set; } } +/// Optional line range to scope the attachment to a specific section of the file. +/// Nested data type for UserMessageDataAttachmentsItemFileLineRange. public partial class UserMessageDataAttachmentsItemFileLineRange { + /// Start line number (1-based). [JsonPropertyName("start")] public required double Start { get; set; } + /// End line number (1-based, inclusive). [JsonPropertyName("end")] public required double End { get; set; } } +/// The file variant of . public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachmentsItem { + /// [JsonIgnore] public override string Type => "file"; + /// Absolute file or directory path. [JsonPropertyName("path")] public required string Path { get; set; } + /// User-facing display name for the attachment. [JsonPropertyName("displayName")] public required string DisplayName { get; set; } + /// Optional line range to scope the attachment to a specific section of the file. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("lineRange")] public UserMessageDataAttachmentsItemFileLineRange? LineRange { get; set; } } +/// Optional line range to scope the attachment to a specific section of the file. +/// Nested data type for UserMessageDataAttachmentsItemDirectoryLineRange. public partial class UserMessageDataAttachmentsItemDirectoryLineRange { + /// Start line number (1-based). [JsonPropertyName("start")] public required double Start { get; set; } + /// End line number (1-based, inclusive). [JsonPropertyName("end")] public required double End { get; set; } } +/// The directory variant of . public partial class UserMessageDataAttachmentsItemDirectory : UserMessageDataAttachmentsItem { + /// [JsonIgnore] public override string Type => "directory"; + /// Absolute file or directory path. [JsonPropertyName("path")] public required string Path { get; set; } + /// User-facing display name for the attachment. [JsonPropertyName("displayName")] public required string DisplayName { get; set; } + /// Optional line range to scope the attachment to a specific section of the file. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("lineRange")] public UserMessageDataAttachmentsItemDirectoryLineRange? LineRange { get; set; } } +/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionStart. public partial class UserMessageDataAttachmentsItemSelectionSelectionStart { + /// Start line number (0-based). [JsonPropertyName("line")] public required double Line { get; set; } + /// Start character offset within the line (0-based). [JsonPropertyName("character")] public required double Character { get; set; } } +/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionEnd. public partial class UserMessageDataAttachmentsItemSelectionSelectionEnd { + /// End line number (0-based). [JsonPropertyName("line")] public required double Line { get; set; } + /// End character offset within the line (0-based). [JsonPropertyName("character")] public required double Character { get; set; } } +/// Position range of the selection within the file. +/// Nested data type for UserMessageDataAttachmentsItemSelectionSelection. public partial class UserMessageDataAttachmentsItemSelectionSelection { + /// Gets or sets the start value. [JsonPropertyName("start")] public required UserMessageDataAttachmentsItemSelectionSelectionStart Start { get; set; } + /// Gets or sets the end value. [JsonPropertyName("end")] public required UserMessageDataAttachmentsItemSelectionSelectionEnd End { get; set; } } +/// The selection variant of . public partial class UserMessageDataAttachmentsItemSelection : UserMessageDataAttachmentsItem { + /// [JsonIgnore] public override string Type => "selection"; + /// Absolute path to the file containing the selection. [JsonPropertyName("filePath")] public required string FilePath { get; set; } + /// User-facing display name for the selection. [JsonPropertyName("displayName")] public required string DisplayName { get; set; } + /// The selected text content. [JsonPropertyName("text")] public required string Text { get; set; } + /// Position range of the selection within the file. [JsonPropertyName("selection")] public required UserMessageDataAttachmentsItemSelectionSelection Selection { get; set; } } +/// The github_reference variant of . public partial class UserMessageDataAttachmentsItemGithubReference : UserMessageDataAttachmentsItem { + /// [JsonIgnore] public override string Type => "github_reference"; + /// Issue, pull request, or discussion number. [JsonPropertyName("number")] public required double Number { get; set; } + /// Title of the referenced item. [JsonPropertyName("title")] public required string Title { get; set; } + /// Type of GitHub reference. [JsonPropertyName("referenceType")] public required UserMessageDataAttachmentsItemGithubReferenceReferenceType ReferenceType { get; set; } + /// Current state of the referenced item (e.g., open, closed, merged). [JsonPropertyName("state")] public required string State { get; set; } + /// URL to the referenced item on GitHub. [JsonPropertyName("url")] public required string Url { get; set; } } +/// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -1909,161 +2256,210 @@ public partial class UserMessageDataAttachmentsItemGithubReference : UserMessage [JsonDerivedType(typeof(UserMessageDataAttachmentsItemGithubReference), "github_reference")] public partial class UserMessageDataAttachmentsItem { + /// The type discriminator. [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } +/// Nested data type for AssistantMessageDataToolRequestsItem. public partial class AssistantMessageDataToolRequestsItem { + /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } + /// Name of the tool being invoked. [JsonPropertyName("name")] public required string Name { get; set; } + /// Arguments to pass to the tool, format depends on the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public object? Arguments { get; set; } + /// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("type")] public AssistantMessageDataToolRequestsItemType? Type { get; set; } } +/// Nested data type for AssistantUsageDataCopilotUsageTokenDetailsItem. public partial class AssistantUsageDataCopilotUsageTokenDetailsItem { + /// Number of tokens in this billing batch. [JsonPropertyName("batchSize")] public required double BatchSize { get; set; } + /// Cost per batch of tokens. [JsonPropertyName("costPerBatch")] public required double CostPerBatch { get; set; } + /// Total token count for this entry. [JsonPropertyName("tokenCount")] public required double TokenCount { get; set; } + /// Token category (e.g., "input", "output"). [JsonPropertyName("tokenType")] public required string TokenType { get; set; } } +/// Per-request cost and usage data from the CAPI copilot_usage response field. +/// Nested data type for AssistantUsageDataCopilotUsage. public partial class AssistantUsageDataCopilotUsage { + /// Itemized token usage breakdown. [JsonPropertyName("tokenDetails")] public required AssistantUsageDataCopilotUsageTokenDetailsItem[] TokenDetails { get; set; } + /// Total cost in nano-AIU (AI Units) for this request. [JsonPropertyName("totalNanoAiu")] public required double TotalNanoAiu { get; set; } } +/// The text variant of . public partial class ToolExecutionCompleteDataResultContentsItemText : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "text"; + /// The text content. [JsonPropertyName("text")] public required string Text { get; set; } } +/// The terminal variant of . public partial class ToolExecutionCompleteDataResultContentsItemTerminal : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "terminal"; + /// Terminal/shell output text. [JsonPropertyName("text")] public required string Text { get; set; } + /// Process exit code, if the command has completed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("exitCode")] public double? ExitCode { get; set; } + /// Working directory where the command was executed. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cwd")] public string? Cwd { get; set; } } +/// The image variant of . public partial class ToolExecutionCompleteDataResultContentsItemImage : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "image"; + /// Base64-encoded image data. [JsonPropertyName("data")] public required string Data { get; set; } + /// MIME type of the image (e.g., image/png, image/jpeg). [JsonPropertyName("mimeType")] public required string MimeType { get; set; } } +/// The audio variant of . public partial class ToolExecutionCompleteDataResultContentsItemAudio : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "audio"; + /// Base64-encoded audio data. [JsonPropertyName("data")] public required string Data { get; set; } + /// MIME type of the audio (e.g., audio/wav, audio/mpeg). [JsonPropertyName("mimeType")] public required string MimeType { get; set; } } +/// Nested data type for ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem. public partial class ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem { + /// URL or path to the icon image. [JsonPropertyName("src")] public required string Src { get; set; } + /// MIME type of the icon image. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mimeType")] public string? MimeType { get; set; } + /// Available icon sizes (e.g., ['16x16', '32x32']). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("sizes")] public string[]? Sizes { get; set; } + /// Theme variant this icon is intended for. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("theme")] public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme? Theme { get; set; } } +/// The resource_link variant of . public partial class ToolExecutionCompleteDataResultContentsItemResourceLink : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "resource_link"; + /// Icons associated with this resource. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("icons")] public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem[]? Icons { get; set; } + /// Resource name identifier. [JsonPropertyName("name")] public required string Name { get; set; } + /// Human-readable display title for the resource. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("title")] public string? Title { get; set; } + /// URI identifying the resource. [JsonPropertyName("uri")] public required string Uri { get; set; } + /// Human-readable description of the resource. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } + /// MIME type of the resource content. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mimeType")] public string? MimeType { get; set; } + /// Size of the resource in bytes. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("size")] public double? Size { get; set; } } +/// The resource variant of . public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolExecutionCompleteDataResultContentsItem { + /// [JsonIgnore] public override string Type => "resource"; + /// The embedded resource contents, either text or base64-encoded binary. [JsonPropertyName("resource")] public required object Resource { get; set; } } +/// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -2075,109 +2471,145 @@ public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolE [JsonDerivedType(typeof(ToolExecutionCompleteDataResultContentsItemResource), "resource")] public partial class ToolExecutionCompleteDataResultContentsItem { + /// The type discriminator. [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } +/// Tool execution result on success. +/// Nested data type for ToolExecutionCompleteDataResult. public partial class ToolExecutionCompleteDataResult { + /// Concise tool result text sent to the LLM for chat completion, potentially truncated for token efficiency. [JsonPropertyName("content")] public required string Content { get; set; } + /// Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("detailedContent")] public string? DetailedContent { get; set; } + /// Structured content blocks (text, images, audio, resources) returned by the tool in their native format. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("contents")] public ToolExecutionCompleteDataResultContentsItem[]? Contents { get; set; } } +/// Error details when the tool execution failed. +/// Nested data type for ToolExecutionCompleteDataError. public partial class ToolExecutionCompleteDataError { + /// Human-readable error message. [JsonPropertyName("message")] public required string Message { get; set; } + /// Machine-readable error code. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("code")] public string? Code { get; set; } } +/// Error details when the hook failed. +/// Nested data type for HookEndDataError. public partial class HookEndDataError { + /// Human-readable error message. [JsonPropertyName("message")] public required string Message { get; set; } + /// Error stack trace, when available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("stack")] public string? Stack { get; set; } } +/// Metadata about the prompt template and its construction. +/// Nested data type for SystemMessageDataMetadata. public partial class SystemMessageDataMetadata { + /// Version identifier of the prompt template used. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } + /// Template variables used when constructing the prompt. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("variables")] public Dictionary? Variables { get; set; } } +/// The agent_completed variant of . public partial class SystemNotificationDataKindAgentCompleted : SystemNotificationDataKind { + /// [JsonIgnore] public override string Type => "agent_completed"; + /// Unique identifier of the background agent. [JsonPropertyName("agentId")] public required string AgentId { get; set; } + /// Type of the agent (e.g., explore, task, general-purpose). [JsonPropertyName("agentType")] public required string AgentType { get; set; } + /// Whether the agent completed successfully or failed. [JsonPropertyName("status")] public required SystemNotificationDataKindAgentCompletedStatus Status { get; set; } + /// Human-readable description of the agent task. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } + /// The full prompt given to the background agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("prompt")] public string? Prompt { get; set; } } +/// The shell_completed variant of . public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind { + /// [JsonIgnore] public override string Type => "shell_completed"; + /// Unique identifier of the shell session. [JsonPropertyName("shellId")] public required string ShellId { get; set; } + /// Exit code of the shell command, if available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("exitCode")] public double? ExitCode { get; set; } + /// Human-readable description of the command. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } } +/// The shell_detached_completed variant of . public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNotificationDataKind { + /// [JsonIgnore] public override string Type => "shell_detached_completed"; + /// Unique identifier of the detached shell session. [JsonPropertyName("shellId")] public required string ShellId { get; set; } + /// Human-readable description of the command. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("description")] public string? Description { get; set; } } +/// Structured metadata identifying what triggered this notification. +/// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -2186,181 +2618,237 @@ public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNo [JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")] public partial class SystemNotificationDataKind { + /// The type discriminator. [JsonPropertyName("type")] public virtual string Type { get; set; } = string.Empty; } +/// Nested data type for PermissionRequestShellCommandsItem. public partial class PermissionRequestShellCommandsItem { + /// Command identifier (e.g., executable name). [JsonPropertyName("identifier")] public required string Identifier { get; set; } + /// Whether this command is read-only (no side effects). [JsonPropertyName("readOnly")] public required bool ReadOnly { get; set; } } +/// Nested data type for PermissionRequestShellPossibleUrlsItem. public partial class PermissionRequestShellPossibleUrlsItem { + /// URL that may be accessed by the command. [JsonPropertyName("url")] public required string Url { get; set; } } +/// The shell variant of . public partial class PermissionRequestShell : PermissionRequest { + /// [JsonIgnore] public override string Kind => "shell"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// The complete shell command text to be executed. [JsonPropertyName("fullCommandText")] public required string FullCommandText { get; set; } + /// Human-readable description of what the command intends to do. [JsonPropertyName("intention")] public required string Intention { get; set; } + /// Parsed command identifiers found in the command text. [JsonPropertyName("commands")] public required PermissionRequestShellCommandsItem[] Commands { get; set; } + /// File paths that may be read or written by the command. [JsonPropertyName("possiblePaths")] public required string[] PossiblePaths { get; set; } + /// URLs that may be accessed by the command. [JsonPropertyName("possibleUrls")] public required PermissionRequestShellPossibleUrlsItem[] PossibleUrls { get; set; } + /// Whether the command includes a file write redirection (e.g., > or >>). [JsonPropertyName("hasWriteFileRedirection")] public required bool HasWriteFileRedirection { get; set; } + /// Whether the UI can offer session-wide approval for this command pattern. [JsonPropertyName("canOfferSessionApproval")] public required bool CanOfferSessionApproval { get; set; } + /// Optional warning message about risks of running this command. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("warning")] public string? Warning { get; set; } } +/// The write variant of . public partial class PermissionRequestWrite : PermissionRequest { + /// [JsonIgnore] public override string Kind => "write"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Human-readable description of the intended file change. [JsonPropertyName("intention")] public required string Intention { get; set; } + /// Path of the file being written to. [JsonPropertyName("fileName")] public required string FileName { get; set; } + /// Unified diff showing the proposed changes. [JsonPropertyName("diff")] public required string Diff { get; set; } + /// Complete new file contents for newly created files. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("newFileContents")] public string? NewFileContents { get; set; } } +/// The read variant of . public partial class PermissionRequestRead : PermissionRequest { + /// [JsonIgnore] public override string Kind => "read"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Human-readable description of why the file is being read. [JsonPropertyName("intention")] public required string Intention { get; set; } + /// Path of the file or directory being read. [JsonPropertyName("path")] public required string Path { get; set; } } +/// The mcp variant of . public partial class PermissionRequestMcp : PermissionRequest { + /// [JsonIgnore] public override string Kind => "mcp"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Name of the MCP server providing the tool. [JsonPropertyName("serverName")] public required string ServerName { get; set; } + /// Internal name of the MCP tool. [JsonPropertyName("toolName")] public required string ToolName { get; set; } + /// Human-readable title of the MCP tool. [JsonPropertyName("toolTitle")] public required string ToolTitle { get; set; } + /// Arguments to pass to the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] public object? Args { get; set; } + /// Whether this MCP tool is read-only (no side effects). [JsonPropertyName("readOnly")] public required bool ReadOnly { get; set; } } +/// The url variant of . public partial class PermissionRequestUrl : PermissionRequest { + /// [JsonIgnore] public override string Kind => "url"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Human-readable description of why the URL is being accessed. [JsonPropertyName("intention")] public required string Intention { get; set; } + /// URL to be fetched. [JsonPropertyName("url")] public required string Url { get; set; } } +/// The memory variant of . public partial class PermissionRequestMemory : PermissionRequest { + /// [JsonIgnore] public override string Kind => "memory"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Topic or subject of the memory being stored. [JsonPropertyName("subject")] public required string Subject { get; set; } + /// The fact or convention being stored. [JsonPropertyName("fact")] public required string Fact { get; set; } + /// Source references for the stored fact. [JsonPropertyName("citations")] public required string Citations { get; set; } } +/// The custom-tool variant of . public partial class PermissionRequestCustomTool : PermissionRequest { + /// [JsonIgnore] public override string Kind => "custom-tool"; + /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolCallId")] public string? ToolCallId { get; set; } + /// Name of the custom tool. [JsonPropertyName("toolName")] public required string ToolName { get; set; } + /// Description of what the custom tool does. [JsonPropertyName("toolDescription")] public required string ToolDescription { get; set; } + /// Arguments to pass to the custom tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] public object? Args { get; set; } } +/// Details of the permission being requested. +/// Polymorphic base type discriminated by kind. [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -2373,139 +2861,188 @@ public partial class PermissionRequestCustomTool : PermissionRequest [JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")] public partial class PermissionRequest { + /// The type discriminator. [JsonPropertyName("kind")] public virtual string Kind { get; set; } = string.Empty; } +/// The result of the permission request. +/// Nested data type for PermissionCompletedDataResult. public partial class PermissionCompletedDataResult { + /// The outcome of the permission request. [JsonPropertyName("kind")] public required PermissionCompletedDataResultKind Kind { get; set; } } +/// JSON Schema describing the form fields to present to the user. +/// Nested data type for ElicitationRequestedDataRequestedSchema. public partial class ElicitationRequestedDataRequestedSchema { + /// Gets or sets the type value. [JsonPropertyName("type")] public required string Type { get; set; } + /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] public required Dictionary Properties { get; set; } + /// List of required field names. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("required")] public string[]? Required { get; set; } } +/// The type of operation performed on the plan file. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionPlanChangedDataOperation { + /// The create variant. [JsonStringEnumMemberName("create")] Create, + /// The update variant. [JsonStringEnumMemberName("update")] Update, + /// The delete variant. [JsonStringEnumMemberName("delete")] Delete, } +/// Whether the file was newly created or updated. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionWorkspaceFileChangedDataOperation { + /// The create variant. [JsonStringEnumMemberName("create")] Create, + /// The update variant. [JsonStringEnumMemberName("update")] Update, } +/// Origin type of the session being handed off. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionHandoffDataSourceType { + /// The remote variant. [JsonStringEnumMemberName("remote")] Remote, + /// The local variant. [JsonStringEnumMemberName("local")] Local, } +/// Whether the session ended normally ("routine") or due to a crash/fatal error ("error"). [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionShutdownDataShutdownType { + /// The routine variant. [JsonStringEnumMemberName("routine")] Routine, + /// The error variant. [JsonStringEnumMemberName("error")] Error, } +/// Type of GitHub reference. [JsonConverter(typeof(JsonStringEnumConverter))] public enum UserMessageDataAttachmentsItemGithubReferenceReferenceType { + /// The issue variant. [JsonStringEnumMemberName("issue")] Issue, + /// The pr variant. [JsonStringEnumMemberName("pr")] Pr, + /// The discussion variant. [JsonStringEnumMemberName("discussion")] Discussion, } +/// The agent mode that was active when this message was sent. [JsonConverter(typeof(JsonStringEnumConverter))] public enum UserMessageDataAgentMode { + /// The interactive variant. [JsonStringEnumMemberName("interactive")] Interactive, + /// The plan variant. [JsonStringEnumMemberName("plan")] Plan, + /// The autopilot variant. [JsonStringEnumMemberName("autopilot")] Autopilot, + /// The shell variant. [JsonStringEnumMemberName("shell")] Shell, } +/// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. [JsonConverter(typeof(JsonStringEnumConverter))] public enum AssistantMessageDataToolRequestsItemType { + /// The function variant. [JsonStringEnumMemberName("function")] Function, + /// The custom variant. [JsonStringEnumMemberName("custom")] Custom, } +/// Theme variant this icon is intended for. [JsonConverter(typeof(JsonStringEnumConverter))] public enum ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme { + /// The light variant. [JsonStringEnumMemberName("light")] Light, + /// The dark variant. [JsonStringEnumMemberName("dark")] Dark, } +/// Message role: "system" for system prompts, "developer" for developer-injected instructions. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SystemMessageDataRole { + /// The system variant. [JsonStringEnumMemberName("system")] System, + /// The developer variant. [JsonStringEnumMemberName("developer")] Developer, } +/// Whether the agent completed successfully or failed. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SystemNotificationDataKindAgentCompletedStatus { + /// The completed variant. [JsonStringEnumMemberName("completed")] Completed, + /// The failed variant. [JsonStringEnumMemberName("failed")] Failed, } +/// The outcome of the permission request. [JsonConverter(typeof(JsonStringEnumConverter))] public enum PermissionCompletedDataResultKind { + /// The approved variant. [JsonStringEnumMemberName("approved")] Approved, + /// The denied-by-rules variant. [JsonStringEnumMemberName("denied-by-rules")] DeniedByRules, + /// The denied-no-approval-rule-and-could-not-request-from-user variant. [JsonStringEnumMemberName("denied-no-approval-rule-and-could-not-request-from-user")] DeniedNoApprovalRuleAndCouldNotRequestFromUser, + /// The denied-interactively-by-user variant. [JsonStringEnumMemberName("denied-interactively-by-user")] DeniedInteractivelyByUser, + /// The denied-by-content-exclusion-policy variant. [JsonStringEnumMemberName("denied-by-content-exclusion-policy")] DeniedByContentExclusionPolicy, } diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index c72eb06df5..1b2f7612d6 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -44,6 +44,60 @@ function applyTypeRename(className: string): string { // ── C# utilities ──────────────────────────────────────────────────────────── +function escapeXml(text: string): string { + return text.replace(/&/g, "&").replace(//g, ">"); +} + +/** Ensures text ends with sentence-ending punctuation. */ +function ensureTrailingPunctuation(text: string): string { + const trimmed = text.trimEnd(); + if (/[.!?]$/.test(trimmed)) return trimmed; + return `${trimmed}.`; +} + +function xmlDocComment(description: string | undefined, indent: string): string[] { + if (!description) return []; + const escaped = ensureTrailingPunctuation(escapeXml(description.trim())); + const lines = escaped.split(/\r?\n/); + if (lines.length === 1) { + return [`${indent}/// ${lines[0]}`]; + } + return [ + `${indent}/// `, + ...lines.map((l) => `${indent}/// ${l}`), + `${indent}/// `, + ]; +} + +/** Like xmlDocComment but skips XML escaping — use only for codegen-controlled strings that already contain valid XML tags. */ +function rawXmlDocSummary(text: string, indent: string): string[] { + const line = ensureTrailingPunctuation(text.trim()); + return [`${indent}/// ${line}`]; +} + +/** Emits a summary (from description or fallback) and, when a real description exists, a remarks line with the fallback. */ +function xmlDocCommentWithFallback(description: string | undefined, fallback: string, indent: string): string[] { + if (description) { + return [ + ...xmlDocComment(description, indent), + `${indent}/// ${ensureTrailingPunctuation(fallback)}`, + ]; + } + return rawXmlDocSummary(fallback, indent); +} + +/** Emits a summary from the schema description, or a fallback naming the property by its JSON key. */ +function xmlDocPropertyComment(description: string | undefined, jsonPropName: string, indent: string): string[] { + if (description) return xmlDocComment(description, indent); + return rawXmlDocSummary(`Gets or sets the ${escapeXml(jsonPropName)} value.`, indent); +} + +/** Emits a summary from the schema description, or a generic fallback. */ +function xmlDocEnumComment(description: string | undefined, indent: string): string[] { + if (description) return xmlDocComment(description, indent); + return rawXmlDocSummary(`Defines the allowed values.`, indent); +} + function toPascalCase(name: string): string { if (name.includes("_") || name.includes("-")) { return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(""); @@ -139,11 +193,12 @@ interface EventVariant { className: string; dataClassName: string; dataSchema: JSONSchema7; + dataDescription?: string; } let generatedEnums = new Map(); -function getOrCreateEnum(parentClassName: string, propName: string, values: string[], enumOutput: string[]): string { +function getOrCreateEnum(parentClassName: string, propName: string, values: string[], enumOutput: string[], description?: string): string { const valuesKey = [...values].sort().join("|"); for (const [, existing] of generatedEnums) { if ([...existing.values].sort().join("|") === valuesKey) return existing.enumName; @@ -151,8 +206,11 @@ function getOrCreateEnum(parentClassName: string, propName: string, values: stri const enumName = `${parentClassName}${propName}`; generatedEnums.set(enumName, { enumName, values }); - const lines = [`[JsonConverter(typeof(JsonStringEnumConverter<${enumName}>))]`, `public enum ${enumName}`, `{`]; + const lines: string[] = []; + lines.push(...xmlDocEnumComment(description, "")); + lines.push(`[JsonConverter(typeof(JsonStringEnumConverter<${enumName}>))]`, `public enum ${enumName}`, `{`); for (const value of values) { + lines.push(` /// The ${escapeXml(value)} variant.`); lines.push(` [JsonStringEnumMemberName("${value}")]`, ` ${toPascalCaseEnumMember(value)},`); } lines.push(`}`, ""); @@ -171,11 +229,13 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { const typeName = typeSchema?.const as string; if (!typeName) throw new Error("Variant must have type.const"); const baseName = typeToClassName(typeName); + const dataSchema = variant.properties.data as JSONSchema7; return { typeName, className: `${baseName}Event`, dataClassName: `${baseName}Data`, - dataSchema: variant.properties.data as JSONSchema7, + dataSchema, + dataDescription: dataSchema?.description, }; }) .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName)); @@ -222,12 +282,14 @@ function generatePolymorphicClasses( variants: JSONSchema7[], knownTypes: Map, nestedClasses: Map, - enumOutput: string[] + enumOutput: string[], + description?: string ): string { const lines: string[] = []; const discriminatorInfo = findDiscriminator(variants)!; const renamedBase = applyTypeRename(baseClassName); + lines.push(...xmlDocCommentWithFallback(description, `Polymorphic base type discriminated by ${escapeXml(discriminatorProperty)}.`, "")); lines.push(`[JsonPolymorphic(`); lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`); lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`); @@ -239,6 +301,7 @@ function generatePolymorphicClasses( lines.push(`public partial class ${renamedBase}`); lines.push(`{`); + lines.push(` /// The type discriminator.`); lines.push(` [JsonPropertyName("${discriminatorProperty}")]`); lines.push(` public virtual string ${toPascalCase(discriminatorProperty)} { get; set; } = string.Empty;`); lines.push(`}`); @@ -269,8 +332,10 @@ function generateDerivedClass( const lines: string[] = []; const required = new Set(schema.required || []); + lines.push(...xmlDocCommentWithFallback(schema.description, `The ${escapeXml(discriminatorValue)} variant of .`, "")); lines.push(`public partial class ${className} : ${baseClassName}`); lines.push(`{`); + lines.push(` /// `); lines.push(` [JsonIgnore]`); lines.push(` public override string ${toPascalCase(discriminatorProperty)} => "${discriminatorValue}";`); lines.push(""); @@ -284,6 +349,7 @@ function generateDerivedClass( const csharpName = toPascalCase(propName); const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); + lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -304,7 +370,9 @@ function generateNestedClass( enumOutput: string[] ): string { const required = new Set(schema.required || []); - const lines = [`public partial class ${className}`, `{`]; + const lines: string[] = []; + lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, "")); + lines.push(`public partial class ${className}`, `{`); for (const [propName, propSchema] of Object.entries(schema.properties || {})) { if (typeof propSchema !== "object") continue; @@ -313,6 +381,7 @@ function generateNestedClass( const csharpName = toPascalCase(propName); const csharpType = resolveSessionPropertyType(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); + lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -345,7 +414,7 @@ function resolveSessionPropertyType( if (discriminatorInfo) { const baseClassName = `${parentClassName}${propName}`; const renamedBase = applyTypeRename(baseClassName); - const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput); + const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, propSchema.description); nestedClasses.set(renamedBase, polymorphicCode); return isRequired && !hasNull ? renamedBase : `${renamedBase}?`; } @@ -353,7 +422,7 @@ function resolveSessionPropertyType( return hasNull || !isRequired ? "object?" : "object"; } if (propSchema.enum && Array.isArray(propSchema.enum)) { - const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput); + const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description); return isRequired ? enumName : `${enumName}?`; } if (propSchema.type === "object" && propSchema.properties) { @@ -370,7 +439,7 @@ function resolveSessionPropertyType( if (discriminatorInfo) { const baseClassName = `${parentClassName}${propName}Item`; const renamedBase = applyTypeRename(baseClassName); - const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput); + const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, items.description); nestedClasses.set(renamedBase, polymorphicCode); return isRequired ? `${renamedBase}[]` : `${renamedBase}[]?`; } @@ -381,7 +450,7 @@ function resolveSessionPropertyType( return isRequired ? `${itemClassName}[]` : `${itemClassName}[]?`; } if (items.enum && Array.isArray(items.enum)) { - const enumName = getOrCreateEnum(parentClassName, `${propName}Item`, items.enum as string[], enumOutput); + const enumName = getOrCreateEnum(parentClassName, `${propName}Item`, items.enum as string[], enumOutput, items.description); return isRequired ? `${enumName}[]` : `${enumName}[]?`; } const itemType = schemaTypeToCSharp(items, true, knownTypes); @@ -394,7 +463,13 @@ function generateDataClass(variant: EventVariant, knownTypes: Map.`, "")); + } + lines.push(`public partial class ${variant.dataClassName}`, `{`); for (const [propName, propSchema] of Object.entries(variant.dataSchema.properties)) { if (typeof propSchema !== "object") continue; @@ -402,6 +477,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map(); const enumOutput: string[] = []; + // Extract descriptions for base class properties from the first variant + const firstVariant = (schema.definitions?.SessionEvent as JSONSchema7)?.anyOf?.[0]; + const baseProps = typeof firstVariant === "object" && firstVariant?.properties ? firstVariant.properties : {}; + const baseDesc = (name: string) => { + const prop = baseProps[name]; + return typeof prop === "object" ? (prop as JSONSchema7).description : undefined; + }; + const lines: string[] = []; lines.push(`${COPYRIGHT} // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json -// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. -#pragma warning disable CS1591 - using System.Text.Json; using System.Text.Json.Serialization; @@ -436,25 +517,41 @@ namespace GitHub.Copilot.SDK; // Base class with XML doc lines.push(`/// `); - lines.push(`/// Base class for all session events with polymorphic JSON serialization.`); + lines.push(`/// Provides the base class from which all session events derive.`); lines.push(`/// `); lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]`); for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) { lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`); } - lines.push(`public abstract partial class SessionEvent`, `{`, ` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, ""); + lines.push(`public abstract partial class SessionEvent`, `{`); + lines.push(...xmlDocComment(baseDesc("id"), " ")); + lines.push(` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, ""); + lines.push(...xmlDocComment(baseDesc("timestamp"), " ")); lines.push(` [JsonPropertyName("timestamp")]`, ` public DateTimeOffset Timestamp { get; set; }`, ""); + lines.push(...xmlDocComment(baseDesc("parentId"), " ")); lines.push(` [JsonPropertyName("parentId")]`, ` public Guid? ParentId { get; set; }`, ""); + lines.push(...xmlDocComment(baseDesc("ephemeral"), " ")); lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, ` [JsonPropertyName("ephemeral")]`, ` public bool? Ephemeral { get; set; }`, ""); lines.push(` /// `, ` /// The event type discriminator.`, ` /// `); lines.push(` [JsonIgnore]`, ` public abstract string Type { get; }`, ""); + lines.push(` /// Deserializes a JSON string into a .`); lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, ""); + lines.push(` /// Serializes this event to a JSON string.`); lines.push(` public string ToJson() =>`, ` JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent);`, `}`, ""); // Event classes with XML docs for (const variant of variants) { - lines.push(`/// `, `/// Event: ${variant.typeName}`, `/// `); - lines.push(`public partial class ${variant.className} : SessionEvent`, `{`, ` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); + const remarksLine = `/// Represents the ${escapeXml(variant.typeName)} event.`; + if (variant.dataDescription) { + lines.push(...xmlDocComment(variant.dataDescription, "")); + lines.push(remarksLine); + } else { + lines.push(`/// Represents the ${escapeXml(variant.typeName)} event.`); + } + lines.push(`public partial class ${variant.className} : SessionEvent`, `{`); + lines.push(` /// `); + lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); + lines.push(` /// The ${escapeXml(variant.typeName)} event payload.`); lines.push(` [JsonPropertyName("data")]`, ` public required ${variant.dataClassName} Data { get; set; }`, `}`, ""); } @@ -512,7 +609,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam } // Handle enums (string unions like "interactive" | "plan" | "autopilot") if (schema.enum && Array.isArray(schema.enum)) { - const enumName = getOrCreateEnum(parentClassName, propName, schema.enum as string[], rpcEnumOutput); + const enumName = getOrCreateEnum(parentClassName, propName, schema.enum as string[], rpcEnumOutput, schema.description); return isRequired ? enumName : `${enumName}?`; } if (schema.type === "object" && schema.properties) { @@ -549,7 +646,7 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi const requiredSet = new Set(schema.required || []); const lines: string[] = []; - if (schema.description) lines.push(`/// ${schema.description}`); + lines.push(...xmlDocComment(schema.description || `RPC data type for ${className.replace(/Request$/, "").replace(/Result$/, "")} operations.`, "")); lines.push(`${visibility} class ${className}`, `{`); const props = Object.entries(schema.properties || {}); @@ -561,7 +658,7 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi const csharpName = toPascalCase(propName); const csharpType = resolveRpcType(prop, isReq, className, csharpName, extraClasses); - if (prop.description && visibility === "public") lines.push(` /// ${prop.description}`); + lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(` [JsonPropertyName("${propName}")]`); let defaultVal = ""; @@ -591,7 +688,7 @@ function emitServerRpcClasses(node: Record, classes: string[]): // ServerRpc class const srLines: string[] = []; - srLines.push(`/// Typed server-scoped RPC methods (no session required).`); + srLines.push(`/// Provides server-scoped RPC methods (no session required).`); srLines.push(`public class ServerRpc`); srLines.push(`{`); srLines.push(` private readonly JsonRpc _rpc;`); @@ -631,7 +728,7 @@ function emitServerRpcClasses(node: Record, classes: string[]): function emitServerApiClass(className: string, node: Record, classes: string[]): string { const lines: string[] = []; const displayName = className.replace(/^Server/, "").replace(/Api$/, ""); - lines.push(`/// Server-scoped ${displayName} APIs.`); + lines.push(`/// Provides server-scoped ${displayName} APIs.`); lines.push(`public class ${className}`); lines.push(`{`); lines.push(` private readonly JsonRpc _rpc;`); @@ -703,11 +800,11 @@ function emitSessionRpcClasses(node: Record, classes: string[]) const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); - const srLines = [`/// Typed session-scoped RPC methods.`, `public class SessionRpc`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; + const srLines = [`/// Provides typed session-scoped RPC methods.`, `public class SessionRpc`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; srLines.push(` internal SessionRpc(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`); for (const [groupName] of groups) srLines.push(` ${toPascalCase(groupName)} = new ${toPascalCase(groupName)}Api(rpc, sessionId);`); srLines.push(` }`); - for (const [groupName] of groups) srLines.push("", ` public ${toPascalCase(groupName)}Api ${toPascalCase(groupName)} { get; }`); + for (const [groupName] of groups) srLines.push("", ` /// ${toPascalCase(groupName)} APIs.`, ` public ${toPascalCase(groupName)}Api ${toPascalCase(groupName)} { get; }`); // Emit top-level session RPC methods directly on the SessionRpc class const topLevelLines: string[] = []; @@ -766,7 +863,8 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas } function emitSessionApiClass(className: string, node: Record, classes: string[]): string { - const lines = [`public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; + const displayName = className.replace(/Api$/, ""); + const lines = [`/// Provides session-scoped ${displayName} APIs.`, `public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; lines.push(` internal ${className}(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`, ` }`); for (const [key, value] of Object.entries(node)) { @@ -796,9 +894,6 @@ function generateRpcCode(schema: ApiSchema): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json -// Generated code does not have XML doc comments; suppress CS1591 to avoid warnings. -#pragma warning disable CS1591 - using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; From 723560972ecce16566739cdaf10e00c11b9a15f0 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 9 Mar 2026 23:29:44 -0400 Subject: [PATCH 19/61] Use lazy property initialization in C# RPC classes (#725) Switched property initializations to lazy accessors for lists, dictionaries, and custom types in C# RPC classes. Updated codegen in csharp.ts to emit these accessors, improving memory usage and consistency. --- dotnet/src/Generated/Rpc.cs | 18 +++++++++--------- scripts/codegen/csharp.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 7cc6bdacaf..2e5d164b7c 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -68,11 +68,11 @@ public class ModelCapabilities { /// Gets or sets the supports value. [JsonPropertyName("supports")] - public ModelCapabilitiesSupports Supports { get; set; } = new(); + public ModelCapabilitiesSupports Supports { get => field ??= new(); set; } /// Gets or sets the limits value. [JsonPropertyName("limits")] - public ModelCapabilitiesLimits Limits { get; set; } = new(); + public ModelCapabilitiesLimits Limits { get => field ??= new(); set; } } /// Policy state (if applicable). @@ -108,7 +108,7 @@ public class Model /// Model capabilities and limits. [JsonPropertyName("capabilities")] - public ModelCapabilities Capabilities { get; set; } = new(); + public ModelCapabilities Capabilities { get => field ??= new(); set; } /// Policy state (if applicable). [JsonPropertyName("policy")] @@ -132,7 +132,7 @@ public class ModelsListResult { /// List of available models with full metadata. [JsonPropertyName("models")] - public List Models { get; set; } = []; + public List Models { get => field ??= []; set; } } /// RPC data type for Tool operations. @@ -164,7 +164,7 @@ public class ToolsListResult { /// List of available built-in tools with metadata. [JsonPropertyName("tools")] - public List Tools { get; set; } = []; + public List Tools { get => field ??= []; set; } } /// RPC data type for ToolsList operations. @@ -208,7 +208,7 @@ public class AccountGetQuotaResult { /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions). [JsonPropertyName("quotaSnapshots")] - public Dictionary QuotaSnapshots { get; set; } = []; + public Dictionary QuotaSnapshots { get => field ??= []; set; } } /// RPC data type for SessionLog operations. @@ -374,7 +374,7 @@ public class SessionWorkspaceListFilesResult { /// Relative file paths in the workspace files directory. [JsonPropertyName("files")] - public List Files { get; set; } = []; + public List Files { get => field ??= []; set; } } /// RPC data type for SessionWorkspaceListFiles operations. @@ -467,7 +467,7 @@ public class SessionAgentListResult { /// Available custom agents. [JsonPropertyName("agents")] - public List Agents { get; set; } = []; + public List Agents { get => field ??= []; set; } } /// RPC data type for SessionAgentList operations. @@ -531,7 +531,7 @@ public class SessionAgentSelectResult { /// The newly selected custom agent. [JsonPropertyName("agent")] - public SessionAgentSelectResultAgent Agent { get; set; } = new(); + public SessionAgentSelectResultAgent Agent { get => field ??= new(); set; } } /// RPC data type for SessionAgentSelect operations. diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 1b2f7612d6..e667c28a54 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -662,13 +662,17 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi lines.push(` [JsonPropertyName("${propName}")]`); let defaultVal = ""; + let propAccessors = "{ get; set; }"; if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; else if (csharpType === "object") defaultVal = " = null!;"; - else if (csharpType.startsWith("List<") || csharpType.startsWith("Dictionary<")) defaultVal = " = [];"; - else if (emittedRpcClasses.has(csharpType)) defaultVal = " = new();"; + else if (csharpType.startsWith("List<") || csharpType.startsWith("Dictionary<")) { + propAccessors = "{ get => field ??= []; set; }"; + } else if (emittedRpcClasses.has(csharpType)) { + propAccessors = "{ get => field ??= new(); set; }"; + } } - lines.push(` public ${csharpType} ${csharpName} { get; set; }${defaultVal}`); + lines.push(` public ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); if (i < props.length - 1) lines.push(""); } lines.push(`}`); From 4125fe76c77e408b84f396f55c9b82dd43353ddc Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 10 Mar 2026 06:53:32 -0400 Subject: [PATCH 20/61] Register sessions before RPC and add SessionConfig.OnEvent (#664) Register sessions in the client's sessions map before issuing the session.create and session.resume RPC calls, so that events emitted by the CLI during the RPC (e.g. session.start, permission requests, tool calls) are not dropped. Previously, sessions were registered only after the RPC completed, creating a window where notifications for the session had no target. The session ID is now generated client-side (via UUID) rather than extracted from the server response. On RPC failure, the session is cleaned up from the map. Add a new OnEvent property to each SDK's session configuration (SessionConfig / ResumeSessionConfig) that registers an event handler on the session before the create/resume RPC is issued. This guarantees that early events emitted by the CLI during session creation (e.g. session.start) are delivered to the handler, unlike calling On() after createSession() returns. Changes across all four SDKs (Node.js, Python, Go, .NET): - Generate sessionId client-side before the RPC - Create and register the session in the sessions map before the RPC - Set workspacePath from the RPC response after it completes - Remove the session from the map if the RPC fails - Add OnEvent/on_event config property wired up before the RPC Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 151 +++++++++------- dotnet/src/Session.cs | 2 +- dotnet/src/Types.cs | 20 +++ dotnet/test/SessionTests.cs | 12 +- go/client.go | 78 +++++--- go/go.mod | 2 + go/go.sum | 2 + go/internal/e2e/session_test.go | 26 ++- go/types.go | 13 +- nodejs/src/client.ts | 169 ++++++++++-------- nodejs/src/session.ts | 2 +- nodejs/src/types.ts | 12 ++ nodejs/test/e2e/session.test.ts | 14 +- python/copilot/client.py | 44 +++-- python/copilot/types.py | 9 +- python/e2e/test_session.py | 16 +- test/scenarios/auth/byok-anthropic/go/go.mod | 5 +- test/scenarios/auth/byok-anthropic/go/go.sum | 2 + test/scenarios/auth/byok-azure/go/go.mod | 5 +- test/scenarios/auth/byok-azure/go/go.sum | 2 + test/scenarios/auth/byok-ollama/go/go.mod | 5 +- test/scenarios/auth/byok-ollama/go/go.sum | 2 + test/scenarios/auth/byok-openai/go/go.mod | 5 +- test/scenarios/auth/byok-openai/go/go.sum | 2 + test/scenarios/auth/gh-app/go/go.mod | 5 +- test/scenarios/auth/gh-app/go/go.sum | 2 + .../bundling/app-backend-to-server/go/go.mod | 5 +- .../bundling/app-backend-to-server/go/go.sum | 2 + .../bundling/app-direct-server/go/go.mod | 5 +- .../bundling/app-direct-server/go/go.sum | 2 + .../bundling/container-proxy/go/go.mod | 5 +- .../bundling/container-proxy/go/go.sum | 2 + .../bundling/fully-bundled/go/go.mod | 5 +- .../bundling/fully-bundled/go/go.sum | 2 + test/scenarios/callbacks/hooks/go/go.mod | 5 +- test/scenarios/callbacks/hooks/go/go.sum | 2 + .../scenarios/callbacks/permissions/go/go.mod | 5 +- .../scenarios/callbacks/permissions/go/go.sum | 2 + test/scenarios/callbacks/user-input/go/go.mod | 5 +- test/scenarios/callbacks/user-input/go/go.sum | 2 + test/scenarios/modes/default/go/go.mod | 5 +- test/scenarios/modes/default/go/go.sum | 2 + test/scenarios/modes/minimal/go/go.mod | 5 +- test/scenarios/modes/minimal/go/go.sum | 2 + test/scenarios/prompts/attachments/go/go.mod | 5 +- test/scenarios/prompts/attachments/go/go.sum | 2 + .../prompts/reasoning-effort/go/go.mod | 5 +- .../prompts/reasoning-effort/go/go.sum | 2 + .../prompts/system-message/go/go.mod | 5 +- .../prompts/system-message/go/go.sum | 2 + .../sessions/concurrent-sessions/go/go.mod | 5 +- .../sessions/concurrent-sessions/go/go.sum | 2 + .../sessions/infinite-sessions/go/go.mod | 5 +- .../sessions/infinite-sessions/go/go.sum | 2 + .../sessions/session-resume/go/go.mod | 5 +- .../sessions/session-resume/go/go.sum | 2 + test/scenarios/sessions/streaming/go/go.mod | 5 +- test/scenarios/sessions/streaming/go/go.sum | 2 + test/scenarios/tools/custom-agents/go/go.mod | 5 +- test/scenarios/tools/custom-agents/go/go.sum | 2 + test/scenarios/tools/mcp-servers/go/go.mod | 5 +- test/scenarios/tools/mcp-servers/go/go.sum | 2 + test/scenarios/tools/no-tools/go/go.mod | 5 +- test/scenarios/tools/no-tools/go/go.sum | 2 + test/scenarios/tools/skills/go/go.mod | 5 +- test/scenarios/tools/skills/go/go.sum | 2 + test/scenarios/tools/tool-filtering/go/go.mod | 5 +- test/scenarios/tools/tool-filtering/go/go.sum | 2 + test/scenarios/tools/tool-overrides/go/go.mod | 5 +- test/scenarios/tools/tool-overrides/go/go.sum | 2 + .../tools/virtual-filesystem/go/go.mod | 5 +- .../tools/virtual-filesystem/go/go.sum | 2 + test/scenarios/transport/reconnect/go/go.mod | 5 +- test/scenarios/transport/reconnect/go/go.sum | 2 + test/scenarios/transport/stdio/go/go.mod | 5 +- test/scenarios/transport/stdio/go/go.sum | 2 + test/scenarios/transport/tcp/go/go.mod | 5 +- test/scenarios/transport/tcp/go/go.sum | 2 + 78 files changed, 580 insertions(+), 209 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 1b4da2ffbe..5b7474a64e 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -403,34 +403,11 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); - var request = new CreateSessionRequest( - config.Model, - config.SessionId, - config.ClientName, - config.ReasoningEffort, - config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, - config.AvailableTools, - config.ExcludedTools, - config.Provider, - (bool?)true, - config.OnUserInputRequest != null ? true : null, - hasHooks ? true : null, - config.WorkingDirectory, - config.Streaming is true ? true : null, - config.McpServers, - "direct", - config.CustomAgents, - config.Agent, - config.ConfigDir, - config.SkillDirectories, - config.DisabledSkills, - config.InfiniteSessions); - - var response = await InvokeRpcAsync( - connection.Rpc, "session.create", [request], cancellationToken); - - var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); + var sessionId = config.SessionId ?? Guid.NewGuid().ToString(); + + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + var session = new CopilotSession(sessionId, connection.Rpc); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) @@ -441,10 +418,47 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { session.RegisterHooks(config.Hooks); } + if (config.OnEvent != null) + { + session.On(config.OnEvent); + } + _sessions[sessionId] = session; - if (!_sessions.TryAdd(response.SessionId, session)) + try { - throw new InvalidOperationException($"Session {response.SessionId} already exists"); + var request = new CreateSessionRequest( + config.Model, + sessionId, + config.ClientName, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, + (bool?)true, + config.OnUserInputRequest != null ? true : null, + hasHooks ? true : null, + config.WorkingDirectory, + config.Streaming is true ? true : null, + config.McpServers, + "direct", + config.CustomAgents, + config.Agent, + config.ConfigDir, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); + + var response = await InvokeRpcAsync( + connection.Rpc, "session.create", [request], cancellationToken); + + session.WorkspacePath = response.WorkspacePath; + } + catch + { + _sessions.TryRemove(sessionId, out _); + throw; } return session; @@ -495,35 +509,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); - var request = new ResumeSessionRequest( - sessionId, - config.ClientName, - config.Model, - config.ReasoningEffort, - config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, - config.AvailableTools, - config.ExcludedTools, - config.Provider, - (bool?)true, - config.OnUserInputRequest != null ? true : null, - hasHooks ? true : null, - config.WorkingDirectory, - config.ConfigDir, - config.DisableResume is true ? true : null, - config.Streaming is true ? true : null, - config.McpServers, - "direct", - config.CustomAgents, - config.Agent, - config.SkillDirectories, - config.DisabledSkills, - config.InfiniteSessions); - - var response = await InvokeRpcAsync( - connection.Rpc, "session.resume", [request], cancellationToken); - - var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + var session = new CopilotSession(sessionId, connection.Rpc); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) @@ -534,9 +522,50 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { session.RegisterHooks(config.Hooks); } + if (config.OnEvent != null) + { + session.On(config.OnEvent); + } + _sessions[sessionId] = session; + + try + { + var request = new ResumeSessionRequest( + sessionId, + config.ClientName, + config.Model, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, + (bool?)true, + config.OnUserInputRequest != null ? true : null, + hasHooks ? true : null, + config.WorkingDirectory, + config.ConfigDir, + config.DisableResume is true ? true : null, + config.Streaming is true ? true : null, + config.McpServers, + "direct", + config.CustomAgents, + config.Agent, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); + + var response = await InvokeRpcAsync( + connection.Rpc, "session.resume", [request], cancellationToken); + + session.WorkspacePath = response.WorkspacePath; + } + catch + { + _sessions.TryRemove(sessionId, out _); + throw; + } - // Replace any existing session entry to ensure new config (like permission handler) is used - _sessions[response.SessionId] = session; return session; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index b9d70a2ab6..324b3df6df 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -86,7 +86,7 @@ public sealed partial class CopilotSession : IAsyncDisposable /// The path to the workspace containing checkpoints/, plan.md, and files/ subdirectories, /// or null if infinite sessions are disabled. /// - public string? WorkspacePath { get; } + public string? WorkspacePath { get; internal set; } /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 4d268434ea..633a976548 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1183,6 +1183,7 @@ protected SessionConfig(SessionConfig? other) ? new Dictionary(other.McpServers, other.McpServers.Comparer) : null; Model = other.Model; + OnEvent = other.OnEvent; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -1307,6 +1308,18 @@ protected SessionConfig(SessionConfig? other) /// public InfiniteSessionConfig? InfiniteSessions { get; set; } + /// + /// Optional event handler that is registered on the session before the + /// session.create RPC is issued. + /// + /// + /// Equivalent to calling immediately + /// after creation, but executes earlier in the lifecycle so no events are missed. + /// Using this property rather than guarantees that early events emitted + /// by the CLI during session creation (e.g. session.start) are delivered to the handler. + /// + public SessionEventHandler? OnEvent { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -1355,6 +1368,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) ? new Dictionary(other.McpServers, other.McpServers.Comparer) : null; Model = other.Model; + OnEvent = other.OnEvent; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -1482,6 +1496,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public InfiniteSessionConfig? InfiniteSessions { get; set; } + /// + /// Optional event handler registered before the session.resume RPC is issued, + /// ensuring early events are delivered. See . + /// + public SessionEventHandler? OnEvent { get; set; } + /// /// Creates a shallow clone of this instance. /// diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 20d6f3ac50..8004395846 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -245,7 +245,17 @@ await session.SendAsync(new MessageOptions [Fact] public async Task Should_Receive_Session_Events() { - var session = await CreateSessionAsync(); + // Use OnEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + var earlyEvents = new List(); + var session = await CreateSessionAsync(new SessionConfig + { + OnEvent = evt => earlyEvents.Add(evt), + }); + + Assert.Contains(earlyEvents, evt => evt is SessionStartEvent); + var receivedEvents = new List(); var idleReceived = new TaskCompletionSource(); diff --git a/go/client.go b/go/client.go index d440b49b40..021de2b142 100644 --- a/go/client.go +++ b/go/client.go @@ -44,6 +44,8 @@ import ( "sync/atomic" "time" + "github.com/google/uuid" + "github.com/github/copilot-sdk/go/internal/embeddedcli" "github.com/github/copilot-sdk/go/internal/jsonrpc2" "github.com/github/copilot-sdk/go/rpc" @@ -493,7 +495,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req := createSessionRequest{} req.Model = config.Model - req.SessionID = config.SessionID req.ClientName = config.ClientName req.ReasoningEffort = config.ReasoningEffort req.ConfigDir = config.ConfigDir @@ -527,17 +528,15 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } req.RequestPermission = Bool(true) - result, err := c.client.Request("session.create", req) - if err != nil { - return nil, fmt.Errorf("failed to create session: %w", err) - } - - var response createSessionResponse - if err := json.Unmarshal(result, &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) + sessionID := config.SessionID + if sessionID == "" { + sessionID = uuid.New().String() } + req.SessionID = sessionID - session := newSession(response.SessionID, c.client, response.WorkspacePath) + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + session := newSession(sessionID, c.client, "") session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) @@ -547,11 +546,32 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.Hooks != nil { session.registerHooks(config.Hooks) } + if config.OnEvent != nil { + session.On(config.OnEvent) + } c.sessionsMux.Lock() - c.sessions[response.SessionID] = session + c.sessions[sessionID] = session c.sessionsMux.Unlock() + result, err := c.client.Request("session.create", req) + if err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to create session: %w", err) + } + + var response createSessionResponse + if err := json.Unmarshal(result, &response); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + session.workspacePath = response.WorkspacePath + return session, nil } @@ -627,17 +647,10 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.InfiniteSessions = config.InfiniteSessions req.RequestPermission = Bool(true) - result, err := c.client.Request("session.resume", req) - if err != nil { - return nil, fmt.Errorf("failed to resume session: %w", err) - } + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + session := newSession(sessionID, c.client, "") - var response resumeSessionResponse - if err := json.Unmarshal(result, &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - session := newSession(response.SessionID, c.client, response.WorkspacePath) session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) if config.OnUserInputRequest != nil { @@ -646,11 +659,32 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.Hooks != nil { session.registerHooks(config.Hooks) } + if config.OnEvent != nil { + session.On(config.OnEvent) + } c.sessionsMux.Lock() - c.sessions[response.SessionID] = session + c.sessions[sessionID] = session c.sessionsMux.Unlock() + result, err := c.client.Request("session.resume", req) + if err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to resume session: %w", err) + } + + var response resumeSessionResponse + if err := json.Unmarshal(result, &response); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + session.workspacePath = response.WorkspacePath + return session, nil } diff --git a/go/go.mod b/go/go.mod index c835cc889e..4895825450 100644 --- a/go/go.mod +++ b/go/go.mod @@ -6,3 +6,5 @@ require ( github.com/google/jsonschema-go v0.4.2 github.com/klauspost/compress v1.18.3 ) + +require github.com/google/uuid v1.6.0 diff --git a/go/go.sum b/go/go.sum index 0cc670e8f1..2ae02ef350 100644 --- a/go/go.sum +++ b/go/go.sum @@ -2,5 +2,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 8da66cdd2f..40f62d4c6d 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -588,11 +588,31 @@ func TestSession(t *testing.T) { t.Run("should receive session events", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) + // Use OnEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + var earlyEvents []copilot.SessionEvent + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + OnEvent: func(event copilot.SessionEvent) { + earlyEvents = append(earlyEvents, event) + }, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } + hasSessionStart := false + for _, evt := range earlyEvents { + if evt.Type == "session.start" { + hasSessionStart = true + break + } + } + if !hasSessionStart { + t.Error("Expected session.start event via OnEvent during creation") + } + var receivedEvents []copilot.SessionEvent idle := make(chan bool) @@ -737,10 +757,10 @@ func TestSession(t *testing.T) { // Verify both sessions are in the list if !contains(sessionIDs, session1.SessionID) { - t.Errorf("Expected session1 ID %s to be in sessions list", session1.SessionID) + t.Errorf("Expected session1 ID %s to be in sessions list %v", session1.SessionID, sessionIDs) } if !contains(sessionIDs, session2.SessionID) { - t.Errorf("Expected session2 ID %s to be in sessions list", session2.SessionID) + t.Errorf("Expected session2 ID %s to be in sessions list %v", session2.SessionID, sessionIDs) } // Verify session metadata structure diff --git a/go/types.go b/go/types.go index eaee2fb111..a139f294f4 100644 --- a/go/types.go +++ b/go/types.go @@ -402,9 +402,13 @@ type SessionConfig struct { // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. // When enabled (default), sessions automatically manage context limits and persist state. InfiniteSessions *InfiniteSessionConfig + // OnEvent is an optional event handler that is registered on the session before + // the session.create RPC is issued. This guarantees that early events emitted + // by the CLI during session creation (e.g. session.start) are delivered to the + // handler. Equivalent to calling session.On(handler) immediately after creation, + // but executes earlier in the lifecycle so no events are missed. + OnEvent SessionEventHandler } - -// Tool describes a caller-implemented tool that can be invoked by Copilot type Tool struct { Name string `json:"name"` Description string `json:"description,omitempty"` @@ -490,9 +494,10 @@ type ResumeSessionConfig struct { // DisableResume, when true, skips emitting the session.resume event. // Useful for reconnecting to a session without triggering resume-related side effects. DisableResume bool + // OnEvent is an optional event handler registered before the session.resume RPC + // is issued, ensuring early events are delivered. See SessionConfig.OnEvent. + OnEvent SessionEventHandler } - -// ProviderConfig configures a custom model provider type ProviderConfig struct { // Type is the provider type: "openai", "azure", or "anthropic". Defaults to "openai". Type string `json:"type,omitempty"` diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index b94c0a5a63..bd4cc19606 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -12,6 +12,7 @@ */ import { spawn, type ChildProcess } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { Socket } from "node:net"; import { dirname, join } from "node:path"; @@ -546,41 +547,11 @@ export class CopilotClient { } } - const response = await this.connection!.sendRequest("session.create", { - model: config.model, - sessionId: config.sessionId, - clientName: config.clientName, - reasoningEffort: config.reasoningEffort, - tools: config.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - parameters: toJsonSchema(tool.parameters), - overridesBuiltInTool: tool.overridesBuiltInTool, - })), - systemMessage: config.systemMessage, - availableTools: config.availableTools, - excludedTools: config.excludedTools, - provider: config.provider, - requestPermission: true, - requestUserInput: !!config.onUserInputRequest, - hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), - workingDirectory: config.workingDirectory, - streaming: config.streaming, - mcpServers: config.mcpServers, - envValueMode: "direct", - customAgents: config.customAgents, - agent: config.agent, - configDir: config.configDir, - skillDirectories: config.skillDirectories, - disabledSkills: config.disabledSkills, - infiniteSessions: config.infiniteSessions, - }); + const sessionId = config.sessionId ?? randomUUID(); - const { sessionId, workspacePath } = response as { - sessionId: string; - workspacePath?: string; - }; - const session = new CopilotSession(sessionId, this.connection!, workspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + const session = new CopilotSession(sessionId, this.connection!); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -589,8 +560,52 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + if (config.onEvent) { + session.on(config.onEvent); + } this.sessions.set(sessionId, session); + try { + const response = await this.connection!.sendRequest("session.create", { + model: config.model, + sessionId, + clientName: config.clientName, + reasoningEffort: config.reasoningEffort, + tools: config.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: toJsonSchema(tool.parameters), + overridesBuiltInTool: tool.overridesBuiltInTool, + })), + systemMessage: config.systemMessage, + availableTools: config.availableTools, + excludedTools: config.excludedTools, + provider: config.provider, + requestPermission: true, + requestUserInput: !!config.onUserInputRequest, + hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), + workingDirectory: config.workingDirectory, + streaming: config.streaming, + mcpServers: config.mcpServers, + envValueMode: "direct", + customAgents: config.customAgents, + agent: config.agent, + configDir: config.configDir, + skillDirectories: config.skillDirectories, + disabledSkills: config.disabledSkills, + infiniteSessions: config.infiniteSessions, + }); + + const { workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + session["_workspacePath"] = workspacePath; + } catch (e) { + this.sessions.delete(sessionId); + throw e; + } + return session; } @@ -633,42 +648,9 @@ export class CopilotClient { } } - const response = await this.connection!.sendRequest("session.resume", { - sessionId, - clientName: config.clientName, - model: config.model, - reasoningEffort: config.reasoningEffort, - systemMessage: config.systemMessage, - availableTools: config.availableTools, - excludedTools: config.excludedTools, - tools: config.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - parameters: toJsonSchema(tool.parameters), - overridesBuiltInTool: tool.overridesBuiltInTool, - })), - provider: config.provider, - requestPermission: true, - requestUserInput: !!config.onUserInputRequest, - hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), - workingDirectory: config.workingDirectory, - configDir: config.configDir, - streaming: config.streaming, - mcpServers: config.mcpServers, - envValueMode: "direct", - customAgents: config.customAgents, - agent: config.agent, - skillDirectories: config.skillDirectories, - disabledSkills: config.disabledSkills, - infiniteSessions: config.infiniteSessions, - disableResume: config.disableResume, - }); - - const { sessionId: resumedSessionId, workspacePath } = response as { - sessionId: string; - workspacePath?: string; - }; - const session = new CopilotSession(resumedSessionId, this.connection!, workspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + const session = new CopilotSession(sessionId, this.connection!); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -677,7 +659,52 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } - this.sessions.set(resumedSessionId, session); + if (config.onEvent) { + session.on(config.onEvent); + } + this.sessions.set(sessionId, session); + + try { + const response = await this.connection!.sendRequest("session.resume", { + sessionId, + clientName: config.clientName, + model: config.model, + reasoningEffort: config.reasoningEffort, + systemMessage: config.systemMessage, + availableTools: config.availableTools, + excludedTools: config.excludedTools, + tools: config.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: toJsonSchema(tool.parameters), + overridesBuiltInTool: tool.overridesBuiltInTool, + })), + provider: config.provider, + requestPermission: true, + requestUserInput: !!config.onUserInputRequest, + hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), + workingDirectory: config.workingDirectory, + configDir: config.configDir, + streaming: config.streaming, + mcpServers: config.mcpServers, + envValueMode: "direct", + customAgents: config.customAgents, + agent: config.agent, + skillDirectories: config.skillDirectories, + disabledSkills: config.disabledSkills, + infiniteSessions: config.infiniteSessions, + disableResume: config.disableResume, + }); + + const { workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + session["_workspacePath"] = workspacePath; + } catch (e) { + this.sessions.delete(sessionId); + throw e; + } return session; } diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index c8c88d2cdf..849daf188a 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -77,7 +77,7 @@ export class CopilotSession { constructor( public readonly sessionId: string, private connection: MessageConnection, - private readonly _workspacePath?: string + private _workspacePath?: string ) {} /** diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 69c29396a1..99b9af75cc 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -756,6 +756,17 @@ export interface SessionConfig { * Set to `{ enabled: false }` to disable. */ infiniteSessions?: InfiniteSessionConfig; + + /** + * Optional event handler that is registered on the session before the + * session.create RPC is issued. This guarantees that early events emitted + * by the CLI during session creation (e.g. session.start) are delivered to + * the handler. + * + * Equivalent to calling `session.on(handler)` immediately after creation, + * but executes earlier in the lifecycle so no events are missed. + */ + onEvent?: SessionEventHandler; } /** @@ -783,6 +794,7 @@ export type ResumeSessionConfig = Pick< | "skillDirectories" | "disabledSkills" | "infiniteSessions" + | "onEvent" > & { /** * When true, skips emitting the session.resume event. diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 7cd781bc2d..0ad60edca9 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -297,7 +297,19 @@ describe("Sessions", async () => { }); it("should receive session events", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + // Use onEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + const earlyEvents: Array<{ type: string }> = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + onEvent: (event) => { + earlyEvents.push(event); + }, + }); + + expect(earlyEvents.some((e) => e.type === "session.start")).toBe(true); + const receivedEvents: Array<{ type: string }> = []; session.on((event) => { diff --git a/python/copilot/client.py b/python/copilot/client.py index ff587d9974..df09a755b8 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -19,6 +19,7 @@ import subprocess import sys import threading +import uuid from collections.abc import Callable from pathlib import Path from typing import Any, cast @@ -507,8 +508,6 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: payload: dict[str, Any] = {} if cfg.get("model"): payload["model"] = cfg["model"] - if cfg.get("session_id"): - payload["sessionId"] = cfg["session_id"] if cfg.get("client_name"): payload["clientName"] = cfg["client_name"] if cfg.get("reasoning_effort"): @@ -609,20 +608,33 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: if not self._client: raise RuntimeError("Client not connected") - response = await self._client.request("session.create", payload) - session_id = response["sessionId"] - workspace_path = response.get("workspacePath") - session = CopilotSession(session_id, self._client, workspace_path) + session_id = cfg.get("session_id") or str(uuid.uuid4()) + payload["sessionId"] = session_id + + # Create and register the session before issuing the RPC so that + # events emitted by the CLI (e.g. session.start) are not dropped. + session = CopilotSession(session_id, self._client, None) session._register_tools(tools) session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + on_event = cfg.get("on_event") + if on_event: + session.on(on_event) with self._sessions_lock: self._sessions[session_id] = session + try: + response = await self._client.request("session.create", payload) + session._workspace_path = response.get("workspacePath") + except BaseException: + with self._sessions_lock: + self._sessions.pop(session_id, None) + raise + return session async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession: @@ -798,19 +810,29 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> if not self._client: raise RuntimeError("Client not connected") - response = await self._client.request("session.resume", payload) - resumed_session_id = response["sessionId"] - workspace_path = response.get("workspacePath") - session = CopilotSession(resumed_session_id, self._client, workspace_path) + # Create and register the session before issuing the RPC so that + # events emitted by the CLI (e.g. session.start) are not dropped. + session = CopilotSession(session_id, self._client, None) session._register_tools(cfg.get("tools")) session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + on_event = cfg.get("on_event") + if on_event: + session.on(on_event) with self._sessions_lock: - self._sessions[resumed_session_id] = session + self._sessions[session_id] = session + + try: + response = await self._client.request("session.resume", payload) + session._workspace_path = response.get("workspacePath") + except BaseException: + with self._sessions_lock: + self._sessions.pop(session_id, None) + raise return session diff --git a/python/copilot/types.py b/python/copilot/types.py index 5f4b7e20d7..33764e5d17 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -526,9 +526,13 @@ class SessionConfig(TypedDict, total=False): # When enabled (default), sessions automatically manage context limits and persist state. # Set to {"enabled": False} to disable. infinite_sessions: InfiniteSessionConfig + # Optional event handler that is registered on the session before the + # session.create RPC is issued, ensuring early events (e.g. session.start) + # are delivered. Equivalent to calling session.on(handler) immediately + # after creation, but executes earlier in the lifecycle so no events are missed. + on_event: Callable[[SessionEvent], None] -# Azure-specific provider options class AzureProviderOptions(TypedDict, total=False): """Azure-specific provider configuration""" @@ -595,6 +599,9 @@ class ResumeSessionConfig(TypedDict, total=False): # When True, skips emitting the session.resume event. # Useful for reconnecting to a session without triggering resume-related side effects. disable_resume: bool + # Optional event handler registered before the session.resume RPC is issued, + # ensuring early events are delivered. See SessionConfig.on_event. + on_event: Callable[[SessionEvent], None] # Options for sending a message to a session diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index aa93ed42df..79fb661df1 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -450,9 +450,23 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): async def test_should_receive_session_events(self, ctx: E2ETestContext): import asyncio + # Use on_event to capture events dispatched during session creation. + # session.start is emitted during the session.create RPC; if the session + # weren't registered in the sessions map before the RPC, it would be dropped. + early_events = [] + + def capture_early(event): + early_events.append(event) + session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + { + "on_permission_request": PermissionHandler.approve_all, + "on_event": capture_early, + } ) + + assert any(e.type.value == "session.start" for e in early_events) + received_events = [] idle_event = asyncio.Event() diff --git a/test/scenarios/auth/byok-anthropic/go/go.mod b/test/scenarios/auth/byok-anthropic/go/go.mod index 9a727c69c2..005601ee35 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.mod +++ b/test/scenarios/auth/byok-anthropic/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-anthropic/go/go.sum b/test/scenarios/auth/byok-anthropic/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.sum +++ b/test/scenarios/auth/byok-anthropic/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-azure/go/go.mod b/test/scenarios/auth/byok-azure/go/go.mod index f0dd08661d..21997114b9 100644 --- a/test/scenarios/auth/byok-azure/go/go.mod +++ b/test/scenarios/auth/byok-azure/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-azure/go/go.sum b/test/scenarios/auth/byok-azure/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/auth/byok-azure/go/go.sum +++ b/test/scenarios/auth/byok-azure/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-ollama/go/go.mod b/test/scenarios/auth/byok-ollama/go/go.mod index 806aaa5c27..a6891a811f 100644 --- a/test/scenarios/auth/byok-ollama/go/go.mod +++ b/test/scenarios/auth/byok-ollama/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-ollama/go/go.sum b/test/scenarios/auth/byok-ollama/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/auth/byok-ollama/go/go.sum +++ b/test/scenarios/auth/byok-ollama/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-openai/go/go.mod b/test/scenarios/auth/byok-openai/go/go.mod index 2d5a75ecff..65b3c90280 100644 --- a/test/scenarios/auth/byok-openai/go/go.mod +++ b/test/scenarios/auth/byok-openai/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-openai/go/go.sum b/test/scenarios/auth/byok-openai/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/auth/byok-openai/go/go.sum +++ b/test/scenarios/auth/byok-openai/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/gh-app/go/go.mod b/test/scenarios/auth/gh-app/go/go.mod index a0d270c6e2..7012daa682 100644 --- a/test/scenarios/auth/gh-app/go/go.mod +++ b/test/scenarios/auth/gh-app/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/gh-app/go/go.sum b/test/scenarios/auth/gh-app/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/auth/gh-app/go/go.sum +++ b/test/scenarios/auth/gh-app/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.mod b/test/scenarios/bundling/app-backend-to-server/go/go.mod index 6d01df73b6..c225d6a2c0 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.mod +++ b/test/scenarios/bundling/app-backend-to-server/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.sum b/test/scenarios/bundling/app-backend-to-server/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.sum +++ b/test/scenarios/bundling/app-backend-to-server/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/app-direct-server/go/go.mod b/test/scenarios/bundling/app-direct-server/go/go.mod index db24ae3933..e36e0f50da 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.mod +++ b/test/scenarios/bundling/app-direct-server/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-direct-server/go/go.sum b/test/scenarios/bundling/app-direct-server/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.sum +++ b/test/scenarios/bundling/app-direct-server/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/container-proxy/go/go.mod b/test/scenarios/bundling/container-proxy/go/go.mod index 086f431754..270a60c618 100644 --- a/test/scenarios/bundling/container-proxy/go/go.mod +++ b/test/scenarios/bundling/container-proxy/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/container-proxy/go/go.sum b/test/scenarios/bundling/container-proxy/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/bundling/container-proxy/go/go.sum +++ b/test/scenarios/bundling/container-proxy/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/fully-bundled/go/go.mod b/test/scenarios/bundling/fully-bundled/go/go.mod index 93af1915a4..5c7d03b112 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.mod +++ b/test/scenarios/bundling/fully-bundled/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/fully-bundled/go/go.sum b/test/scenarios/bundling/fully-bundled/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.sum +++ b/test/scenarios/bundling/fully-bundled/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/hooks/go/go.mod b/test/scenarios/callbacks/hooks/go/go.mod index 51b27e4917..3220cd5060 100644 --- a/test/scenarios/callbacks/hooks/go/go.mod +++ b/test/scenarios/callbacks/hooks/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/hooks/go/go.sum b/test/scenarios/callbacks/hooks/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/callbacks/hooks/go/go.sum +++ b/test/scenarios/callbacks/hooks/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/permissions/go/go.mod b/test/scenarios/callbacks/permissions/go/go.mod index 25eb7d22a3..bf88ca7ec6 100644 --- a/test/scenarios/callbacks/permissions/go/go.mod +++ b/test/scenarios/callbacks/permissions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/permissions/go/go.sum b/test/scenarios/callbacks/permissions/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/callbacks/permissions/go/go.sum +++ b/test/scenarios/callbacks/permissions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/user-input/go/go.mod b/test/scenarios/callbacks/user-input/go/go.mod index 11419b6349..b050ef88b5 100644 --- a/test/scenarios/callbacks/user-input/go/go.mod +++ b/test/scenarios/callbacks/user-input/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/user-input/go/go.sum b/test/scenarios/callbacks/user-input/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/callbacks/user-input/go/go.sum +++ b/test/scenarios/callbacks/user-input/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/modes/default/go/go.mod b/test/scenarios/modes/default/go/go.mod index 50b92181fa..5ce3524d7f 100644 --- a/test/scenarios/modes/default/go/go.mod +++ b/test/scenarios/modes/default/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/default/go/go.sum b/test/scenarios/modes/default/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/modes/default/go/go.sum +++ b/test/scenarios/modes/default/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/modes/minimal/go/go.mod b/test/scenarios/modes/minimal/go/go.mod index 72fbe3540b..c8eb4bbfda 100644 --- a/test/scenarios/modes/minimal/go/go.mod +++ b/test/scenarios/modes/minimal/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/minimal/go/go.sum b/test/scenarios/modes/minimal/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/modes/minimal/go/go.sum +++ b/test/scenarios/modes/minimal/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/attachments/go/go.mod b/test/scenarios/prompts/attachments/go/go.mod index 0a5dc6c1f1..22aa80a14e 100644 --- a/test/scenarios/prompts/attachments/go/go.mod +++ b/test/scenarios/prompts/attachments/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/attachments/go/go.sum b/test/scenarios/prompts/attachments/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/prompts/attachments/go/go.sum +++ b/test/scenarios/prompts/attachments/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/reasoning-effort/go/go.mod b/test/scenarios/prompts/reasoning-effort/go/go.mod index f2aa4740c5..b3fafcc1ce 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.mod +++ b/test/scenarios/prompts/reasoning-effort/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/reasoning-effort/go/go.sum b/test/scenarios/prompts/reasoning-effort/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.sum +++ b/test/scenarios/prompts/reasoning-effort/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/system-message/go/go.mod b/test/scenarios/prompts/system-message/go/go.mod index b8301c15a4..8bc1c55ce3 100644 --- a/test/scenarios/prompts/system-message/go/go.mod +++ b/test/scenarios/prompts/system-message/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/system-message/go/go.sum b/test/scenarios/prompts/system-message/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/prompts/system-message/go/go.sum +++ b/test/scenarios/prompts/system-message/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.mod b/test/scenarios/sessions/concurrent-sessions/go/go.mod index c016423209..a69dedd169 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.mod +++ b/test/scenarios/sessions/concurrent-sessions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.sum b/test/scenarios/sessions/concurrent-sessions/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.sum +++ b/test/scenarios/sessions/concurrent-sessions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/infinite-sessions/go/go.mod b/test/scenarios/sessions/infinite-sessions/go/go.mod index cb8d2713db..15f8e48f7f 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.mod +++ b/test/scenarios/sessions/infinite-sessions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/infinite-sessions/go/go.sum b/test/scenarios/sessions/infinite-sessions/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.sum +++ b/test/scenarios/sessions/infinite-sessions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/session-resume/go/go.mod b/test/scenarios/sessions/session-resume/go/go.mod index 3722b78d25..ab1b82c398 100644 --- a/test/scenarios/sessions/session-resume/go/go.mod +++ b/test/scenarios/sessions/session-resume/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/session-resume/go/go.sum b/test/scenarios/sessions/session-resume/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/sessions/session-resume/go/go.sum +++ b/test/scenarios/sessions/session-resume/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/streaming/go/go.mod b/test/scenarios/sessions/streaming/go/go.mod index acb5163799..f6c553680f 100644 --- a/test/scenarios/sessions/streaming/go/go.mod +++ b/test/scenarios/sessions/streaming/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/streaming/go/go.sum b/test/scenarios/sessions/streaming/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/sessions/streaming/go/go.sum +++ b/test/scenarios/sessions/streaming/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/custom-agents/go/go.mod b/test/scenarios/tools/custom-agents/go/go.mod index 9acbccb06a..f6f670b8cb 100644 --- a/test/scenarios/tools/custom-agents/go/go.mod +++ b/test/scenarios/tools/custom-agents/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/custom-agents/go/go.sum b/test/scenarios/tools/custom-agents/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/custom-agents/go/go.sum +++ b/test/scenarios/tools/custom-agents/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/mcp-servers/go/go.mod b/test/scenarios/tools/mcp-servers/go/go.mod index 4b93e09e7e..65de0a40b2 100644 --- a/test/scenarios/tools/mcp-servers/go/go.mod +++ b/test/scenarios/tools/mcp-servers/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/mcp-servers/go/go.sum b/test/scenarios/tools/mcp-servers/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/mcp-servers/go/go.sum +++ b/test/scenarios/tools/mcp-servers/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/no-tools/go/go.mod b/test/scenarios/tools/no-tools/go/go.mod index 74131d3e6b..387c1b51d9 100644 --- a/test/scenarios/tools/no-tools/go/go.mod +++ b/test/scenarios/tools/no-tools/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/no-tools/go/go.sum b/test/scenarios/tools/no-tools/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/no-tools/go/go.sum +++ b/test/scenarios/tools/no-tools/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/skills/go/go.mod b/test/scenarios/tools/skills/go/go.mod index 1467fd64fc..ad94ef6b7c 100644 --- a/test/scenarios/tools/skills/go/go.mod +++ b/test/scenarios/tools/skills/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/skills/go/go.sum b/test/scenarios/tools/skills/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/skills/go/go.sum +++ b/test/scenarios/tools/skills/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/tool-filtering/go/go.mod b/test/scenarios/tools/tool-filtering/go/go.mod index c3051c52b6..ad36d3f636 100644 --- a/test/scenarios/tools/tool-filtering/go/go.mod +++ b/test/scenarios/tools/tool-filtering/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-filtering/go/go.sum b/test/scenarios/tools/tool-filtering/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/tool-filtering/go/go.sum +++ b/test/scenarios/tools/tool-filtering/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/tool-overrides/go/go.mod b/test/scenarios/tools/tool-overrides/go/go.mod index 3530667617..ba48b0e7b4 100644 --- a/test/scenarios/tools/tool-overrides/go/go.mod +++ b/test/scenarios/tools/tool-overrides/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-overrides/go/go.sum b/test/scenarios/tools/tool-overrides/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/tool-overrides/go/go.sum +++ b/test/scenarios/tools/tool-overrides/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/virtual-filesystem/go/go.mod b/test/scenarios/tools/virtual-filesystem/go/go.mod index d6606bb7b2..e5f1216118 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.mod +++ b/test/scenarios/tools/virtual-filesystem/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/virtual-filesystem/go/go.sum b/test/scenarios/tools/virtual-filesystem/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.sum +++ b/test/scenarios/tools/virtual-filesystem/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/reconnect/go/go.mod b/test/scenarios/transport/reconnect/go/go.mod index 7a1f80d6ce..e1267bb72f 100644 --- a/test/scenarios/transport/reconnect/go/go.mod +++ b/test/scenarios/transport/reconnect/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/reconnect/go/go.sum b/test/scenarios/transport/reconnect/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/transport/reconnect/go/go.sum +++ b/test/scenarios/transport/reconnect/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/stdio/go/go.mod b/test/scenarios/transport/stdio/go/go.mod index 2dcc353102..63ad24beef 100644 --- a/test/scenarios/transport/stdio/go/go.mod +++ b/test/scenarios/transport/stdio/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/stdio/go/go.sum b/test/scenarios/transport/stdio/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/transport/stdio/go/go.sum +++ b/test/scenarios/transport/stdio/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/tcp/go/go.mod b/test/scenarios/transport/tcp/go/go.mod index dc1a0b6f93..85fac7926e 100644 --- a/test/scenarios/transport/tcp/go/go.mod +++ b/test/scenarios/transport/tcp/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/tcp/go/go.sum b/test/scenarios/transport/tcp/go/go.sum index 6e171099c0..6029a9b710 100644 --- a/test/scenarios/transport/tcp/go/go.sum +++ b/test/scenarios/transport/tcp/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From abfbc9e7837e5f4aeedbb252919ab93615ce1c2e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 10 Mar 2026 06:54:43 -0400 Subject: [PATCH 21/61] Add a copilot package icon to the .NET nuget package (#688) --- assets/copilot.png | Bin 0 -> 7435 bytes dotnet/src/GitHub.Copilot.SDK.csproj | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 assets/copilot.png diff --git a/assets/copilot.png b/assets/copilot.png new file mode 100644 index 0000000000000000000000000000000000000000..e71958c94738d3837cb1fb2bc6082ecbc96a5a01 GIT binary patch literal 7435 zcmW+*2{hE-7k}R|7+V-imM~+_lC2_H#@L2m*-NsGJt14!m$467QcAKfl_DV_+l(bd zSyB;YFZ;eUG5G6$&U@$Gd*A2XbI&{X+;`u-i6%z+ER1}N000&PoQ@d)&@l-C1l{ps z<5%i>yfFCSto#9B;`lcq@Fa)lAH?5G9}6n_PAwcWa5t^XS^!j~F;N|80XS7)prd6G z1g+$LO1tv)NvHjM_78(hzvtXBGYB8y686x~8mc*Rd#du#a5N(xAD+$ zHInli)*Z-g?ahQ+v%SZw%h;yhtugf0UtScmZtYEd^!SjIV4o(;yTLaXx=67iosTp= z?=^bcm{{kDoL1u%nRt=f<%0il(dEN}!U&s0J$csL0;&OfLXY?%wa7zB6hr8t;8tqbMYln8yI zvFM_THu^C$EuQ%-6_1O>%5yW4*^WtrSZpQ4NQhU6I4g=pUlR*~Oybi<53CBOy68d> zza8a-Zau#7z=tR>668c#^QtNTG!Sabg=7!1i>B6^mWTobbx^<%o}xs6{WN|L@_W` zMMf#JmCzTwX~>L?H(tJnD;UT42on0%{l7?@Unrdk-SKReiDFNtFPNKaw`E$@7B-67 zq3?^W4svIM71$nnv>k?jW=@17kwHQ*$r(*4noN>N>{)GnI<%ItT9*ejz&lFopfBVu zsOFzfu@hTYgb!?J+}yW$bhjRea|>)K$W7(~`EiNV`^1D`=rtDn{Hi}AaptpW*SQH- z?hp|W(t3hV(>E)X`F_lJ>@zzAp>MmLlC>;S5#zRfU+%JY9}`A=$e3uHvr~wjyp1%O zAP1v6?0;WzTu)X>dIQ7ENI+BN2$j>|Yy=Wzltl3Vzi1{0p(6HA1Q{g~l>Uj>r;-sW zXb{4l_9ymU(EMrkdX(%?1DLOP@gEi!hF`_lUoW`=4TKLp?(ILJi4=LPj$p4pEjxH! z0`GcNC?&rm$(?@{_g~F4H2;@s^FHAol&Bi;wki)}EeN=`<`HQ4ZD%}3%1}L+_Y<<* zL_4d~k3ClVasyFt($rIT!Et)^b9H%ndG{;pz}XhnnQMb?l_f2jN#DQg)6E@^SBsu4 zwNCrzgXar*(Qk)t1Xn6R_|6Fp`E7pXmIu~>t0j8jOS`$-9(N!J7l(J%t1a7lV7&ZH zeuO@lV0&3s_8Sq%FSsQv$p^U1S{{YTs!xze!e@Zol!6$1KcZQg3x28unRqsX)6)?2 zF$ON5W@e^N|C08b8C#oeP_leDZs%QWlSd1^ zougUgCgjKZ9$ou*Bhq)n@JUq~0|K-5S7e*lY22Gi#T8T3aJE?P4O8wg;<5=5CdpqaQzb=H$ z6LM4zi#)%|e(1O*iMIyxdrUvW$K++M>Dx1q?W$tWL6bT{se5;{z42TgEZD1X)SCul zec`yO2qk&td(m`-`d%NoD&GnMWTZeMXFaul*REhg8LVzZwI8A8G$}DU%&H!*$^}fm z(-1FgB=82$eTl(kk_32=7RHECHXySg@Rd>1=TG`9lS_5#8oqjPa4xlEi&?nX4_kR3 zbUdOIPc^c@Cs&jwh@V7CYw9ppAY##S!m+N+r>V<;2{N6ijSxVH5t zry8%0=jLVKzKDj^bB_r^6;OQ4k~?D%-1W}vUG6OflIBj*sqjN0+_12PPy{374OhVF zl3msgCvaEfrB*{jDyes}tHkxpW)(aWff{#x-y7L)UsHHSkaf~0Nr*dG)8lhLUC_D}olpIh5-TMJJ{Z?HPG_eO1K!(n1|CrzBsPnt5Ysa7TtqPm_9TU`*39gDNk=?HOc@CE* zfgtE8{D35iSv}1e<0XATvQTlPKCJlCGUre!k<@9C0_J5-U1Cs)QxkpJqBsdGak9C@4hpj z%fDi3%)&4QqBj1BACS*Ni0a+QgRvmJNHAPa)Z#n{9wzT=T2xB0tt)zSXj7_S1wW4} z;JCr>5$I*ygWSA}DOq4~dY?&L<8>r|Ik+tm2R%?DpgfMc_r@C@+Ji|A{rb`JKa)z* z)9*_>WMdm2j*a9I3}9@|yCmWU&8pFooNjNv*BO|(l5xs@Ck`suC&j1j_$U^X2GPp^ zr*?v`AjT5*Yt^snw@?T;tbUH4udVqpe&2;|LCGP+WrJG?`0aMNw_KT;a= zf--^-z}0}1l%_?hw`8_ga~k->IU;m5Nn~2Qv-yA|Yk@RmKC6Ag#Vt#*I8u^#L-<#J ze*5lyJoRD%NyQLEe>~(*;?YLz_?Wm>gh_7rq|;SA^ywwUvphX8g;koven+&jq2N6{ z{*tRc?CfbM<7x*VdkN4$Jy`=d9mF}Idg~vL*W&NfQma{&Q3+x7pWE{Y{TG z+x^svgNVY1a+~Oc0Up2NX!eAAh~%mbY5KJ6lUC3?!YX0yRUvVzOmqu-P38@esCg=l ze&C#35fhYD6Tdgk64)Niw6^b zikELW6K8k-#G$+(nL82iY<(?zpWHTlDJaWBfEJ6eZzmL8$SJe8X9~6@JD8%pY;HQ2)xqW zBRPrRIm=80tvrtb$&JW&k}!&A-byp7cK!66>HSJ=!pjdNhez8qzwaZO{_ZxW@R3^2 zvCDUQZ*A>X!7T2{MxN8uJW4}elR!Dv9Z`lNdr0}}ZsDe^)X-8!ylvF^I+k{tw4hVN zR`KDaap@y!o92gzJDqPc7`!X(z@Btv)XnURlCq`gA*&C~H#14p*CTYpM2~j0DEq_B zOe9a8@(VK%Hd_G)%tz5lV{DD*(#frY+Y+gy$9dUpo#!08iu<`W>~eV%({CLI`;5as zm7Un*bi{AEPR{T_$zLLC_J?o`9QB0Kr2SpNaF7)Xo}8yT;Bn2z4>Q&z(~`i>_m`P} zn9^tM-8~gU|1Eq)ddFKx(^R%SZ6B{wbw)(t4>VABUFtvlz+7)4sxN zi$^y<{dz4-MhKb-{-pAEr#xH1ptI7@uRgv~|yX|IZjA5207<$>GtPVAbrfR$b^ zJX=hoBT&PL`k~k7hvtcs(gL-urCv(j1f9jP9{zQCQC!m!jr=dc?9L?m>EbQ7@T;4N zv*#k@%OcMg=nSi_^UmxAELUchX&rW$4=#Ihr^$EPTk7Y{t^U9z@sQ9K>}0o4v&fd( zOwgsYP>cu?0Y3X)$|1?ZqU@ND_FpJ0dT`Iw46pm=4jagC5)G4#{Si&<1o7kv0{t&- zE+j<_COnoFJK-7UL0+@_yUFyLb}$Gd!QVg^1kg1 zNb!BzqDT%JxHy@O60T}2g_kdw2_Tb$T*Jx15GH#L``-h_&(~jvtcMd5(xWVky^?pRrXY&IbO>hDXh9549I>fYC-z*#}h$F^?en+xLEQy#sv zk#BEJ-~XVOA;pJXw#|cHBHgn$R(~#pOG-yvW&k&eV5Ugri%_=T0m~IQp0n=KQG;ss zH@l(fCtmvkojg0`x3b=IetESxQaqmT%}~FrF8`<1=BV^~@a~?^ic(Lwk*@bx?U#q~ zQ!^1Gn*mQaL4tPF?8#|Vi(EI6osnLOp{gykk}j5Y>D=fkXU`}pjPMEQO6}Wc{OF3w z`QK8B{w`$PBPVh8>FGBt5JkurZeLd*EXsO9W8ww9kMXI(o?B;PDFi1wa(^EJ)4+1h zDH5rY>ZRh>pCyX7P7#ego1)GCvBU)O@$!0WbQGBuG_yCB`QrcXdnm#M2j9A;IHAWhDOcFQ*n2J)-V;j@P(zV1b(yJ(0tOepr#@`%=5wbhU1LbJI;(m47SZRH-(i6r~_=rP&Ix9{rj_8Tl&}RVc0DBY8)KJ?I-x{6)Fk*c6Liqh%FD#Dv zV~qqjn3D5kBh>CemB}@)Ugb0Ra;eRYq_)uRecvNF{PKx>|+_L+VY88vm1jJvGaQyDX5%Xr?;-S>6uVX0gy zCs>h6rp7e{&wWy^FH2@tD+)JWlij`;RKLhvyq=fDc>Cwthm-4YQb9nYcZWbJ&k+r@~d`sFqLWLZ`r)q5?^eWyT6>YCDSrp06H7FMU9k?TxZ%r*%X zHE_YR&UwbU5weS)#NkDT5%;3bb$Fpy>1qZ*xx4eKAD6{!Eu3>(RyuWMT zJvf&}yj;mDR9BUnpnBeP<%Qd4g&E_F6{FmdIs?D;$<~-)-dR`1L|*+igj3B^ zITUVA#zajyHTsP?MqYQ>3w=Z{#`N(t^!R$uag@Ws4)ay$0&>+O9Qzucm?PSX&a_Xw z%>H&tHc&%V_K^M^97wX)ml)B%S1^f>R`%K0wcBYQ6@h7tVS(kr^-wKmtT&FC4gaI_4R7~a% z+S&GaE4J-=$2;_{QbRWniu$hxvIAe*qbdOWm%yeqC^zz*TRImPp+aSumS=IB4hVvp zaX~Zs_LzWHw=k{namvS~y(V?u9hR;PATxU~g@u6(1hvFSz&A(LpT_E*gbMW|6i?4$ z{yaPX{R7hp!sm%%-H5_uu>EC$V*`!=P0-lGXoqzIUh6@)`;uaFwCWAV8s$kPonY@D zqB8oY&+f&v!9dssjQNk{Rp+Up{#Sokl4*+RHsRL>VmO>$LfQ#LkrwZlR!qGCS|8Q- zml@Lz3(CQEhlm_`z~b6H7m^*_&3=x?({vXMI6~~pV#lW+)(jWFFu>=%+i&L-JkUA~ zftFy%s>Q7MgZ0twtrdq>c{ZB*3^tm0h9&pjc`e{FZdR;WDh$*&2AH}(TSVYFgGShd zCr>T4@%k)yhwbOubPcxIFca4k<$D^Q>P4Dd2ZW_?DA?T`0c`q?>k8zH1fEzaL_LY; zm-o+i5Aw#YPqe|JLo-ud1O4Obm*lIqfy_sl zxwbE5qc5C#=Ssa?R1JU_>~SFVKAaIwCPsN8;5m9Vtg$KtM)i zeqAWnX|uFvb?rB02H&NB1zT)bbHp1j*`A+^uq zWDTM(BYs0!-ZNY#%*)ZS;?L}z5~jP|>@;q+?M*TD;PPxktNrg3Rke z-Ew4K`sb8nrjgU4w0TkN@H3hkyhFmD+`sZyF7EL!A_=A1Q3eI&@i|fMiMq1tuJ~Ek z=r0d)6fQ87Rgem~A;3(#>2Q8HS(hzfdE^n_UaHqvefo`v^2WpXQ8u8U#Ir?Z*l=P>ll-vg0_3R^VxRcIhmQ3)}f!?-4JUNopjA$=XZ)P zeEwQt4#Rt4^lnyZ*+ZML&?|)j5Gd^DT2FV-DnMWC+V7#*KYHSB2-sjK&f^-ZGhZUJ z&CzJ!uUk3*Y(cTJMZ5Kyy{_(va4ml+hW3(ioVsh$)3!#0iR8a*2^@DhMKG<%B{!FSBDYBaxEdOs_^VgmT! zdpez24WZPn&o@utc`DEt;ey(C2QEH_0O%6n$;bBPZ~zQD3YyN@ppl>jCU6`^#hpsF zf&eFk1buv(KG*+2gOZ1ip(g>^|&;f7gHQ8%fIO?=Q*U&#Ff2)ln zz(5(J!zxA};hUyI*BNQ9)++r^*R*6Jv+Iu7G`mp^?4;Jjr0wF3=CmhlEP3y3TX;3s zjR5#9Pr8xPP9F?bC{fcZmD^&CTlazB_sBOroeq2ALI1 zE?@eQItY1Y4aw%`s%7vJDW07e-@geApJ}?W-h>Qz@u5O~!(PvEm*QUBI5-`}yAyLh zg01QQTo%@z?(M-O%fvrkY=#BTyQzcV#Xkwz?R2K1q*zgRcjA*^^F^>>KeSOGLT zy&Kg?LOF9Wl6fTIt@jWtkuscw(2)ZN_z@I^;lCrD3{&Sjx&hQIKxJwGRbuE6ac|IE05 zi8q1~(=Mq%z8+`G8$_R0=Jfp7S7~DOxsSwCTC602Y;iLU5o0M}Y(R-7upjb=w_J+x zKFLUMy20x3Tmyv=*s$b8@^)-!c9SPBv|Xg5C3D=SxzA1?dqN<*rkQ$ySv5T>>ZrV5 z1 literal 0 HcmV?d00001 diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 8ae53ca74c..5d2502c87d 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -11,6 +11,7 @@ https://github.com/github/copilot-sdk README.md https://github.com/github/copilot-sdk + copilot.png github;copilot;sdk;jsonrpc;agent true true @@ -25,6 +26,7 @@ + From 9a0a1a5f21111f4ad02b5ce911750ecc75e054c3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 10 Mar 2026 07:11:56 -0400 Subject: [PATCH 22/61] Add DebuggerDisplay to SessionEvent for better debugging (#726) Added [DebuggerDisplay] attribute to SessionEvent base class to show JSON in debugger views. --- dotnet/src/Generated/SessionEvents.cs | 5 +++++ scripts/codegen/csharp.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6648bd189b..6dbfa8941c 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -5,6 +5,7 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,6 +14,7 @@ namespace GitHub.Copilot.SDK; /// /// Provides the base class from which all session events derive. /// +[DebuggerDisplay("{DebuggerDisplay,nq}")] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] @@ -107,6 +109,9 @@ public static SessionEvent FromJson(string json) => /// Serializes this event to a JSON string. public string ToJson() => JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay => ToJson(); } /// Represents the session.start event. diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index e667c28a54..3aeb0eef35 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -509,6 +509,7 @@ function generateSessionEventsCode(schema: JSONSchema7): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json +using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; @@ -519,6 +520,7 @@ namespace GitHub.Copilot.SDK; lines.push(`/// `); lines.push(`/// Provides the base class from which all session events derive.`); lines.push(`/// `); + lines.push(`[DebuggerDisplay("{DebuggerDisplay,nq}")]`); lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]`); for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) { lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`); @@ -537,7 +539,9 @@ namespace GitHub.Copilot.SDK; lines.push(` /// Deserializes a JSON string into a .`); lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, ""); lines.push(` /// Serializes this event to a JSON string.`); - lines.push(` public string ToJson() =>`, ` JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent);`, `}`, ""); + lines.push(` public string ToJson() =>`, ` JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent);`, ""); + lines.push(` [DebuggerBrowsable(DebuggerBrowsableState.Never)]`, ` private string DebuggerDisplay => ToJson();`); + lines.push(`}`, ""); // Event classes with XML docs for (const variant of variants) { From 27f487f9a65b7b39dbb96d0a13fbedc9c65cbcb1 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Tue, 10 Mar 2026 08:50:09 -0700 Subject: [PATCH 23/61] Remove PR creation from cross-repo issue analysis workflow (#780) Simplify the workflow to only open linked issues in copilot-agent-runtime instead of also creating draft PRs with suggested fixes. This reduces scope and avoids potentially noisy automated PRs. Changes: - Remove pull-requests permission - Remove edit tool and create-pull-request safe-output - Remove PR creation instructions from the prompt - Update guidelines to reflect issue-only workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../workflows/cross-repo-issue-analysis.md | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cross-repo-issue-analysis.md b/.github/workflows/cross-repo-issue-analysis.md index 8a0218427a..61b19f4913 100644 --- a/.github/workflows/cross-repo-issue-analysis.md +++ b/.github/workflows/cross-repo-issue-analysis.md @@ -1,5 +1,5 @@ --- -description: Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there +description: Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there on: issues: types: [labeled] @@ -13,7 +13,6 @@ if: "github.event_name == 'workflow_dispatch' || github.event.label.name == 'run permissions: contents: read issues: read - pull-requests: read steps: - name: Clone copilot-agent-runtime run: git clone --depth 1 https://x-access-token:${{ secrets.RUNTIME_TRIAGE_TOKEN }}@github.com/github/copilot-agent-runtime.git ${{ github.workspace }}/copilot-agent-runtime @@ -21,7 +20,6 @@ tools: github: toolsets: [default] github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - edit: bash: - "grep:*" - "find:*" @@ -42,12 +40,6 @@ safe-outputs: labels: [upstream-from-sdk, ai-triaged] target-repo: "github/copilot-agent-runtime" max: 1 - create-pull-request: - title-prefix: "[copilot-sdk] " - labels: [upstream-from-sdk, ai-suggested-fix] - draft: true - target-repo: "github/copilot-agent-runtime" - timeout-minutes: 20 --- @@ -106,10 +98,6 @@ Classify the issue into one of these categories: - References the original SDK issue (e.g., `github/copilot-sdk#123`) - Includes the specific files and code paths involved - Suggests a fix approach - - Create a draft PR in `github/copilot-agent-runtime` with a suggested fix: - - Make the minimal, targeted code changes needed - - Include a clear PR description linking back to both issues - - If you're uncertain about the fix, still create the PR as a starting point for discussion 3. **Needs-investigation**: You cannot confidently determine the root cause. Label the issue `needs-investigation`. @@ -117,7 +105,6 @@ Classify the issue into one of these categories: 1. **Be thorough but focused**: Read enough code to be confident in your analysis, but don't read every file in both repos 2. **Err on the side of creating the runtime issue**: If there's a reasonable chance the fix is in the runtime, create the issue. False positives are better than missed upstream bugs. -3. **Make actionable PRs**: Even if the fix isn't perfect, a draft PR with a concrete starting point is more useful than just an issue description -4. **Link everything**: Always cross-reference between the SDK issue, runtime issue, and runtime PR so maintainers can follow the trail -5. **Be specific**: When describing the root cause, point to specific files, functions, and line numbers in both repos -6. **Don't duplicate**: Before creating a runtime issue, search existing open issues in `github/copilot-agent-runtime` to avoid duplicates. If a related issue exists, reference it instead of creating a new one. +3. **Link everything**: Always cross-reference between the SDK issue and runtime issue so maintainers can follow the trail +4. **Be specific**: When describing the root cause, point to specific files, functions, and line numbers in both repos +5. **Don't duplicate**: Before creating a runtime issue, search existing open issues in `github/copilot-agent-runtime` to avoid duplicates. If a related issue exists, reference it instead of creating a new one. From ab8cc5a8cdf443fa5d4cb0d2f123f467354637de Mon Sep 17 00:00:00 2001 From: Sergio Padrino Date: Wed, 11 Mar 2026 14:57:13 +0100 Subject: [PATCH 24/61] [nodejs] Ignore `cliPath` if `cliUrl` is set (#787) * Don't pass cliPath to client if cliUrl was provided * Add cliPath option and validate it at runtime Include an optional cliPath in the client options type and update the Omit<> to account for it. Simplify the default assignment for cliPath (use cliUrl to disable, otherwise use provided cliPath or bundled fallback). Add a runtime check that throws a clear error if cliPath is not available before trying to access the file system, ensuring users provide a local CLI path or use cliUrl. * Add test: cliPath undefined when cliUrl set Add a unit test to nodejs/test/client.test.ts that verifies CopilotClient does not resolve or set options.cliPath when instantiated with a cliUrl. This ensures providing a remote CLI URL doesn't trigger resolution of a local CLI path. --- nodejs/src/client.ts | 14 ++++++++++++-- nodejs/test/client.test.ts | 9 +++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index bd4cc19606..954d88b599 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -141,8 +141,12 @@ export class CopilotClient { private sessions: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages private options: Required< - Omit + Omit< + CopilotClientOptions, + "cliPath" | "cliUrl" | "githubToken" | "useLoggedInUser" | "onListModels" + > > & { + cliPath?: string; cliUrl?: string; githubToken?: string; useLoggedInUser?: boolean; @@ -230,7 +234,7 @@ export class CopilotClient { this.onListModels = options.onListModels; this.options = { - cliPath: options.cliPath || getBundledCliPath(), + cliPath: options.cliUrl ? undefined : options.cliPath || getBundledCliPath(), cliArgs: options.cliArgs ?? [], cwd: options.cwd ?? process.cwd(), port: options.port || 0, @@ -1135,6 +1139,12 @@ export class CopilotClient { envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken; } + if (!this.options.cliPath) { + throw new Error( + "Path to Copilot CLI is required. Please provide it via the cliPath option, or use cliUrl to rely on a remote CLI." + ); + } + // Verify CLI exists before attempting to spawn if (!existsSync(this.options.cliPath)) { throw new Error( diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index ef227b6988..7206c903b3 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -210,6 +210,15 @@ describe("CopilotClient", () => { expect((client as any).isExternalServer).toBe(true); }); + + it("should not resolve cliPath when cliUrl is provided", () => { + const client = new CopilotClient({ + cliUrl: "localhost:8080", + logLevel: "error", + }); + + expect(client["options"].cliPath).toBeUndefined(); + }); }); describe("Auth options", () => { From 062b61c8aa63b9b5d45fa1d7b01723e6660ffa83 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 11 Mar 2026 16:16:49 +0000 Subject: [PATCH 25/61] Update cross-repo-issue-analysis.lock.yml --- .../cross-repo-issue-analysis.lock.yml | 311 +++++------------- 1 file changed, 80 insertions(+), 231 deletions(-) diff --git a/.github/workflows/cross-repo-issue-analysis.lock.yml b/.github/workflows/cross-repo-issue-analysis.lock.yml index c7cd9f4de5..05b2f23cb2 100644 --- a/.github/workflows/cross-repo-issue-analysis.lock.yml +++ b/.github/workflows/cross-repo-issue-analysis.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.50.5). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.52.1). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -21,9 +21,9 @@ # # For more information: https://github.github.com/gh-aw/introduction/overview/ # -# Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there +# Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there # -# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"553bdce55a05e3f846f312d711680323ba79effef8a001bd23cb72c1c0459413","compiler_version":"v0.50.5"} +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"bbe407b2d324d84d7c6653015841817713551b010318cee1ec12dd5c1c077977","compiler_version":"v0.52.1"} name: "SDK Runtime Triage" "on": @@ -40,7 +40,7 @@ name: "SDK Runtime Triage" permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number }}" + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" run-name: "SDK Runtime Triage" @@ -56,33 +56,50 @@ jobs: body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} text: ${{ steps.sanitized.outputs.text }} title: ${{ steps.sanitized.outputs.title }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5 + uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1 with: destination: /opt/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "0.0.420" + GH_AW_INFO_CLI_VERSION: "v0.52.1" + GH_AW_INFO_WORKFLOW_NAME: "SDK Runtime Triage" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Validate context variables - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/validate_context_variables.cjs'); - await main(); - name: Checkout .github and .agents folders uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout: | .github .agents + sparse-checkout-cone-mode: true fetch-depth: 1 persist-credentials: false - name: Check workflow file timestamps @@ -130,10 +147,7 @@ jobs: cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" cat << 'GH_AW_PROMPT_EOF' - Tools: create_issue, create_pull_request, add_labels, missing_tool, missing_data - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_EOF' + Tools: create_issue, add_labels, missing_tool, missing_data, noop The following GitHub context information is available for this workflow: @@ -231,12 +245,14 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Upload prompt artifact + - name: Upload activation artifact if: success() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: prompt - path: /tmp/gh-aw/aw-prompts/prompt.txt + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt retention-days: 1 agent: @@ -245,7 +261,6 @@ jobs: permissions: contents: read issues: read - pull-requests: read env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -261,12 +276,13 @@ jobs: detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_success: ${{ steps.detection_conclusion.outputs.success }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - model: ${{ steps.generate_aw_info.outputs.model }} + inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + model: ${{ needs.activation.outputs.model }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5 + uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1 with: destination: /opt/gh-aw/actions - name: Checkout repository @@ -293,7 +309,7 @@ jobs: - name: Checkout PR branch id: checkout-pr if: | - github.event.pull_request + (github.event.pull_request) || (github.event.issue.pull_request) uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} @@ -304,52 +320,8 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "copilot", - engine_name: "GitHub Copilot CLI", - model: process.env.GH_AW_MODEL_AGENT_COPILOT || "", - version: "", - agent_version: "0.0.418", - cli_version: "v0.50.5", - workflow_name: "SDK Runtime Triage", - experimental: false, - supports_tools_allowlist: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.23.0", - awmg_version: "v0.1.5", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.418 + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.420 - name: Install awf binary run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 - name: Determine automatic lockdown mode for GitHub MCP Server @@ -364,14 +336,14 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.5 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.7 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine - name: Write Safe Outputs Config run: | mkdir -p /opt/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"max":1},"create_pull_request":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} GH_AW_SAFE_OUTPUTS_CONFIG_EOF cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' [ @@ -416,43 +388,6 @@ jobs: }, "name": "create_issue" }, - { - "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[copilot-sdk] \". Labels [upstream-from-sdk ai-suggested-fix] will be automatically added. PRs will be created as drafts.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", - "type": "string" - }, - "branch": { - "description": "Source branch name containing the changes. If omitted, uses the current working branch.", - "type": "string" - }, - "draft": { - "description": "Whether to create the PR as a draft. Draft PRs cannot be merged until marked as ready for review. Use mark_pull_request_as_ready_for_review to convert a draft PR. Default: true.", - "type": "boolean" - }, - "labels": { - "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "title": { - "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_pull_request" - }, { "description": "Add labels to an existing GitHub issue or pull request for categorization and filtering. Labels must already exist in the repository. For creating new issues with labels, use create_issue with the labels property instead. CONSTRAINTS: Maximum 3 label(s) can be added. Only these labels are allowed: [runtime sdk-fix-only needs-investigation]. Target: triggering.", "inputSchema": { @@ -599,42 +534,6 @@ jobs: } } }, - "create_pull_request": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "branch": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "draft": { - "type": "boolean" - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, "missing_data": { "defaultMax": 20, "fields": { @@ -752,10 +651,11 @@ jobs: export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.5' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.7' mkdir -p /home/runner/.copilot cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh @@ -787,17 +687,11 @@ jobs: } } GH_AW_MCP_CONFIG_EOF - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Download prompt artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + - name: Download activation artifact + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: prompt - path: /tmp/gh-aw/aw-prompts + name: activation + path: /tmp/gh-aw - name: Clean git credentials run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute GitHub Copilot CLI @@ -810,14 +704,6 @@ jobs: # --allow-tool shell(date) # --allow-tool shell(echo) # --allow-tool shell(find:*) - # --allow-tool shell(git add:*) - # --allow-tool shell(git branch:*) - # --allow-tool shell(git checkout:*) - # --allow-tool shell(git commit:*) - # --allow-tool shell(git merge:*) - # --allow-tool shell(git rm:*) - # --allow-tool shell(git status) - # --allow-tool shell(git switch:*) # --allow-tool shell(grep) # --allow-tool shell(grep:*) # --allow-tool shell(head) @@ -838,7 +724,7 @@ jobs: set -o pipefail # shellcheck disable=SC1003 sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -854,6 +740,11 @@ jobs: GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} GITHUB_WORKSPACE: ${{ github.workspace }} XDG_CONFIG_HOME: /home/runner + - name: Detect inference access error + id: detect-inference-error + if: always() + continue-on-error: true + run: bash /opt/gh-aw/actions/detect_inference_access_error.sh - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -909,7 +800,7 @@ jobs: SECRET_RUNTIME_TRIAGE_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - name: Upload Safe Outputs if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: safe-output path: ${{ env.GH_AW_SAFE_OUTPUTS }} @@ -932,13 +823,13 @@ jobs: await main(); - name: Upload sanitized agent output if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent-output path: ${{ env.GH_AW_AGENT_OUTPUT }} if-no-files-found: warn - name: Upload engine output files - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent_outputs path: | @@ -983,17 +874,15 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: agent-artifacts path: | /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ - /tmp/gh-aw/aw-*.patch if-no-files-found: ignore # --- Threat Detection (inline) --- - name: Check if detection needed @@ -1032,7 +921,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: WORKFLOW_NAME: "SDK Runtime Triage" - WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue and suggested-fix PR there" + WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there" HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} with: script: | @@ -1086,7 +975,7 @@ jobs: await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: threat-detection.log path: /tmp/gh-aw/threat-detection/detection.log @@ -1120,7 +1009,7 @@ jobs: if: (always()) && (needs.agent.result != 'skipped') runs-on: ubuntu-slim permissions: - contents: write + contents: read issues: write pull-requests: write outputs: @@ -1129,12 +1018,12 @@ jobs: total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5 + uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1 with: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -1181,9 +1070,9 @@ jobs: GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} - GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_GROUP_REPORTS: "false" + GH_AW_TIMEOUT_MINUTES: "20" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | @@ -1208,20 +1097,6 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); await main(); - - name: Handle Create Pull Request Error - id: handle_create_pr_error - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "SDK Runtime Triage" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); - await main(); pre_activation: if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'runtime triage' @@ -1231,7 +1106,7 @@ jobs: matched_command: '' steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5 + uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1 with: destination: /opt/gh-aw/actions - name: Check team membership for workflow @@ -1248,17 +1123,16 @@ jobs: await main(); safe_outputs: - needs: - - activation - - agent + needs: agent if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') runs-on: ubuntu-slim permissions: - contents: write + contents: read issues: write pull-requests: write timeout-minutes: 15 env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/cross-repo-issue-analysis" GH_AW_ENGINE_ID: "copilot" GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" GH_AW_WORKFLOW_NAME: "SDK Runtime Triage" @@ -1267,16 +1141,18 @@ jobs: code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@a7d371cc7e68f270ded0592942424548e05bf1c2 # v0.50.5 + uses: github/gh-aw/actions/setup@a86e657586e4ac5f549a790628971ec02f6a4a8f # v0.52.1 with: destination: /opt/gh-aw/actions - name: Download agent output artifact continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: agent-output path: /tmp/gh-aw/safeoutputs/ @@ -1285,42 +1161,15 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs/ find "/tmp/gh-aw/safeoutputs/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 - with: - name: agent-artifacts - path: /tmp/gh-aw/ - - name: Checkout repository - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: github/copilot-agent-runtime - ref: ${{ github.base_ref || github.ref_name }} - token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - env: - REPO_NAME: "github/copilot-agent-runtime" - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - name: Process Safe Outputs id: process_safe_outputs uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_pull_request\":{\"base_branch\":\"${{ github.base_ref || github.ref_name }}\",\"draft\":true,\"labels\":[\"upstream-from-sdk\",\"ai-suggested-fix\"],\"max\":1,\"max_patch_size\":1024,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"missing_data\":{},\"missing_tool\":{}}" - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | @@ -1330,7 +1179,7 @@ jobs: await main(); - name: Upload safe output items manifest if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: safe-output-items path: /tmp/safe-output-items.jsonl From 0dd6bfb52610307d67f9fd2a0b9ca8cc089aa8f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:21:13 +0000 Subject: [PATCH 26/61] Update @github/copilot to 1.0.4 (#796) - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 160 ++++-- dotnet/src/Generated/SessionEvents.cs | 636 +++++++++++++++------ go/generated_session_events.go | 510 ++++++++++++++--- go/rpc/generated_rpc.go | 119 +++- nodejs/package-lock.json | 56 +- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 101 +++- nodejs/src/generated/session-events.ts | 377 +++++++++++- python/copilot/generated/rpc.py | 180 +++++- python/copilot/generated/session_events.py | 473 ++++++++++++++- test/harness/package-lock.json | 56 +- test/harness/package.json | 2 +- 13 files changed, 2269 insertions(+), 405 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 2e5d164b7c..f6ca0382f1 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -35,10 +35,10 @@ internal class PingRequest public string? Message { get; set; } } -/// RPC data type for ModelCapabilitiesSupports operations. +/// Feature flags indicating what the model supports. public class ModelCapabilitiesSupports { - /// Gets or sets the vision value. + /// Whether this model supports vision/image input. [JsonPropertyName("vision")] public bool? Vision { get; set; } @@ -47,18 +47,18 @@ public class ModelCapabilitiesSupports public bool? ReasoningEffort { get; set; } } -/// RPC data type for ModelCapabilitiesLimits operations. +/// Token limits for prompts, outputs, and context window. public class ModelCapabilitiesLimits { - /// Gets or sets the max_prompt_tokens value. + /// Maximum number of prompt/input tokens. [JsonPropertyName("max_prompt_tokens")] public double? MaxPromptTokens { get; set; } - /// Gets or sets the max_output_tokens value. + /// Maximum number of output/completion tokens. [JsonPropertyName("max_output_tokens")] public double? MaxOutputTokens { get; set; } - /// Gets or sets the max_context_window_tokens value. + /// Maximum total context window size in tokens. [JsonPropertyName("max_context_window_tokens")] public double MaxContextWindowTokens { get; set; } } @@ -66,11 +66,11 @@ public class ModelCapabilitiesLimits /// Model capabilities and limits. public class ModelCapabilities { - /// Gets or sets the supports value. + /// Feature flags indicating what the model supports. [JsonPropertyName("supports")] public ModelCapabilitiesSupports Supports { get => field ??= new(); set; } - /// Gets or sets the limits value. + /// Token limits for prompts, outputs, and context window. [JsonPropertyName("limits")] public ModelCapabilitiesLimits Limits { get => field ??= new(); set; } } @@ -78,11 +78,11 @@ public class ModelCapabilities /// Policy state (if applicable). public class ModelPolicy { - /// Gets or sets the state value. + /// Current policy state for this model. [JsonPropertyName("state")] public string State { get; set; } = string.Empty; - /// Gets or sets the terms value. + /// Usage terms or conditions for this model. [JsonPropertyName("terms")] public string Terms { get; set; } = string.Empty; } @@ -90,7 +90,7 @@ public class ModelPolicy /// Billing information. public class ModelBilling { - /// Gets or sets the multiplier value. + /// Billing cost multiplier relative to the base rate. [JsonPropertyName("multiplier")] public double Multiplier { get; set; } } @@ -242,7 +242,7 @@ internal class SessionLogRequest /// RPC data type for SessionModelGetCurrent operations. public class SessionModelGetCurrentResult { - /// Gets or sets the modelId value. + /// Currently active model identifier. [JsonPropertyName("modelId")] public string? ModelId { get; set; } } @@ -258,7 +258,7 @@ internal class SessionModelGetCurrentRequest /// RPC data type for SessionModelSwitchTo operations. public class SessionModelSwitchToResult { - /// Gets or sets the modelId value. + /// Currently active model identifier after the switch. [JsonPropertyName("modelId")] public string? ModelId { get; set; } } @@ -270,13 +270,13 @@ internal class SessionModelSwitchToRequest [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Gets or sets the modelId value. + /// Model identifier to switch to. [JsonPropertyName("modelId")] public string ModelId { get; set; } = string.Empty; - /// Gets or sets the reasoningEffort value. + /// Reasoning effort level to use for the model. [JsonPropertyName("reasoningEffort")] - public SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; } + public string? ReasoningEffort { get; set; } } /// RPC data type for SessionModeGet operations. @@ -586,7 +586,7 @@ internal class SessionCompactionCompactRequest /// RPC data type for SessionToolsHandlePendingToolCall operations. public class SessionToolsHandlePendingToolCallResult { - /// Gets or sets the success value. + /// Whether the tool call result was handled successfully. [JsonPropertyName("success")] public bool Success { get; set; } } @@ -614,7 +614,7 @@ internal class SessionToolsHandlePendingToolCallRequest /// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations. public class SessionPermissionsHandlePendingPermissionRequestResult { - /// Gets or sets the success value. + /// Whether the permission request was handled successfully. [JsonPropertyName("success")] public bool Success { get; set; } } @@ -635,6 +635,58 @@ internal class SessionPermissionsHandlePendingPermissionRequestRequest public object Result { get; set; } = null!; } +/// RPC data type for SessionShellExec operations. +public class SessionShellExecResult +{ + /// Unique identifier for tracking streamed output. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; +} + +/// RPC data type for SessionShellExec operations. +internal class SessionShellExecRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Shell command to execute. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; + + /// Working directory (defaults to session working directory). + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } + + /// Timeout in milliseconds (default: 30000). + [JsonPropertyName("timeout")] + public double? Timeout { get; set; } +} + +/// RPC data type for SessionShellKill operations. +public class SessionShellKillResult +{ + /// Whether the signal was sent successfully. + [JsonPropertyName("killed")] + public bool Killed { get; set; } +} + +/// RPC data type for SessionShellKill operations. +internal class SessionShellKillRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Process identifier returned by shell.exec. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; + + /// Signal to send (default: SIGTERM). + [JsonPropertyName("signal")] + public SessionShellKillRequestSignal? Signal { get; set; } +} + /// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionLogRequestLevel @@ -651,25 +703,6 @@ public enum SessionLogRequestLevel } -/// Defines the allowed values. -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum SessionModelSwitchToRequestReasoningEffort -{ - /// The low variant. - [JsonStringEnumMemberName("low")] - Low, - /// The medium variant. - [JsonStringEnumMemberName("medium")] - Medium, - /// The high variant. - [JsonStringEnumMemberName("high")] - High, - /// The xhigh variant. - [JsonStringEnumMemberName("xhigh")] - Xhigh, -} - - /// The current agent mode. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode @@ -686,6 +719,22 @@ public enum SessionModeGetResultMode } +/// Signal to send (default: SIGTERM). +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionShellKillRequestSignal +{ + /// The SIGTERM variant. + [JsonStringEnumMemberName("SIGTERM")] + SIGTERM, + /// The SIGKILL variant. + [JsonStringEnumMemberName("SIGKILL")] + SIGKILL, + /// The SIGINT variant. + [JsonStringEnumMemberName("SIGINT")] + SIGINT, +} + + /// Provides server-scoped RPC methods (no session required). public class ServerRpc { @@ -787,6 +836,7 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Compaction = new CompactionApi(rpc, sessionId); Tools = new ToolsApi(rpc, sessionId); Permissions = new PermissionsApi(rpc, sessionId); + Shell = new ShellApi(rpc, sessionId); } /// Model APIs. @@ -816,6 +866,9 @@ internal SessionRpc(JsonRpc rpc, string sessionId) /// Permissions APIs. public PermissionsApi Permissions { get; } + /// Shell APIs. + public ShellApi Shell { get; } + /// Calls "session.log". public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) { @@ -844,7 +897,7 @@ public async Task GetCurrentAsync(CancellationToke } /// Calls "session.model.switchTo". - public async Task SwitchToAsync(string modelId, SessionModelSwitchToRequestReasoningEffort? reasoningEffort = null, CancellationToken cancellationToken = default) + public async Task SwitchToAsync(string modelId, string? reasoningEffort = null, CancellationToken cancellationToken = default) { var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId, ReasoningEffort = reasoningEffort }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.model.switchTo", [request], cancellationToken); @@ -1067,6 +1120,33 @@ public async Task Handle } } +/// Provides session-scoped Shell APIs. +public class ShellApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal ShellApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.shell.exec". + public async Task ExecAsync(string command, string? cwd = null, double? timeout = null, CancellationToken cancellationToken = default) + { + var request = new SessionShellExecRequest { SessionId = _sessionId, Command = command, Cwd = cwd, Timeout = timeout }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.shell.exec", [request], cancellationToken); + } + + /// Calls "session.shell.kill". + public async Task KillAsync(string processId, SessionShellKillRequestSignal? signal = null, CancellationToken cancellationToken = default) + { + var request = new SessionShellKillRequest { SessionId = _sessionId, ProcessId = processId, Signal = signal }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.shell.kill", [request], cancellationToken); + } +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -1115,6 +1195,10 @@ public async Task Handle [JsonSerializable(typeof(SessionPlanReadResult))] [JsonSerializable(typeof(SessionPlanUpdateRequest))] [JsonSerializable(typeof(SessionPlanUpdateResult))] +[JsonSerializable(typeof(SessionShellExecRequest))] +[JsonSerializable(typeof(SessionShellExecResult))] +[JsonSerializable(typeof(SessionShellKillRequest))] +[JsonSerializable(typeof(SessionShellKillResult))] [JsonSerializable(typeof(SessionToolsHandlePendingToolCallRequest))] [JsonSerializable(typeof(SessionToolsHandlePendingToolCallResult))] [JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6dbfa8941c..5ef1be3524 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -41,6 +41,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] [JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")] [JsonDerivedType(typeof(PermissionRequestedEvent), "permission.requested")] +[JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] [JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] @@ -57,6 +58,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SessionStartEvent), "session.start")] [JsonDerivedType(typeof(SessionTaskCompleteEvent), "session.task_complete")] [JsonDerivedType(typeof(SessionTitleChangedEvent), "session.title_changed")] +[JsonDerivedType(typeof(SessionToolsUpdatedEvent), "session.tools_updated")] [JsonDerivedType(typeof(SessionTruncationEvent), "session.truncation")] [JsonDerivedType(typeof(SessionUsageInfoEvent), "session.usage_info")] [JsonDerivedType(typeof(SessionWarningEvent), "session.warning")] @@ -114,7 +116,8 @@ public string ToJson() => private string DebuggerDisplay => ToJson(); } -/// Represents the session.start event. +/// Session initialization metadata including context and configuration. +/// Represents the session.start event. public partial class SessionStartEvent : SessionEvent { /// @@ -126,7 +129,8 @@ public partial class SessionStartEvent : SessionEvent public required SessionStartData Data { get; set; } } -/// Represents the session.resume event. +/// Session resume metadata including current context and event count. +/// Represents the session.resume event. public partial class SessionResumeEvent : SessionEvent { /// @@ -138,7 +142,8 @@ public partial class SessionResumeEvent : SessionEvent public required SessionResumeData Data { get; set; } } -/// Represents the session.error event. +/// Error details for timeline display including message and optional diagnostic information. +/// Represents the session.error event. public partial class SessionErrorEvent : SessionEvent { /// @@ -163,7 +168,8 @@ public partial class SessionIdleEvent : SessionEvent public required SessionIdleData Data { get; set; } } -/// Represents the session.title_changed event. +/// Session title change payload containing the new display title. +/// Represents the session.title_changed event. public partial class SessionTitleChangedEvent : SessionEvent { /// @@ -175,7 +181,8 @@ public partial class SessionTitleChangedEvent : SessionEvent public required SessionTitleChangedData Data { get; set; } } -/// Represents the session.info event. +/// Informational message for timeline display with categorization. +/// Represents the session.info event. public partial class SessionInfoEvent : SessionEvent { /// @@ -187,7 +194,8 @@ public partial class SessionInfoEvent : SessionEvent public required SessionInfoData Data { get; set; } } -/// Represents the session.warning event. +/// Warning message for timeline display with categorization. +/// Represents the session.warning event. public partial class SessionWarningEvent : SessionEvent { /// @@ -199,7 +207,8 @@ public partial class SessionWarningEvent : SessionEvent public required SessionWarningData Data { get; set; } } -/// Represents the session.model_change event. +/// Model change details including previous and new model identifiers. +/// Represents the session.model_change event. public partial class SessionModelChangeEvent : SessionEvent { /// @@ -211,7 +220,8 @@ public partial class SessionModelChangeEvent : SessionEvent public required SessionModelChangeData Data { get; set; } } -/// Represents the session.mode_changed event. +/// Agent mode change details including previous and new modes. +/// Represents the session.mode_changed event. public partial class SessionModeChangedEvent : SessionEvent { /// @@ -223,7 +233,8 @@ public partial class SessionModeChangedEvent : SessionEvent public required SessionModeChangedData Data { get; set; } } -/// Represents the session.plan_changed event. +/// Plan file operation details indicating what changed. +/// Represents the session.plan_changed event. public partial class SessionPlanChangedEvent : SessionEvent { /// @@ -235,7 +246,8 @@ public partial class SessionPlanChangedEvent : SessionEvent public required SessionPlanChangedData Data { get; set; } } -/// Represents the session.workspace_file_changed event. +/// Workspace file change details including path and operation type. +/// Represents the session.workspace_file_changed event. public partial class SessionWorkspaceFileChangedEvent : SessionEvent { /// @@ -247,7 +259,8 @@ public partial class SessionWorkspaceFileChangedEvent : SessionEvent public required SessionWorkspaceFileChangedData Data { get; set; } } -/// Represents the session.handoff event. +/// Session handoff metadata including source, context, and repository information. +/// Represents the session.handoff event. public partial class SessionHandoffEvent : SessionEvent { /// @@ -259,7 +272,8 @@ public partial class SessionHandoffEvent : SessionEvent public required SessionHandoffData Data { get; set; } } -/// Represents the session.truncation event. +/// Conversation truncation statistics including token counts and removed content metrics. +/// Represents the session.truncation event. public partial class SessionTruncationEvent : SessionEvent { /// @@ -271,7 +285,8 @@ public partial class SessionTruncationEvent : SessionEvent public required SessionTruncationData Data { get; set; } } -/// Represents the session.snapshot_rewind event. +/// Session rewind details including target event and count of removed events. +/// Represents the session.snapshot_rewind event. public partial class SessionSnapshotRewindEvent : SessionEvent { /// @@ -283,7 +298,8 @@ public partial class SessionSnapshotRewindEvent : SessionEvent public required SessionSnapshotRewindData Data { get; set; } } -/// Represents the session.shutdown event. +/// Session termination metrics including usage statistics, code changes, and shutdown reason. +/// Represents the session.shutdown event. public partial class SessionShutdownEvent : SessionEvent { /// @@ -295,7 +311,8 @@ public partial class SessionShutdownEvent : SessionEvent public required SessionShutdownData Data { get; set; } } -/// Represents the session.context_changed event. +/// Updated working directory and git context after the change. +/// Represents the session.context_changed event. public partial class SessionContextChangedEvent : SessionEvent { /// @@ -307,7 +324,8 @@ public partial class SessionContextChangedEvent : SessionEvent public required SessionContextChangedData Data { get; set; } } -/// Represents the session.usage_info event. +/// Current context window usage statistics including token and message counts. +/// Represents the session.usage_info event. public partial class SessionUsageInfoEvent : SessionEvent { /// @@ -332,7 +350,8 @@ public partial class SessionCompactionStartEvent : SessionEvent public required SessionCompactionStartData Data { get; set; } } -/// Represents the session.compaction_complete event. +/// Conversation compaction results including success status, metrics, and optional error details. +/// Represents the session.compaction_complete event. public partial class SessionCompactionCompleteEvent : SessionEvent { /// @@ -344,7 +363,8 @@ public partial class SessionCompactionCompleteEvent : SessionEvent public required SessionCompactionCompleteData Data { get; set; } } -/// Represents the session.task_complete event. +/// Task completion notification with optional summary from the agent. +/// Represents the session.task_complete event. public partial class SessionTaskCompleteEvent : SessionEvent { /// @@ -356,7 +376,8 @@ public partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } -/// Represents the user.message event. +/// User message content with optional attachments, source information, and interaction metadata. +/// Represents the user.message event. public partial class UserMessageEvent : SessionEvent { /// @@ -381,7 +402,8 @@ public partial class PendingMessagesModifiedEvent : SessionEvent public required PendingMessagesModifiedData Data { get; set; } } -/// Represents the assistant.turn_start event. +/// Turn initialization metadata including identifier and interaction tracking. +/// Represents the assistant.turn_start event. public partial class AssistantTurnStartEvent : SessionEvent { /// @@ -393,7 +415,8 @@ public partial class AssistantTurnStartEvent : SessionEvent public required AssistantTurnStartData Data { get; set; } } -/// Represents the assistant.intent event. +/// Agent intent description for current activity or plan. +/// Represents the assistant.intent event. public partial class AssistantIntentEvent : SessionEvent { /// @@ -405,7 +428,8 @@ public partial class AssistantIntentEvent : SessionEvent public required AssistantIntentData Data { get; set; } } -/// Represents the assistant.reasoning event. +/// Assistant reasoning content for timeline display with complete thinking text. +/// Represents the assistant.reasoning event. public partial class AssistantReasoningEvent : SessionEvent { /// @@ -417,7 +441,8 @@ public partial class AssistantReasoningEvent : SessionEvent public required AssistantReasoningData Data { get; set; } } -/// Represents the assistant.reasoning_delta event. +/// Streaming reasoning delta for incremental extended thinking updates. +/// Represents the assistant.reasoning_delta event. public partial class AssistantReasoningDeltaEvent : SessionEvent { /// @@ -429,7 +454,8 @@ public partial class AssistantReasoningDeltaEvent : SessionEvent public required AssistantReasoningDeltaData Data { get; set; } } -/// Represents the assistant.streaming_delta event. +/// Streaming response progress with cumulative byte count. +/// Represents the assistant.streaming_delta event. public partial class AssistantStreamingDeltaEvent : SessionEvent { /// @@ -441,7 +467,8 @@ public partial class AssistantStreamingDeltaEvent : SessionEvent public required AssistantStreamingDeltaData Data { get; set; } } -/// Represents the assistant.message event. +/// Assistant response containing text content, optional tool requests, and interaction metadata. +/// Represents the assistant.message event. public partial class AssistantMessageEvent : SessionEvent { /// @@ -453,7 +480,8 @@ public partial class AssistantMessageEvent : SessionEvent public required AssistantMessageData Data { get; set; } } -/// Represents the assistant.message_delta event. +/// Streaming assistant message delta for incremental response updates. +/// Represents the assistant.message_delta event. public partial class AssistantMessageDeltaEvent : SessionEvent { /// @@ -465,7 +493,8 @@ public partial class AssistantMessageDeltaEvent : SessionEvent public required AssistantMessageDeltaData Data { get; set; } } -/// Represents the assistant.turn_end event. +/// Turn completion metadata including the turn identifier. +/// Represents the assistant.turn_end event. public partial class AssistantTurnEndEvent : SessionEvent { /// @@ -477,7 +506,8 @@ public partial class AssistantTurnEndEvent : SessionEvent public required AssistantTurnEndData Data { get; set; } } -/// Represents the assistant.usage event. +/// LLM API call usage metrics including tokens, costs, quotas, and billing information. +/// Represents the assistant.usage event. public partial class AssistantUsageEvent : SessionEvent { /// @@ -489,7 +519,8 @@ public partial class AssistantUsageEvent : SessionEvent public required AssistantUsageData Data { get; set; } } -/// Represents the abort event. +/// Turn abort information including the reason for termination. +/// Represents the abort event. public partial class AbortEvent : SessionEvent { /// @@ -501,7 +532,8 @@ public partial class AbortEvent : SessionEvent public required AbortData Data { get; set; } } -/// Represents the tool.user_requested event. +/// User-initiated tool invocation request with tool name and arguments. +/// Represents the tool.user_requested event. public partial class ToolUserRequestedEvent : SessionEvent { /// @@ -513,7 +545,8 @@ public partial class ToolUserRequestedEvent : SessionEvent public required ToolUserRequestedData Data { get; set; } } -/// Represents the tool.execution_start event. +/// Tool execution startup details including MCP server information when applicable. +/// Represents the tool.execution_start event. public partial class ToolExecutionStartEvent : SessionEvent { /// @@ -525,7 +558,8 @@ public partial class ToolExecutionStartEvent : SessionEvent public required ToolExecutionStartData Data { get; set; } } -/// Represents the tool.execution_partial_result event. +/// Streaming tool execution output for incremental result display. +/// Represents the tool.execution_partial_result event. public partial class ToolExecutionPartialResultEvent : SessionEvent { /// @@ -537,7 +571,8 @@ public partial class ToolExecutionPartialResultEvent : SessionEvent public required ToolExecutionPartialResultData Data { get; set; } } -/// Represents the tool.execution_progress event. +/// Tool execution progress notification with status message. +/// Represents the tool.execution_progress event. public partial class ToolExecutionProgressEvent : SessionEvent { /// @@ -549,7 +584,8 @@ public partial class ToolExecutionProgressEvent : SessionEvent public required ToolExecutionProgressData Data { get; set; } } -/// Represents the tool.execution_complete event. +/// Tool execution completion results including success status, detailed output, and error information. +/// Represents the tool.execution_complete event. public partial class ToolExecutionCompleteEvent : SessionEvent { /// @@ -561,7 +597,8 @@ public partial class ToolExecutionCompleteEvent : SessionEvent public required ToolExecutionCompleteData Data { get; set; } } -/// Represents the skill.invoked event. +/// Skill invocation details including content, allowed tools, and plugin metadata. +/// Represents the skill.invoked event. public partial class SkillInvokedEvent : SessionEvent { /// @@ -573,7 +610,8 @@ public partial class SkillInvokedEvent : SessionEvent public required SkillInvokedData Data { get; set; } } -/// Represents the subagent.started event. +/// Sub-agent startup details including parent tool call and agent information. +/// Represents the subagent.started event. public partial class SubagentStartedEvent : SessionEvent { /// @@ -585,7 +623,8 @@ public partial class SubagentStartedEvent : SessionEvent public required SubagentStartedData Data { get; set; } } -/// Represents the subagent.completed event. +/// Sub-agent completion details for successful execution. +/// Represents the subagent.completed event. public partial class SubagentCompletedEvent : SessionEvent { /// @@ -597,7 +636,8 @@ public partial class SubagentCompletedEvent : SessionEvent public required SubagentCompletedData Data { get; set; } } -/// Represents the subagent.failed event. +/// Sub-agent failure details including error message and agent information. +/// Represents the subagent.failed event. public partial class SubagentFailedEvent : SessionEvent { /// @@ -609,7 +649,8 @@ public partial class SubagentFailedEvent : SessionEvent public required SubagentFailedData Data { get; set; } } -/// Represents the subagent.selected event. +/// Custom agent selection details including name and available tools. +/// Represents the subagent.selected event. public partial class SubagentSelectedEvent : SessionEvent { /// @@ -634,7 +675,8 @@ public partial class SubagentDeselectedEvent : SessionEvent public required SubagentDeselectedData Data { get; set; } } -/// Represents the hook.start event. +/// Hook invocation start details including type and input data. +/// Represents the hook.start event. public partial class HookStartEvent : SessionEvent { /// @@ -646,7 +688,8 @@ public partial class HookStartEvent : SessionEvent public required HookStartData Data { get; set; } } -/// Represents the hook.end event. +/// Hook invocation completion details including output, success status, and error information. +/// Represents the hook.end event. public partial class HookEndEvent : SessionEvent { /// @@ -658,7 +701,8 @@ public partial class HookEndEvent : SessionEvent public required HookEndData Data { get; set; } } -/// Represents the system.message event. +/// System or developer message content with role and optional template metadata. +/// Represents the system.message event. public partial class SystemMessageEvent : SessionEvent { /// @@ -670,7 +714,8 @@ public partial class SystemMessageEvent : SessionEvent public required SystemMessageData Data { get; set; } } -/// Represents the system.notification event. +/// System-generated notification for runtime events like background task completion. +/// Represents the system.notification event. public partial class SystemNotificationEvent : SessionEvent { /// @@ -682,7 +727,8 @@ public partial class SystemNotificationEvent : SessionEvent public required SystemNotificationData Data { get; set; } } -/// Represents the permission.requested event. +/// Permission request notification requiring client approval with request details. +/// Represents the permission.requested event. public partial class PermissionRequestedEvent : SessionEvent { /// @@ -694,7 +740,8 @@ public partial class PermissionRequestedEvent : SessionEvent public required PermissionRequestedData Data { get; set; } } -/// Represents the permission.completed event. +/// Permission request completion notification signaling UI dismissal. +/// Represents the permission.completed event. public partial class PermissionCompletedEvent : SessionEvent { /// @@ -706,7 +753,8 @@ public partial class PermissionCompletedEvent : SessionEvent public required PermissionCompletedData Data { get; set; } } -/// Represents the user_input.requested event. +/// User input request notification with question and optional predefined choices. +/// Represents the user_input.requested event. public partial class UserInputRequestedEvent : SessionEvent { /// @@ -718,7 +766,8 @@ public partial class UserInputRequestedEvent : SessionEvent public required UserInputRequestedData Data { get; set; } } -/// Represents the user_input.completed event. +/// User input request completion notification signaling UI dismissal. +/// Represents the user_input.completed event. public partial class UserInputCompletedEvent : SessionEvent { /// @@ -730,7 +779,8 @@ public partial class UserInputCompletedEvent : SessionEvent public required UserInputCompletedData Data { get; set; } } -/// Represents the elicitation.requested event. +/// Structured form elicitation request with JSON schema definition for form fields. +/// Represents the elicitation.requested event. public partial class ElicitationRequestedEvent : SessionEvent { /// @@ -742,7 +792,8 @@ public partial class ElicitationRequestedEvent : SessionEvent public required ElicitationRequestedData Data { get; set; } } -/// Represents the elicitation.completed event. +/// Elicitation request completion notification signaling UI dismissal. +/// Represents the elicitation.completed event. public partial class ElicitationCompletedEvent : SessionEvent { /// @@ -754,7 +805,8 @@ public partial class ElicitationCompletedEvent : SessionEvent public required ElicitationCompletedData Data { get; set; } } -/// Represents the external_tool.requested event. +/// External tool invocation request for client-side tool execution. +/// Represents the external_tool.requested event. public partial class ExternalToolRequestedEvent : SessionEvent { /// @@ -766,7 +818,8 @@ public partial class ExternalToolRequestedEvent : SessionEvent public required ExternalToolRequestedData Data { get; set; } } -/// Represents the external_tool.completed event. +/// External tool completion notification signaling UI dismissal. +/// Represents the external_tool.completed event. public partial class ExternalToolCompletedEvent : SessionEvent { /// @@ -778,7 +831,8 @@ public partial class ExternalToolCompletedEvent : SessionEvent public required ExternalToolCompletedData Data { get; set; } } -/// Represents the command.queued event. +/// Queued slash command dispatch request for client execution. +/// Represents the command.queued event. public partial class CommandQueuedEvent : SessionEvent { /// @@ -790,7 +844,8 @@ public partial class CommandQueuedEvent : SessionEvent public required CommandQueuedData Data { get; set; } } -/// Represents the command.completed event. +/// Queued command completion notification signaling UI dismissal. +/// Represents the command.completed event. public partial class CommandCompletedEvent : SessionEvent { /// @@ -802,7 +857,8 @@ public partial class CommandCompletedEvent : SessionEvent public required CommandCompletedData Data { get; set; } } -/// Represents the exit_plan_mode.requested event. +/// Plan approval request with plan content and available user actions. +/// Represents the exit_plan_mode.requested event. public partial class ExitPlanModeRequestedEvent : SessionEvent { /// @@ -814,7 +870,8 @@ public partial class ExitPlanModeRequestedEvent : SessionEvent public required ExitPlanModeRequestedData Data { get; set; } } -/// Represents the exit_plan_mode.completed event. +/// Plan mode exit completion notification signaling UI dismissal. +/// Represents the exit_plan_mode.completed event. public partial class ExitPlanModeCompletedEvent : SessionEvent { /// @@ -826,7 +883,31 @@ public partial class ExitPlanModeCompletedEvent : SessionEvent public required ExitPlanModeCompletedData Data { get; set; } } -/// Event payload for . +/// Represents the session.tools_updated event. +public partial class SessionToolsUpdatedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.tools_updated"; + + /// The session.tools_updated event payload. + [JsonPropertyName("data")] + public required SessionToolsUpdatedData Data { get; set; } +} + +/// Represents the session.background_tasks_changed event. +public partial class SessionBackgroundTasksChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.background_tasks_changed"; + + /// The session.background_tasks_changed event payload. + [JsonPropertyName("data")] + public required SessionBackgroundTasksChangedData Data { get; set; } +} + +/// Session initialization metadata including context and configuration. public partial class SessionStartData { /// Unique identifier for the session. @@ -854,18 +935,23 @@ public partial class SessionStartData [JsonPropertyName("selectedModel")] public string? SelectedModel { get; set; } + /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + /// Working directory and git context at session start. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionStartDataContext? Context { get; set; } - /// Gets or sets the alreadyInUse value. + /// Whether the session was already in use by another client at start time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } } -/// Event payload for . +/// Session resume metadata including current context and event count. public partial class SessionResumeData { /// ISO 8601 timestamp when the session was resumed. @@ -876,18 +962,28 @@ public partial class SessionResumeData [JsonPropertyName("eventCount")] public required double EventCount { get; set; } + /// Model currently selected at resume time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("selectedModel")] + public string? SelectedModel { get; set; } + + /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + /// Updated working directory and git context at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("context")] public SessionResumeDataContext? Context { get; set; } - /// Gets or sets the alreadyInUse value. + /// Whether the session was already in use by another client at resume time. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("alreadyInUse")] public bool? AlreadyInUse { get; set; } } -/// Event payload for . +/// Error details for timeline display including message and optional diagnostic information. public partial class SessionErrorData { /// Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query"). @@ -923,7 +1019,7 @@ public partial class SessionIdleData public SessionIdleDataBackgroundTasks? BackgroundTasks { get; set; } } -/// Event payload for . +/// Session title change payload containing the new display title. public partial class SessionTitleChangedData { /// The new display title for the session. @@ -931,7 +1027,7 @@ public partial class SessionTitleChangedData public required string Title { get; set; } } -/// Event payload for . +/// Informational message for timeline display with categorization. public partial class SessionInfoData { /// Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model"). @@ -943,7 +1039,7 @@ public partial class SessionInfoData public required string Message { get; set; } } -/// Event payload for . +/// Warning message for timeline display with categorization. public partial class SessionWarningData { /// Category of warning (e.g., "subscription", "policy", "mcp"). @@ -955,7 +1051,7 @@ public partial class SessionWarningData public required string Message { get; set; } } -/// Event payload for . +/// Model change details including previous and new model identifiers. public partial class SessionModelChangeData { /// Model that was previously selected, if any. @@ -966,9 +1062,19 @@ public partial class SessionModelChangeData /// Newly selected model identifier. [JsonPropertyName("newModel")] public required string NewModel { get; set; } + + /// Reasoning effort level before the model change, if applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("previousReasoningEffort")] + public string? PreviousReasoningEffort { get; set; } + + /// Reasoning effort level after the model change, if applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } } -/// Event payload for . +/// Agent mode change details including previous and new modes. public partial class SessionModeChangedData { /// Agent mode before the change (e.g., "interactive", "plan", "autopilot"). @@ -980,7 +1086,7 @@ public partial class SessionModeChangedData public required string NewMode { get; set; } } -/// Event payload for . +/// Plan file operation details indicating what changed. public partial class SessionPlanChangedData { /// The type of operation performed on the plan file. @@ -988,7 +1094,7 @@ public partial class SessionPlanChangedData public required SessionPlanChangedDataOperation Operation { get; set; } } -/// Event payload for . +/// Workspace file change details including path and operation type. public partial class SessionWorkspaceFileChangedData { /// Relative path within the session workspace files directory. @@ -1000,7 +1106,7 @@ public partial class SessionWorkspaceFileChangedData public required SessionWorkspaceFileChangedDataOperation Operation { get; set; } } -/// Event payload for . +/// Session handoff metadata including source, context, and repository information. public partial class SessionHandoffData { /// ISO 8601 timestamp when the handoff occurred. @@ -1032,7 +1138,7 @@ public partial class SessionHandoffData public string? RemoteSessionId { get; set; } } -/// Event payload for . +/// Conversation truncation statistics including token counts and removed content metrics. public partial class SessionTruncationData { /// Maximum token count for the model's context window. @@ -1068,7 +1174,7 @@ public partial class SessionTruncationData public required string PerformedBy { get; set; } } -/// Event payload for . +/// Session rewind details including target event and count of removed events. public partial class SessionSnapshotRewindData { /// Event ID that was rewound to; all events after this one were removed. @@ -1080,7 +1186,7 @@ public partial class SessionSnapshotRewindData public required double EventsRemoved { get; set; } } -/// Event payload for . +/// Session termination metrics including usage statistics, code changes, and shutdown reason. public partial class SessionShutdownData { /// Whether the session ended normally ("routine") or due to a crash/fatal error ("error"). @@ -1118,7 +1224,7 @@ public partial class SessionShutdownData public string? CurrentModel { get; set; } } -/// Event payload for . +/// Updated working directory and git context after the change. public partial class SessionContextChangedData { /// Current working directory path. @@ -1130,18 +1236,33 @@ public partial class SessionContextChangedData [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } - /// Repository identifier in "owner/name" format, derived from the git remote URL. + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Hosting platform type of the repository (github or ado). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hostType")] + public SessionStartDataContextHostType? HostType { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } + + /// Head commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Base commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } } -/// Event payload for . +/// Current context window usage statistics including token and message counts. public partial class SessionUsageInfoData { /// Maximum token count for the model's context window. @@ -1162,7 +1283,7 @@ public partial class SessionCompactionStartData { } -/// Event payload for . +/// Conversation compaction results including success status, metrics, and optional error details. public partial class SessionCompactionCompleteData { /// Whether compaction completed successfully. @@ -1225,7 +1346,7 @@ public partial class SessionCompactionCompleteData public string? RequestId { get; set; } } -/// Event payload for . +/// Task completion notification with optional summary from the agent. public partial class SessionTaskCompleteData { /// Optional summary of the completed task, provided by the agent. @@ -1234,7 +1355,7 @@ public partial class SessionTaskCompleteData public string? Summary { get; set; } } -/// Event payload for . +/// User message content with optional attachments, source information, and interaction metadata. public partial class UserMessageData { /// The user's message text as displayed in the timeline. @@ -1251,10 +1372,10 @@ public partial class UserMessageData [JsonPropertyName("attachments")] public UserMessageDataAttachmentsItem[]? Attachments { get; set; } - /// Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user). + /// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] - public string? Source { get; set; } + public UserMessageDataSource? Source { get; set; } /// The agent mode that was active when this message was sent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1272,7 +1393,7 @@ public partial class PendingMessagesModifiedData { } -/// Event payload for . +/// Turn initialization metadata including identifier and interaction tracking. public partial class AssistantTurnStartData { /// Identifier for this turn within the agentic loop, typically a stringified turn number. @@ -1285,7 +1406,7 @@ public partial class AssistantTurnStartData public string? InteractionId { get; set; } } -/// Event payload for . +/// Agent intent description for current activity or plan. public partial class AssistantIntentData { /// Short description of what the agent is currently doing or planning to do. @@ -1293,7 +1414,7 @@ public partial class AssistantIntentData public required string Intent { get; set; } } -/// Event payload for . +/// Assistant reasoning content for timeline display with complete thinking text. public partial class AssistantReasoningData { /// Unique identifier for this reasoning block. @@ -1305,7 +1426,7 @@ public partial class AssistantReasoningData public required string Content { get; set; } } -/// Event payload for . +/// Streaming reasoning delta for incremental extended thinking updates. public partial class AssistantReasoningDeltaData { /// Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event. @@ -1317,7 +1438,7 @@ public partial class AssistantReasoningDeltaData public required string DeltaContent { get; set; } } -/// Event payload for . +/// Streaming response progress with cumulative byte count. public partial class AssistantStreamingDeltaData { /// Cumulative total bytes received from the streaming response so far. @@ -1325,7 +1446,7 @@ public partial class AssistantStreamingDeltaData public required double TotalResponseSizeBytes { get; set; } } -/// Event payload for . +/// Assistant response containing text content, optional tool requests, and interaction metadata. public partial class AssistantMessageData { /// Unique identifier for this assistant message. @@ -1377,7 +1498,7 @@ public partial class AssistantMessageData public string? ParentToolCallId { get; set; } } -/// Event payload for . +/// Streaming assistant message delta for incremental response updates. public partial class AssistantMessageDeltaData { /// Message ID this delta belongs to, matching the corresponding assistant.message event. @@ -1394,7 +1515,7 @@ public partial class AssistantMessageDeltaData public string? ParentToolCallId { get; set; } } -/// Event payload for . +/// Turn completion metadata including the turn identifier. public partial class AssistantTurnEndData { /// Identifier of the turn that has ended, matching the corresponding assistant.turn_start event. @@ -1402,7 +1523,7 @@ public partial class AssistantTurnEndData public required string TurnId { get; set; } } -/// Event payload for . +/// LLM API call usage metrics including tokens, costs, quotas, and billing information. public partial class AssistantUsageData { /// Model identifier used for this API call. @@ -1468,9 +1589,14 @@ public partial class AssistantUsageData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("copilotUsage")] public AssistantUsageDataCopilotUsage? CopilotUsage { get; set; } + + /// Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh"). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } } -/// Event payload for . +/// Turn abort information including the reason for termination. public partial class AbortData { /// Reason the current turn was aborted (e.g., "user initiated"). @@ -1478,7 +1604,7 @@ public partial class AbortData public required string Reason { get; set; } } -/// Event payload for . +/// User-initiated tool invocation request with tool name and arguments. public partial class ToolUserRequestedData { /// Unique identifier for this tool call. @@ -1495,7 +1621,7 @@ public partial class ToolUserRequestedData public object? Arguments { get; set; } } -/// Event payload for . +/// Tool execution startup details including MCP server information when applicable. public partial class ToolExecutionStartData { /// Unique identifier for this tool call. @@ -1527,7 +1653,7 @@ public partial class ToolExecutionStartData public string? ParentToolCallId { get; set; } } -/// Event payload for . +/// Streaming tool execution output for incremental result display. public partial class ToolExecutionPartialResultData { /// Tool call ID this partial result belongs to. @@ -1539,7 +1665,7 @@ public partial class ToolExecutionPartialResultData public required string PartialOutput { get; set; } } -/// Event payload for . +/// Tool execution progress notification with status message. public partial class ToolExecutionProgressData { /// Tool call ID this progress notification belongs to. @@ -1551,7 +1677,7 @@ public partial class ToolExecutionProgressData public required string ProgressMessage { get; set; } } -/// Event payload for . +/// Tool execution completion results including success status, detailed output, and error information. public partial class ToolExecutionCompleteData { /// Unique identifier for the completed tool call. @@ -1598,7 +1724,7 @@ public partial class ToolExecutionCompleteData public string? ParentToolCallId { get; set; } } -/// Event payload for . +/// Skill invocation details including content, allowed tools, and plugin metadata. public partial class SkillInvokedData { /// Name of the invoked skill. @@ -1629,7 +1755,7 @@ public partial class SkillInvokedData public string? PluginVersion { get; set; } } -/// Event payload for . +/// Sub-agent startup details including parent tool call and agent information. public partial class SubagentStartedData { /// Tool call ID of the parent tool invocation that spawned this sub-agent. @@ -1649,7 +1775,7 @@ public partial class SubagentStartedData public required string AgentDescription { get; set; } } -/// Event payload for . +/// Sub-agent completion details for successful execution. public partial class SubagentCompletedData { /// Tool call ID of the parent tool invocation that spawned this sub-agent. @@ -1665,7 +1791,7 @@ public partial class SubagentCompletedData public required string AgentDisplayName { get; set; } } -/// Event payload for . +/// Sub-agent failure details including error message and agent information. public partial class SubagentFailedData { /// Tool call ID of the parent tool invocation that spawned this sub-agent. @@ -1685,7 +1811,7 @@ public partial class SubagentFailedData public required string Error { get; set; } } -/// Event payload for . +/// Custom agent selection details including name and available tools. public partial class SubagentSelectedData { /// Internal name of the selected custom agent. @@ -1706,7 +1832,7 @@ public partial class SubagentDeselectedData { } -/// Event payload for . +/// Hook invocation start details including type and input data. public partial class HookStartData { /// Unique identifier for this hook invocation. @@ -1723,7 +1849,7 @@ public partial class HookStartData public object? Input { get; set; } } -/// Event payload for . +/// Hook invocation completion details including output, success status, and error information. public partial class HookEndData { /// Identifier matching the corresponding hook.start event. @@ -1749,7 +1875,7 @@ public partial class HookEndData public HookEndDataError? Error { get; set; } } -/// Event payload for . +/// System or developer message content with role and optional template metadata. public partial class SystemMessageData { /// The system or developer prompt text. @@ -1771,7 +1897,7 @@ public partial class SystemMessageData public SystemMessageDataMetadata? Metadata { get; set; } } -/// Event payload for . +/// System-generated notification for runtime events like background task completion. public partial class SystemNotificationData { /// The notification text, typically wrapped in <system_notification> XML tags. @@ -1783,7 +1909,7 @@ public partial class SystemNotificationData public required SystemNotificationDataKind Kind { get; set; } } -/// Event payload for . +/// Permission request notification requiring client approval with request details. public partial class PermissionRequestedData { /// Unique identifier for this permission request; used to respond via session.respondToPermission(). @@ -1795,7 +1921,7 @@ public partial class PermissionRequestedData public required PermissionRequest PermissionRequest { get; set; } } -/// Event payload for . +/// Permission request completion notification signaling UI dismissal. public partial class PermissionCompletedData { /// Request ID of the resolved permission request; clients should dismiss any UI for this request. @@ -1807,7 +1933,7 @@ public partial class PermissionCompletedData public required PermissionCompletedDataResult Result { get; set; } } -/// Event payload for . +/// User input request notification with question and optional predefined choices. public partial class UserInputRequestedData { /// Unique identifier for this input request; used to respond via session.respondToUserInput(). @@ -1829,7 +1955,7 @@ public partial class UserInputRequestedData public bool? AllowFreeform { get; set; } } -/// Event payload for . +/// User input request completion notification signaling UI dismissal. public partial class UserInputCompletedData { /// Request ID of the resolved user input request; clients should dismiss any UI for this request. @@ -1837,7 +1963,7 @@ public partial class UserInputCompletedData public required string RequestId { get; set; } } -/// Event payload for . +/// Structured form elicitation request with JSON schema definition for form fields. public partial class ElicitationRequestedData { /// Unique identifier for this elicitation request; used to respond via session.respondToElicitation(). @@ -1858,7 +1984,7 @@ public partial class ElicitationRequestedData public required ElicitationRequestedDataRequestedSchema RequestedSchema { get; set; } } -/// Event payload for . +/// Elicitation request completion notification signaling UI dismissal. public partial class ElicitationCompletedData { /// Request ID of the resolved elicitation request; clients should dismiss any UI for this request. @@ -1866,7 +1992,7 @@ public partial class ElicitationCompletedData public required string RequestId { get; set; } } -/// Event payload for . +/// External tool invocation request for client-side tool execution. public partial class ExternalToolRequestedData { /// Unique identifier for this request; used to respond via session.respondToExternalTool(). @@ -1889,9 +2015,19 @@ public partial class ExternalToolRequestedData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] public object? Arguments { get; set; } + + /// W3C Trace Context traceparent header for the execute_tool span. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("traceparent")] + public string? Traceparent { get; set; } + + /// W3C Trace Context tracestate header for the execute_tool span. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tracestate")] + public string? Tracestate { get; set; } } -/// Event payload for . +/// External tool completion notification signaling UI dismissal. public partial class ExternalToolCompletedData { /// Request ID of the resolved external tool request; clients should dismiss any UI for this request. @@ -1899,7 +2035,7 @@ public partial class ExternalToolCompletedData public required string RequestId { get; set; } } -/// Event payload for . +/// Queued slash command dispatch request for client execution. public partial class CommandQueuedData { /// Unique identifier for this request; used to respond via session.respondToQueuedCommand(). @@ -1911,7 +2047,7 @@ public partial class CommandQueuedData public required string Command { get; set; } } -/// Event payload for . +/// Queued command completion notification signaling UI dismissal. public partial class CommandCompletedData { /// Request ID of the resolved command request; clients should dismiss any UI for this request. @@ -1919,7 +2055,7 @@ public partial class CommandCompletedData public required string RequestId { get; set; } } -/// Event payload for . +/// Plan approval request with plan content and available user actions. public partial class ExitPlanModeRequestedData { /// Unique identifier for this request; used to respond via session.respondToExitPlanMode(). @@ -1943,7 +2079,7 @@ public partial class ExitPlanModeRequestedData public required string RecommendedAction { get; set; } } -/// Event payload for . +/// Plan mode exit completion notification signaling UI dismissal. public partial class ExitPlanModeCompletedData { /// Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request. @@ -1951,6 +2087,19 @@ public partial class ExitPlanModeCompletedData public required string RequestId { get; set; } } +/// Event payload for . +public partial class SessionToolsUpdatedData +{ + /// Gets or sets the model value. + [JsonPropertyName("model")] + public required string Model { get; set; } +} + +/// Event payload for . +public partial class SessionBackgroundTasksChangedData +{ +} + /// Working directory and git context at session start. /// Nested data type for SessionStartDataContext. public partial class SessionStartDataContext @@ -1964,15 +2113,30 @@ public partial class SessionStartDataContext [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } - /// Repository identifier in "owner/name" format, derived from the git remote URL. + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Hosting platform type of the repository (github or ado). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hostType")] + public SessionStartDataContextHostType? HostType { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } + + /// Head commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Base commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } } /// Updated working directory and git context at resume time. @@ -1988,18 +2152,34 @@ public partial class SessionResumeDataContext [JsonPropertyName("gitRoot")] public string? GitRoot { get; set; } - /// Repository identifier in "owner/name" format, derived from the git remote URL. + /// Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("repository")] public string? Repository { get; set; } + /// Hosting platform type of the repository (github or ado). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hostType")] + public SessionStartDataContextHostType? HostType { get; set; } + /// Current git branch name. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("branch")] public string? Branch { get; set; } + + /// Head commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } + + /// Base commit of current git branch at session start time. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } } -/// Nested data type for SessionIdleDataBackgroundTasksAgentsItem. +/// A background agent task. +/// Nested data type for SessionIdleDataBackgroundTasksAgentsItem. public partial class SessionIdleDataBackgroundTasksAgentsItem { /// Unique identifier of the background agent. @@ -2016,7 +2196,8 @@ public partial class SessionIdleDataBackgroundTasksAgentsItem public string? Description { get; set; } } -/// Nested data type for SessionIdleDataBackgroundTasksShellsItem. +/// A background shell command. +/// Nested data type for SessionIdleDataBackgroundTasksShellsItem. public partial class SessionIdleDataBackgroundTasksShellsItem { /// Unique identifier of the background shell. @@ -2107,14 +2288,15 @@ public partial class UserMessageDataAttachmentsItemFileLineRange public required double End { get; set; } } -/// The file variant of . +/// File attachment. +/// The file variant of . public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachmentsItem { /// [JsonIgnore] public override string Type => "file"; - /// Absolute file or directory path. + /// Absolute file path. [JsonPropertyName("path")] public required string Path { get; set; } @@ -2128,41 +2310,25 @@ public partial class UserMessageDataAttachmentsItemFile : UserMessageDataAttachm public UserMessageDataAttachmentsItemFileLineRange? LineRange { get; set; } } -/// Optional line range to scope the attachment to a specific section of the file. -/// Nested data type for UserMessageDataAttachmentsItemDirectoryLineRange. -public partial class UserMessageDataAttachmentsItemDirectoryLineRange -{ - /// Start line number (1-based). - [JsonPropertyName("start")] - public required double Start { get; set; } - - /// End line number (1-based, inclusive). - [JsonPropertyName("end")] - public required double End { get; set; } -} - -/// The directory variant of . +/// Directory attachment. +/// The directory variant of . public partial class UserMessageDataAttachmentsItemDirectory : UserMessageDataAttachmentsItem { /// [JsonIgnore] public override string Type => "directory"; - /// Absolute file or directory path. + /// Absolute directory path. [JsonPropertyName("path")] public required string Path { get; set; } /// User-facing display name for the attachment. [JsonPropertyName("displayName")] public required string DisplayName { get; set; } - - /// Optional line range to scope the attachment to a specific section of the file. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("lineRange")] - public UserMessageDataAttachmentsItemDirectoryLineRange? LineRange { get; set; } } -/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionStart. +/// Start position of the selection. +/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionStart. public partial class UserMessageDataAttachmentsItemSelectionSelectionStart { /// Start line number (0-based). @@ -2174,7 +2340,8 @@ public partial class UserMessageDataAttachmentsItemSelectionSelectionStart public required double Character { get; set; } } -/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionEnd. +/// End position of the selection. +/// Nested data type for UserMessageDataAttachmentsItemSelectionSelectionEnd. public partial class UserMessageDataAttachmentsItemSelectionSelectionEnd { /// End line number (0-based). @@ -2190,16 +2357,17 @@ public partial class UserMessageDataAttachmentsItemSelectionSelectionEnd /// Nested data type for UserMessageDataAttachmentsItemSelectionSelection. public partial class UserMessageDataAttachmentsItemSelectionSelection { - /// Gets or sets the start value. + /// Start position of the selection. [JsonPropertyName("start")] public required UserMessageDataAttachmentsItemSelectionSelectionStart Start { get; set; } - /// Gets or sets the end value. + /// End position of the selection. [JsonPropertyName("end")] public required UserMessageDataAttachmentsItemSelectionSelectionEnd End { get; set; } } -/// The selection variant of . +/// Code selection attachment from an editor. +/// The selection variant of . public partial class UserMessageDataAttachmentsItemSelection : UserMessageDataAttachmentsItem { /// @@ -2223,7 +2391,8 @@ public partial class UserMessageDataAttachmentsItemSelection : UserMessageDataAt public required UserMessageDataAttachmentsItemSelectionSelection Selection { get; set; } } -/// The github_reference variant of . +/// GitHub issue, pull request, or discussion reference. +/// The github_reference variant of . public partial class UserMessageDataAttachmentsItemGithubReference : UserMessageDataAttachmentsItem { /// @@ -2251,7 +2420,30 @@ public partial class UserMessageDataAttachmentsItemGithubReference : UserMessage public required string Url { get; set; } } -/// Polymorphic base type discriminated by type. +/// Blob attachment with inline base64-encoded data. +/// The blob variant of . +public partial class UserMessageDataAttachmentsItemBlob : UserMessageDataAttachmentsItem +{ + /// + [JsonIgnore] + public override string Type => "blob"; + + /// Base64-encoded content. + [JsonPropertyName("data")] + public required string Data { get; set; } + + /// MIME type of the inline data. + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } + + /// User-facing display name for the attachment. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } +} + +/// A user message attachment — a file, directory, code selection, blob, or GitHub reference. +/// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -2259,6 +2451,7 @@ public partial class UserMessageDataAttachmentsItemGithubReference : UserMessage [JsonDerivedType(typeof(UserMessageDataAttachmentsItemDirectory), "directory")] [JsonDerivedType(typeof(UserMessageDataAttachmentsItemSelection), "selection")] [JsonDerivedType(typeof(UserMessageDataAttachmentsItemGithubReference), "github_reference")] +[JsonDerivedType(typeof(UserMessageDataAttachmentsItemBlob), "blob")] public partial class UserMessageDataAttachmentsItem { /// The type discriminator. @@ -2267,7 +2460,8 @@ public partial class UserMessageDataAttachmentsItem } -/// Nested data type for AssistantMessageDataToolRequestsItem. +/// A tool invocation request from the assistant. +/// Nested data type for AssistantMessageDataToolRequestsItem. public partial class AssistantMessageDataToolRequestsItem { /// Unique identifier for this tool call. @@ -2287,9 +2481,20 @@ public partial class AssistantMessageDataToolRequestsItem [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("type")] public AssistantMessageDataToolRequestsItemType? Type { get; set; } + + /// Human-readable display title for the tool. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolTitle")] + public string? ToolTitle { get; set; } + + /// Resolved intention summary describing what this specific call does. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("intentionSummary")] + public string? IntentionSummary { get; set; } } -/// Nested data type for AssistantUsageDataCopilotUsageTokenDetailsItem. +/// Token usage detail for a single billing category. +/// Nested data type for AssistantUsageDataCopilotUsageTokenDetailsItem. public partial class AssistantUsageDataCopilotUsageTokenDetailsItem { /// Number of tokens in this billing batch. @@ -2322,7 +2527,8 @@ public partial class AssistantUsageDataCopilotUsage public required double TotalNanoAiu { get; set; } } -/// The text variant of . +/// Plain text content block. +/// The text variant of . public partial class ToolExecutionCompleteDataResultContentsItemText : ToolExecutionCompleteDataResultContentsItem { /// @@ -2334,7 +2540,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemText : ToolExecu public required string Text { get; set; } } -/// The terminal variant of . +/// Terminal/shell output content block with optional exit code and working directory. +/// The terminal variant of . public partial class ToolExecutionCompleteDataResultContentsItemTerminal : ToolExecutionCompleteDataResultContentsItem { /// @@ -2356,7 +2563,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemTerminal : ToolE public string? Cwd { get; set; } } -/// The image variant of . +/// Image content block with base64-encoded data. +/// The image variant of . public partial class ToolExecutionCompleteDataResultContentsItemImage : ToolExecutionCompleteDataResultContentsItem { /// @@ -2372,7 +2580,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemImage : ToolExec public required string MimeType { get; set; } } -/// The audio variant of . +/// Audio content block with base64-encoded data. +/// The audio variant of . public partial class ToolExecutionCompleteDataResultContentsItemAudio : ToolExecutionCompleteDataResultContentsItem { /// @@ -2388,7 +2597,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemAudio : ToolExec public required string MimeType { get; set; } } -/// Nested data type for ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem. +/// Icon image for a resource. +/// Nested data type for ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem. public partial class ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItem { /// URL or path to the icon image. @@ -2411,7 +2621,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemResourceLinkIcon public ToolExecutionCompleteDataResultContentsItemResourceLinkIconsItemTheme? Theme { get; set; } } -/// The resource_link variant of . +/// Resource link content block referencing an external resource. +/// The resource_link variant of . public partial class ToolExecutionCompleteDataResultContentsItemResourceLink : ToolExecutionCompleteDataResultContentsItem { /// @@ -2452,7 +2663,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemResourceLink : T public double? Size { get; set; } } -/// The resource variant of . +/// Embedded resource content block with inline text or binary data. +/// The resource variant of . public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolExecutionCompleteDataResultContentsItem { /// @@ -2464,7 +2676,8 @@ public partial class ToolExecutionCompleteDataResultContentsItemResource : ToolE public required object Resource { get; set; } } -/// Polymorphic base type discriminated by type. +/// A content block within a tool result, which may be text, terminal output, image, audio, or a resource. +/// Polymorphic base type discriminated by type. [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] @@ -2649,7 +2862,8 @@ public partial class PermissionRequestShellPossibleUrlsItem public required string Url { get; set; } } -/// The shell variant of . +/// Shell command permission request. +/// The shell variant of . public partial class PermissionRequestShell : PermissionRequest { /// @@ -2695,7 +2909,8 @@ public partial class PermissionRequestShell : PermissionRequest public string? Warning { get; set; } } -/// The write variant of . +/// File write permission request. +/// The write variant of . public partial class PermissionRequestWrite : PermissionRequest { /// @@ -2725,7 +2940,8 @@ public partial class PermissionRequestWrite : PermissionRequest public string? NewFileContents { get; set; } } -/// The read variant of . +/// File or directory read permission request. +/// The read variant of . public partial class PermissionRequestRead : PermissionRequest { /// @@ -2746,7 +2962,8 @@ public partial class PermissionRequestRead : PermissionRequest public required string Path { get; set; } } -/// The mcp variant of . +/// MCP tool invocation permission request. +/// The mcp variant of . public partial class PermissionRequestMcp : PermissionRequest { /// @@ -2780,7 +2997,8 @@ public partial class PermissionRequestMcp : PermissionRequest public required bool ReadOnly { get; set; } } -/// The url variant of . +/// URL access permission request. +/// The url variant of . public partial class PermissionRequestUrl : PermissionRequest { /// @@ -2801,7 +3019,8 @@ public partial class PermissionRequestUrl : PermissionRequest public required string Url { get; set; } } -/// The memory variant of . +/// Memory storage permission request. +/// The memory variant of . public partial class PermissionRequestMemory : PermissionRequest { /// @@ -2826,7 +3045,8 @@ public partial class PermissionRequestMemory : PermissionRequest public required string Citations { get; set; } } -/// The custom-tool variant of . +/// Custom tool invocation permission request. +/// The custom-tool variant of . public partial class PermissionRequestCustomTool : PermissionRequest { /// @@ -2852,6 +3072,34 @@ public partial class PermissionRequestCustomTool : PermissionRequest public object? Args { get; set; } } +/// Hook confirmation permission request. +/// The hook variant of . +public partial class PermissionRequestHook : PermissionRequest +{ + /// + [JsonIgnore] + public override string Kind => "hook"; + + /// Tool call ID that triggered this permission request. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + /// Name of the tool the hook is gating. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } + + /// Arguments of the tool call being gated. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolArgs")] + public object? ToolArgs { get; set; } + + /// Optional message from the hook explaining why confirmation is needed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hookMessage")] + public string? HookMessage { get; set; } +} + /// Details of the permission being requested. /// Polymorphic base type discriminated by kind. [JsonPolymorphic( @@ -2864,6 +3112,7 @@ public partial class PermissionRequestCustomTool : PermissionRequest [JsonDerivedType(typeof(PermissionRequestUrl), "url")] [JsonDerivedType(typeof(PermissionRequestMemory), "memory")] [JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")] +[JsonDerivedType(typeof(PermissionRequestHook), "hook")] public partial class PermissionRequest { /// The type discriminator. @@ -2885,7 +3134,7 @@ public partial class PermissionCompletedDataResult /// Nested data type for ElicitationRequestedDataRequestedSchema. public partial class ElicitationRequestedDataRequestedSchema { - /// Gets or sets the type value. + /// Schema type indicator (always 'object'). [JsonPropertyName("type")] public required string Type { get; set; } @@ -2899,6 +3148,18 @@ public partial class ElicitationRequestedDataRequestedSchema public string[]? Required { get; set; } } +/// Hosting platform type of the repository (github or ado). +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionStartDataContextHostType +{ + /// The github variant. + [JsonStringEnumMemberName("github")] + Github, + /// The ado variant. + [JsonStringEnumMemberName("ado")] + Ado, +} + /// The type of operation performed on the plan file. [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionPlanChangedDataOperation @@ -2965,6 +3226,42 @@ public enum UserMessageDataAttachmentsItemGithubReferenceReferenceType Discussion, } +/// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command"). +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum UserMessageDataSource +{ + /// The user variant. + [JsonStringEnumMemberName("user")] + User, + /// The autopilot variant. + [JsonStringEnumMemberName("autopilot")] + Autopilot, + /// The skill variant. + [JsonStringEnumMemberName("skill")] + Skill, + /// The system variant. + [JsonStringEnumMemberName("system")] + System, + /// The command variant. + [JsonStringEnumMemberName("command")] + Command, + /// The immediate-prompt variant. + [JsonStringEnumMemberName("immediate-prompt")] + ImmediatePrompt, + /// The jit-instruction variant. + [JsonStringEnumMemberName("jit-instruction")] + JitInstruction, + /// The snippy-blocking variant. + [JsonStringEnumMemberName("snippy-blocking")] + SnippyBlocking, + /// The thinking-exhausted-continuation variant. + [JsonStringEnumMemberName("thinking-exhausted-continuation")] + ThinkingExhaustedContinuation, + /// The other variant. + [JsonStringEnumMemberName("other")] + Other, +} + /// The agent mode that was active when this message was sent. [JsonConverter(typeof(JsonStringEnumConverter))] public enum UserMessageDataAgentMode @@ -3109,6 +3406,7 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(PermissionCompletedEvent))] [JsonSerializable(typeof(PermissionRequest))] [JsonSerializable(typeof(PermissionRequestCustomTool))] +[JsonSerializable(typeof(PermissionRequestHook))] [JsonSerializable(typeof(PermissionRequestMcp))] [JsonSerializable(typeof(PermissionRequestMemory))] [JsonSerializable(typeof(PermissionRequestRead))] @@ -3119,6 +3417,8 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(PermissionRequestWrite))] [JsonSerializable(typeof(PermissionRequestedData))] [JsonSerializable(typeof(PermissionRequestedEvent))] +[JsonSerializable(typeof(SessionBackgroundTasksChangedData))] +[JsonSerializable(typeof(SessionBackgroundTasksChangedEvent))] [JsonSerializable(typeof(SessionCompactionCompleteData))] [JsonSerializable(typeof(SessionCompactionCompleteDataCompactionTokensUsed))] [JsonSerializable(typeof(SessionCompactionCompleteEvent))] @@ -3160,6 +3460,8 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SessionTaskCompleteEvent))] [JsonSerializable(typeof(SessionTitleChangedData))] [JsonSerializable(typeof(SessionTitleChangedEvent))] +[JsonSerializable(typeof(SessionToolsUpdatedData))] +[JsonSerializable(typeof(SessionToolsUpdatedEvent))] [JsonSerializable(typeof(SessionTruncationData))] [JsonSerializable(typeof(SessionTruncationEvent))] [JsonSerializable(typeof(SessionUsageInfoData))] @@ -3215,8 +3517,8 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(UserInputRequestedEvent))] [JsonSerializable(typeof(UserMessageData))] [JsonSerializable(typeof(UserMessageDataAttachmentsItem))] +[JsonSerializable(typeof(UserMessageDataAttachmentsItemBlob))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectory))] -[JsonSerializable(typeof(UserMessageDataAttachmentsItemDirectoryLineRange))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemFile))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemFileLineRange))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemGithubReference))] diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 72e428d164..55eea011ea 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -26,14 +26,130 @@ func (r *SessionEvent) Marshal() ([]byte, error) { } type SessionEvent struct { + // Session initialization metadata including context and configuration + // + // Session resume metadata including current context and event count + // + // Error details for timeline display including message and optional diagnostic information + // // Payload indicating the agent is idle; includes any background tasks still in flight // + // Session title change payload containing the new display title + // + // Informational message for timeline display with categorization + // + // Warning message for timeline display with categorization + // + // Model change details including previous and new model identifiers + // + // Agent mode change details including previous and new modes + // + // Plan file operation details indicating what changed + // + // Workspace file change details including path and operation type + // + // Session handoff metadata including source, context, and repository information + // + // Conversation truncation statistics including token counts and removed content metrics + // + // Session rewind details including target event and count of removed events + // + // Session termination metrics including usage statistics, code changes, and shutdown + // reason + // + // Updated working directory and git context after the change + // + // Current context window usage statistics including token and message counts + // // Empty payload; the event signals that LLM-powered conversation compaction has begun // + // Conversation compaction results including success status, metrics, and optional error + // details + // + // Task completion notification with optional summary from the agent + // + // User message content with optional attachments, source information, and interaction + // metadata + // // Empty payload; the event signals that the pending message queue has changed // + // Turn initialization metadata including identifier and interaction tracking + // + // Agent intent description for current activity or plan + // + // Assistant reasoning content for timeline display with complete thinking text + // + // Streaming reasoning delta for incremental extended thinking updates + // + // Streaming response progress with cumulative byte count + // + // Assistant response containing text content, optional tool requests, and interaction + // metadata + // + // Streaming assistant message delta for incremental response updates + // + // Turn completion metadata including the turn identifier + // + // LLM API call usage metrics including tokens, costs, quotas, and billing information + // + // Turn abort information including the reason for termination + // + // User-initiated tool invocation request with tool name and arguments + // + // Tool execution startup details including MCP server information when applicable + // + // Streaming tool execution output for incremental result display + // + // Tool execution progress notification with status message + // + // Tool execution completion results including success status, detailed output, and error + // information + // + // Skill invocation details including content, allowed tools, and plugin metadata + // + // Sub-agent startup details including parent tool call and agent information + // + // Sub-agent completion details for successful execution + // + // Sub-agent failure details including error message and agent information + // + // Custom agent selection details including name and available tools + // // Empty payload; the event signals that the custom agent was deselected, returning to the // default agent + // + // Hook invocation start details including type and input data + // + // Hook invocation completion details including output, success status, and error + // information + // + // System or developer message content with role and optional template metadata + // + // System-generated notification for runtime events like background task completion + // + // Permission request notification requiring client approval with request details + // + // Permission request completion notification signaling UI dismissal + // + // User input request notification with question and optional predefined choices + // + // User input request completion notification signaling UI dismissal + // + // Structured form elicitation request with JSON schema definition for form fields + // + // Elicitation request completion notification signaling UI dismissal + // + // External tool invocation request for client-side tool execution + // + // External tool completion notification signaling UI dismissal + // + // Queued slash command dispatch request for client execution + // + // Queued command completion notification signaling UI dismissal + // + // Plan approval request with plan content and available user actions + // + // Plan mode exit completion notification signaling UI dismissal Data Data `json:"data"` // When true, the event is transient and not persisted to the session event log on disk Ephemeral *bool `json:"ephemeral,omitempty"` @@ -47,15 +163,134 @@ type SessionEvent struct { Type SessionEventType `json:"type"` } +// Session initialization metadata including context and configuration +// +// # Session resume metadata including current context and event count +// +// # Error details for timeline display including message and optional diagnostic information +// // Payload indicating the agent is idle; includes any background tasks still in flight // +// # Session title change payload containing the new display title +// +// # Informational message for timeline display with categorization +// +// # Warning message for timeline display with categorization +// +// # Model change details including previous and new model identifiers +// +// # Agent mode change details including previous and new modes +// +// # Plan file operation details indicating what changed +// +// # Workspace file change details including path and operation type +// +// # Session handoff metadata including source, context, and repository information +// +// # Conversation truncation statistics including token counts and removed content metrics +// +// # Session rewind details including target event and count of removed events +// +// Session termination metrics including usage statistics, code changes, and shutdown +// reason +// +// # Updated working directory and git context after the change +// +// # Current context window usage statistics including token and message counts +// // Empty payload; the event signals that LLM-powered conversation compaction has begun // +// Conversation compaction results including success status, metrics, and optional error +// details +// +// # Task completion notification with optional summary from the agent +// +// User message content with optional attachments, source information, and interaction +// metadata +// // Empty payload; the event signals that the pending message queue has changed // +// # Turn initialization metadata including identifier and interaction tracking +// +// # Agent intent description for current activity or plan +// +// # Assistant reasoning content for timeline display with complete thinking text +// +// # Streaming reasoning delta for incremental extended thinking updates +// +// # Streaming response progress with cumulative byte count +// +// Assistant response containing text content, optional tool requests, and interaction +// metadata +// +// # Streaming assistant message delta for incremental response updates +// +// # Turn completion metadata including the turn identifier +// +// # LLM API call usage metrics including tokens, costs, quotas, and billing information +// +// # Turn abort information including the reason for termination +// +// # User-initiated tool invocation request with tool name and arguments +// +// # Tool execution startup details including MCP server information when applicable +// +// # Streaming tool execution output for incremental result display +// +// # Tool execution progress notification with status message +// +// Tool execution completion results including success status, detailed output, and error +// information +// +// # Skill invocation details including content, allowed tools, and plugin metadata +// +// # Sub-agent startup details including parent tool call and agent information +// +// # Sub-agent completion details for successful execution +// +// # Sub-agent failure details including error message and agent information +// +// # Custom agent selection details including name and available tools +// // Empty payload; the event signals that the custom agent was deselected, returning to the // default agent +// +// # Hook invocation start details including type and input data +// +// Hook invocation completion details including output, success status, and error +// information +// +// # System or developer message content with role and optional template metadata +// +// # System-generated notification for runtime events like background task completion +// +// # Permission request notification requiring client approval with request details +// +// # Permission request completion notification signaling UI dismissal +// +// # User input request notification with question and optional predefined choices +// +// # User input request completion notification signaling UI dismissal +// +// # Structured form elicitation request with JSON schema definition for form fields +// +// # Elicitation request completion notification signaling UI dismissal +// +// # External tool invocation request for client-side tool execution +// +// # External tool completion notification signaling UI dismissal +// +// # Queued slash command dispatch request for client execution +// +// # Queued command completion notification signaling UI dismissal +// +// # Plan approval request with plan content and available user actions +// +// Plan mode exit completion notification signaling UI dismissal type Data struct { + // Whether the session was already in use by another client at start time + // + // Whether the session was already in use by another client at resume time AlreadyInUse *bool `json:"alreadyInUse,omitempty"` // Working directory and git context at session start // @@ -67,7 +302,14 @@ type Data struct { CopilotVersion *string `json:"copilotVersion,omitempty"` // Identifier of the software producing the events (e.g., "copilot-agent") Producer *string `json:"producer,omitempty"` + // Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", + // "xhigh") + // + // Reasoning effort level after the model change, if applicable + ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Model selected at session creation time, if any + // + // Model currently selected at resume time SelectedModel *string `json:"selectedModel,omitempty"` // Unique identifier for the session // @@ -114,6 +356,8 @@ type Data struct { NewModel *string `json:"newModel,omitempty"` // Model that was previously selected, if any PreviousModel *string `json:"previousModel,omitempty"` + // Reasoning effort level before the model change, if applicable + PreviousReasoningEffort *string `json:"previousReasoningEffort,omitempty"` // Agent mode after the change (e.g., "interactive", "plan", "autopilot") NewMode *string `json:"newMode,omitempty"` // Agent mode before the change (e.g., "interactive", "plan", "autopilot") @@ -132,7 +376,8 @@ type Data struct { RemoteSessionID *string `json:"remoteSessionId,omitempty"` // Repository context for the handed-off session // - // Repository identifier in "owner/name" format, derived from the git remote URL + // Repository identifier derived from the git remote URL ("owner/name" for GitHub, + // "org/project/repo" for Azure DevOps) Repository *RepositoryUnion `json:"repository"` // Origin type of the session being handed off SourceType *SourceType `json:"sourceType,omitempty"` @@ -178,12 +423,18 @@ type Data struct { TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"` // Total number of premium API requests used during the session TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` + // Base commit of current git branch at session start time + BaseCommit *string `json:"baseCommit,omitempty"` // Current git branch name Branch *string `json:"branch,omitempty"` // Current working directory path Cwd *string `json:"cwd,omitempty"` // Root directory of the git repository, resolved via git rev-parse GitRoot *string `json:"gitRoot,omitempty"` + // Head commit of current git branch at session start time + HeadCommit *string `json:"headCommit,omitempty"` + // Hosting platform type of the repository (github or ado) + HostType *HostType `json:"hostType,omitempty"` // Current number of tokens in the context window CurrentTokens *float64 `json:"currentTokens,omitempty"` // Current number of messages in the conversation @@ -279,9 +530,9 @@ type Data struct { // // CAPI interaction ID for correlating this tool execution with upstream telemetry InteractionID *string `json:"interactionId,omitempty"` - // Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected - // messages that should be hidden from the user) - Source *string `json:"source,omitempty"` + // Origin of this message, used for timeline filtering and telemetry (e.g., "user", + // "autopilot", "skill", or "command") + Source *Source `json:"source,omitempty"` // Transformed version of the message sent to the model, with XML wrapping, timestamps, and // other augmentations for prompt caching TransformedContent *string `json:"transformedContent,omitempty"` @@ -443,6 +694,10 @@ type Data struct { Mode *Mode `json:"mode,omitempty"` // JSON Schema describing the form fields to present to the user RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"` + // W3C Trace Context traceparent header for the execute_tool span + Traceparent *string `json:"traceparent,omitempty"` + // W3C Trace Context tracestate header for the execute_tool span + Tracestate *string `json:"tracestate,omitempty"` // The slash command text to be executed (e.g., /help, /clear) Command *string `json:"command,omitempty"` // Available actions the user can take (e.g., approve, edit, reject) @@ -453,6 +708,17 @@ type Data struct { RecommendedAction *string `json:"recommendedAction,omitempty"` } +// A user message attachment — a file, directory, code selection, blob, or GitHub reference +// +// # File attachment +// +// # Directory attachment +// +// # Code selection attachment from an editor +// +// # GitHub issue, pull request, or discussion reference +// +// Blob attachment with inline base64-encoded data type Attachment struct { // User-facing display name for the attachment // @@ -460,7 +726,9 @@ type Attachment struct { DisplayName *string `json:"displayName,omitempty"` // Optional line range to scope the attachment to a specific section of the file LineRange *LineRange `json:"lineRange,omitempty"` - // Absolute file or directory path + // Absolute file path + // + // Absolute directory path Path *string `json:"path,omitempty"` // Attachment type discriminator Type AttachmentType `json:"type"` @@ -480,6 +748,10 @@ type Attachment struct { Title *string `json:"title,omitempty"` // URL to the referenced item on GitHub URL *string `json:"url,omitempty"` + // Base64-encoded content + Data *string `json:"data,omitempty"` + // MIME type of the inline data + MIMEType *string `json:"mimeType,omitempty"` } // Optional line range to scope the attachment to a specific section of the file @@ -492,10 +764,13 @@ type LineRange struct { // Position range of the selection within the file type SelectionClass struct { - End End `json:"end"` + // End position of the selection + End End `json:"end"` + // Start position of the selection Start Start `json:"start"` } +// End position of the selection type End struct { // End character offset within the line (0-based) Character float64 `json:"character"` @@ -503,6 +778,7 @@ type End struct { Line float64 `json:"line"` } +// Start position of the selection type Start struct { // Start character offset within the line (0-based) Character float64 `json:"character"` @@ -518,6 +794,7 @@ type BackgroundTasks struct { Shells []Shell `json:"shells"` } +// A background agent task type Agent struct { // Unique identifier of the background agent AgentID string `json:"agentId"` @@ -527,6 +804,7 @@ type Agent struct { Description *string `json:"description,omitempty"` } +// A background shell command type Shell struct { // Human-readable description of the shell command Description *string `json:"description,omitempty"` @@ -558,13 +836,20 @@ type CompactionTokensUsed struct { // // Updated working directory and git context at resume time type ContextClass struct { + // Base commit of current git branch at session start time + BaseCommit *string `json:"baseCommit,omitempty"` // Current git branch name Branch *string `json:"branch,omitempty"` // Current working directory path Cwd string `json:"cwd"` // Root directory of the git repository, resolved via git rev-parse GitRoot *string `json:"gitRoot,omitempty"` - // Repository identifier in "owner/name" format, derived from the git remote URL + // Head commit of current git branch at session start time + HeadCommit *string `json:"headCommit,omitempty"` + // Hosting platform type of the repository (github or ado) + HostType *HostType `json:"hostType,omitempty"` + // Repository identifier derived from the git remote URL ("owner/name" for GitHub, + // "org/project/repo" for Azure DevOps) Repository *string `json:"repository,omitempty"` } @@ -576,6 +861,7 @@ type CopilotUsage struct { TotalNanoAiu float64 `json:"totalNanoAiu"` } +// Token usage detail for a single billing category type TokenDetail struct { // Number of tokens in this billing batch BatchSize float64 `json:"batchSize"` @@ -658,11 +944,27 @@ type Usage struct { } // Details of the permission being requested +// +// # Shell command permission request +// +// # File write permission request +// +// # File or directory read permission request +// +// # MCP tool invocation permission request +// +// # URL access permission request +// +// # Memory storage permission request +// +// # Custom tool invocation permission request +// +// Hook confirmation permission request type PermissionRequest struct { // Whether the UI can offer session-wide approval for this command pattern CanOfferSessionApproval *bool `json:"canOfferSessionApproval,omitempty"` // Parsed command identifiers found in the command text - Commands []Command `json:"commands,omitempty"` + Commands []CommandElement `json:"commands,omitempty"` // The complete shell command text to be executed FullCommandText *string `json:"fullCommandText,omitempty"` // Whether the command includes a file write redirection (e.g., > or >>) @@ -704,6 +1006,8 @@ type PermissionRequest struct { // Internal name of the MCP tool // // Name of the custom tool + // + // Name of the tool the hook is gating ToolName *string `json:"toolName,omitempty"` // Human-readable title of the MCP tool ToolTitle *string `json:"toolTitle,omitempty"` @@ -717,9 +1021,13 @@ type PermissionRequest struct { Subject *string `json:"subject,omitempty"` // Description of what the custom tool does ToolDescription *string `json:"toolDescription,omitempty"` + // Optional message from the hook explaining why confirmation is needed + HookMessage *string `json:"hookMessage,omitempty"` + // Arguments of the tool call being gated + ToolArgs interface{} `json:"toolArgs"` } -type Command struct { +type CommandElement struct { // Command identifier (e.g., executable name) Identifier string `json:"identifier"` // Whether this command is read-only (no side effects) @@ -765,8 +1073,9 @@ type RequestedSchema struct { // Form field definitions, keyed by field name Properties map[string]interface{} `json:"properties"` // List of required field names - Required []string `json:"required,omitempty"` - Type RequestedSchemaType `json:"type"` + Required []string `json:"required,omitempty"` + // Schema type indicator (always 'object') + Type RequestedSchemaType `json:"type"` } // Tool execution result on success @@ -786,6 +1095,20 @@ type Result struct { Kind *ResultKind `json:"kind,omitempty"` } +// A content block within a tool result, which may be text, terminal output, image, audio, +// or a resource +// +// # Plain text content block +// +// Terminal/shell output content block with optional exit code and working directory +// +// # Image content block with base64-encoded data +// +// # Audio content block with base64-encoded data +// +// # Resource link content block referencing an external resource +// +// Embedded resource content block with inline text or binary data type Content struct { // The text content // @@ -823,6 +1146,7 @@ type Content struct { Resource *ResourceClass `json:"resource,omitempty"` } +// Icon image for a resource type Icon struct { // MIME type of the icon image MIMEType *string `json:"mimeType,omitempty"` @@ -848,13 +1172,18 @@ type ResourceClass struct { Blob *string `json:"blob,omitempty"` } +// A tool invocation request from the assistant type ToolRequest struct { // Arguments to pass to the tool, format depends on the tool Arguments interface{} `json:"arguments"` + // Resolved intention summary describing what this specific call does + IntentionSummary *string `json:"intentionSummary"` // Name of the tool being invoked Name string `json:"name"` // Unique identifier for this tool call ToolCallID string `json:"toolCallId"` + // Human-readable display title for the tool + ToolTitle *string `json:"toolTitle,omitempty"` // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool // calls. Defaults to "function" when absent. Type *ToolRequestType `json:"type,omitempty"` @@ -864,10 +1193,10 @@ type ToolRequest struct { type AgentMode string const ( - AgentModeShell AgentMode = "shell" - Autopilot AgentMode = "autopilot" - Interactive AgentMode = "interactive" - Plan AgentMode = "plan" + AgentModeAutopilot AgentMode = "autopilot" + AgentModeShell AgentMode = "shell" + Interactive AgentMode = "interactive" + Plan AgentMode = "plan" ) // Type of GitHub reference @@ -882,12 +1211,21 @@ const ( type AttachmentType string const ( + Blob AttachmentType = "blob" Directory AttachmentType = "directory" File AttachmentType = "file" GithubReference AttachmentType = "github_reference" Selection AttachmentType = "selection" ) +// Hosting platform type of the repository (github or ado) +type HostType string + +const ( + ADO HostType = "ado" + Github HostType = "github" +) + // Whether the agent completed successfully or failed type Status string @@ -925,6 +1263,7 @@ type PermissionRequestKind string const ( CustomTool PermissionRequestKind = "custom-tool" + Hook PermissionRequestKind = "hook" KindShell PermissionRequestKind = "shell" MCP PermissionRequestKind = "mcp" Memory PermissionRequestKind = "memory" @@ -973,8 +1312,8 @@ const ( type Role string const ( - Developer Role = "developer" - System Role = "system" + Developer Role = "developer" + RoleSystem Role = "system" ) // Whether the session ended normally ("routine") or due to a crash/fatal error ("error") @@ -985,6 +1324,23 @@ const ( Routine ShutdownType = "routine" ) +// Origin of this message, used for timeline filtering and telemetry (e.g., "user", +// "autopilot", "skill", or "command") +type Source string + +const ( + Command Source = "command" + ImmediatePrompt Source = "immediate-prompt" + JITInstruction Source = "jit-instruction" + Other Source = "other" + Skill Source = "skill" + SnippyBlocking Source = "snippy-blocking" + SourceAutopilot Source = "autopilot" + SourceSystem Source = "system" + ThinkingExhaustedContinuation Source = "thinking-exhausted-continuation" + User Source = "user" +) + // Origin type of the session being handed off type SourceType string @@ -1005,65 +1361,67 @@ const ( type SessionEventType string const ( - Abort SessionEventType = "abort" - AssistantIntent SessionEventType = "assistant.intent" - AssistantMessage SessionEventType = "assistant.message" - AssistantMessageDelta SessionEventType = "assistant.message_delta" - AssistantReasoning SessionEventType = "assistant.reasoning" - AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" - AssistantStreamingDelta SessionEventType = "assistant.streaming_delta" - AssistantTurnEnd SessionEventType = "assistant.turn_end" - AssistantTurnStart SessionEventType = "assistant.turn_start" - AssistantUsage SessionEventType = "assistant.usage" - CommandCompleted SessionEventType = "command.completed" - CommandQueued SessionEventType = "command.queued" - ElicitationCompleted SessionEventType = "elicitation.completed" - ElicitationRequested SessionEventType = "elicitation.requested" - ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" - ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" - ExternalToolCompleted SessionEventType = "external_tool.completed" - ExternalToolRequested SessionEventType = "external_tool.requested" - HookEnd SessionEventType = "hook.end" - HookStart SessionEventType = "hook.start" - PendingMessagesModified SessionEventType = "pending_messages.modified" - PermissionCompleted SessionEventType = "permission.completed" - PermissionRequested SessionEventType = "permission.requested" - SessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionCompactionStart SessionEventType = "session.compaction_start" - SessionContextChanged SessionEventType = "session.context_changed" - SessionError SessionEventType = "session.error" - SessionHandoff SessionEventType = "session.handoff" - SessionIdle SessionEventType = "session.idle" - SessionInfo SessionEventType = "session.info" - SessionModeChanged SessionEventType = "session.mode_changed" - SessionModelChange SessionEventType = "session.model_change" - SessionPlanChanged SessionEventType = "session.plan_changed" - SessionResume SessionEventType = "session.resume" - SessionShutdown SessionEventType = "session.shutdown" - SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" - SessionStart SessionEventType = "session.start" - SessionTaskComplete SessionEventType = "session.task_complete" - SessionTitleChanged SessionEventType = "session.title_changed" - SessionTruncation SessionEventType = "session.truncation" - SessionUsageInfo SessionEventType = "session.usage_info" - SessionWarning SessionEventType = "session.warning" - SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" - SkillInvoked SessionEventType = "skill.invoked" - SubagentCompleted SessionEventType = "subagent.completed" - SubagentDeselected SessionEventType = "subagent.deselected" - SubagentFailed SessionEventType = "subagent.failed" - SubagentSelected SessionEventType = "subagent.selected" - SubagentStarted SessionEventType = "subagent.started" - SystemMessage SessionEventType = "system.message" - SystemNotification SessionEventType = "system.notification" - ToolExecutionComplete SessionEventType = "tool.execution_complete" - ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" - ToolExecutionProgress SessionEventType = "tool.execution_progress" - ToolExecutionStart SessionEventType = "tool.execution_start" - ToolUserRequested SessionEventType = "tool.user_requested" - UserInputCompleted SessionEventType = "user_input.completed" - UserInputRequested SessionEventType = "user_input.requested" - UserMessage SessionEventType = "user.message" + Abort SessionEventType = "abort" + AssistantIntent SessionEventType = "assistant.intent" + AssistantMessage SessionEventType = "assistant.message" + AssistantMessageDelta SessionEventType = "assistant.message_delta" + AssistantReasoning SessionEventType = "assistant.reasoning" + AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" + AssistantStreamingDelta SessionEventType = "assistant.streaming_delta" + AssistantTurnEnd SessionEventType = "assistant.turn_end" + AssistantTurnStart SessionEventType = "assistant.turn_start" + AssistantUsage SessionEventType = "assistant.usage" + CommandCompleted SessionEventType = "command.completed" + CommandQueued SessionEventType = "command.queued" + ElicitationCompleted SessionEventType = "elicitation.completed" + ElicitationRequested SessionEventType = "elicitation.requested" + ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" + ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" + ExternalToolCompleted SessionEventType = "external_tool.completed" + ExternalToolRequested SessionEventType = "external_tool.requested" + HookEnd SessionEventType = "hook.end" + HookStart SessionEventType = "hook.start" + PendingMessagesModified SessionEventType = "pending_messages.modified" + PermissionCompleted SessionEventType = "permission.completed" + PermissionRequested SessionEventType = "permission.requested" + SessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" + SessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionCompactionStart SessionEventType = "session.compaction_start" + SessionContextChanged SessionEventType = "session.context_changed" + SessionError SessionEventType = "session.error" + SessionHandoff SessionEventType = "session.handoff" + SessionIdle SessionEventType = "session.idle" + SessionInfo SessionEventType = "session.info" + SessionModeChanged SessionEventType = "session.mode_changed" + SessionModelChange SessionEventType = "session.model_change" + SessionPlanChanged SessionEventType = "session.plan_changed" + SessionResume SessionEventType = "session.resume" + SessionShutdown SessionEventType = "session.shutdown" + SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" + SessionStart SessionEventType = "session.start" + SessionTaskComplete SessionEventType = "session.task_complete" + SessionTitleChanged SessionEventType = "session.title_changed" + SessionToolsUpdated SessionEventType = "session.tools_updated" + SessionTruncation SessionEventType = "session.truncation" + SessionUsageInfo SessionEventType = "session.usage_info" + SessionWarning SessionEventType = "session.warning" + SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" + SkillInvoked SessionEventType = "skill.invoked" + SubagentCompleted SessionEventType = "subagent.completed" + SubagentDeselected SessionEventType = "subagent.deselected" + SubagentFailed SessionEventType = "subagent.failed" + SubagentSelected SessionEventType = "subagent.selected" + SubagentStarted SessionEventType = "subagent.started" + SystemMessage SessionEventType = "system.message" + SystemNotification SessionEventType = "system.notification" + ToolExecutionComplete SessionEventType = "tool.execution_complete" + ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" + ToolExecutionProgress SessionEventType = "tool.execution_progress" + ToolExecutionStart SessionEventType = "tool.execution_start" + ToolUserRequested SessionEventType = "tool.user_requested" + UserInputCompleted SessionEventType = "user_input.completed" + UserInputRequested SessionEventType = "user_input.requested" + UserMessage SessionEventType = "user.message" ) type ContextUnion struct { diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 0e4b96e4f3..ffe87455e4 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -48,30 +48,41 @@ type Model struct { // Billing information type Billing struct { + // Billing cost multiplier relative to the base rate Multiplier float64 `json:"multiplier"` } // Model capabilities and limits type Capabilities struct { - Limits Limits `json:"limits"` + // Token limits for prompts, outputs, and context window + Limits Limits `json:"limits"` + // Feature flags indicating what the model supports Supports Supports `json:"supports"` } +// Token limits for prompts, outputs, and context window type Limits struct { - MaxContextWindowTokens float64 `json:"max_context_window_tokens"` - MaxOutputTokens *float64 `json:"max_output_tokens,omitempty"` - MaxPromptTokens *float64 `json:"max_prompt_tokens,omitempty"` + // Maximum total context window size in tokens + MaxContextWindowTokens float64 `json:"max_context_window_tokens"` + // Maximum number of output/completion tokens + MaxOutputTokens *float64 `json:"max_output_tokens,omitempty"` + // Maximum number of prompt/input tokens + MaxPromptTokens *float64 `json:"max_prompt_tokens,omitempty"` } +// Feature flags indicating what the model supports type Supports struct { // Whether this model supports reasoning effort configuration ReasoningEffort *bool `json:"reasoningEffort,omitempty"` - Vision *bool `json:"vision,omitempty"` + // Whether this model supports vision/image input + Vision *bool `json:"vision,omitempty"` } // Policy state (if applicable) type Policy struct { + // Current policy state for this model State string `json:"state"` + // Usage terms or conditions for this model Terms string `json:"terms"` } @@ -121,16 +132,20 @@ type QuotaSnapshot struct { } type SessionModelGetCurrentResult struct { + // Currently active model identifier ModelID *string `json:"modelId,omitempty"` } type SessionModelSwitchToResult struct { + // Currently active model identifier after the switch ModelID *string `json:"modelId,omitempty"` } type SessionModelSwitchToParams struct { - ModelID string `json:"modelId"` - ReasoningEffort *ReasoningEffort `json:"reasoningEffort,omitempty"` + // Model identifier to switch to + ModelID string `json:"modelId"` + // Reasoning effort level to use for the model + ReasoningEffort *string `json:"reasoningEffort,omitempty"` } type SessionModeGetResult struct { @@ -264,6 +279,7 @@ type SessionCompactionCompactResult struct { } type SessionToolsHandlePendingToolCallResult struct { + // Whether the tool call result was handled successfully Success bool `json:"success"` } @@ -281,6 +297,7 @@ type ResultResult struct { } type SessionPermissionsHandlePendingPermissionRequestResult struct { + // Whether the permission request was handled successfully Success bool `json:"success"` } @@ -312,14 +329,31 @@ type SessionLogParams struct { Message string `json:"message"` } -type ReasoningEffort string +type SessionShellExecResult struct { + // Unique identifier for tracking streamed output + ProcessID string `json:"processId"` +} -const ( - High ReasoningEffort = "high" - Low ReasoningEffort = "low" - Medium ReasoningEffort = "medium" - Xhigh ReasoningEffort = "xhigh" -) +type SessionShellExecParams struct { + // Shell command to execute + Command string `json:"command"` + // Working directory (defaults to session working directory) + Cwd *string `json:"cwd,omitempty"` + // Timeout in milliseconds (default: 30000) + Timeout *float64 `json:"timeout,omitempty"` +} + +type SessionShellKillResult struct { + // Whether the signal was sent successfully + Killed bool `json:"killed"` +} + +type SessionShellKillParams struct { + // Process identifier returned by shell.exec + ProcessID string `json:"processId"` + // Signal to send (default: SIGTERM) + Signal *Signal `json:"signal,omitempty"` +} // The current agent mode. // @@ -354,6 +388,15 @@ const ( Warning Level = "warning" ) +// Signal to send (default: SIGTERM) +type Signal string + +const ( + Sigint Signal = "SIGINT" + Sigkill Signal = "SIGKILL" + Sigterm Signal = "SIGTERM" +) + type ResultUnion struct { ResultResult *ResultResult String *string @@ -748,6 +791,52 @@ func (a *PermissionsRpcApi) HandlePendingPermissionRequest(ctx context.Context, return &result, nil } +type ShellRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *ShellRpcApi) Exec(ctx context.Context, params *SessionShellExecParams) (*SessionShellExecResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["command"] = params.Command + if params.Cwd != nil { + req["cwd"] = *params.Cwd + } + if params.Timeout != nil { + req["timeout"] = *params.Timeout + } + } + raw, err := a.client.Request("session.shell.exec", req) + if err != nil { + return nil, err + } + var result SessionShellExecResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ShellRpcApi) Kill(ctx context.Context, params *SessionShellKillParams) (*SessionShellKillResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["processId"] = params.ProcessID + if params.Signal != nil { + req["signal"] = *params.Signal + } + } + raw, err := a.client.Request("session.shell.kill", req) + if err != nil { + return nil, err + } + var result SessionShellKillResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // SessionRpc provides typed session-scoped RPC methods. type SessionRpc struct { client *jsonrpc2.Client @@ -761,6 +850,7 @@ type SessionRpc struct { Compaction *CompactionRpcApi Tools *ToolsRpcApi Permissions *PermissionsRpcApi + Shell *ShellRpcApi } func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*SessionLogResult, error) { @@ -796,5 +886,6 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { Compaction: &CompactionRpcApi{client: client, sessionID: sessionID}, Tools: &ToolsRpcApi{client: client, sessionID: sessionID}, Permissions: &PermissionsRpcApi{client: client, sessionID: sessionID}, + Shell: &ShellRpcApi{client: client, sessionID: sessionID}, } } diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index a07746bfdc..0952122f0e 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.3-0", + "@github/copilot": "^1.0.4", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.3-0.tgz", - "integrity": "sha512-wvd3FwQUgf4Bm3dwRBNXdjE60eGi+4cK0Shn9Ky8GSuusHtClIanTL65ft5HdOlZ1H+ieyWrrGgu7rO1Sip/yQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz", + "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.3-0", - "@github/copilot-darwin-x64": "1.0.3-0", - "@github/copilot-linux-arm64": "1.0.3-0", - "@github/copilot-linux-x64": "1.0.3-0", - "@github/copilot-win32-arm64": "1.0.3-0", - "@github/copilot-win32-x64": "1.0.3-0" + "@github/copilot-darwin-arm64": "1.0.4", + "@github/copilot-darwin-x64": "1.0.4", + "@github/copilot-linux-arm64": "1.0.4", + "@github/copilot-linux-x64": "1.0.4", + "@github/copilot-win32-arm64": "1.0.4", + "@github/copilot-win32-x64": "1.0.4" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.3-0.tgz", - "integrity": "sha512-9bpouod3i4S5TbO9zMb6e47O2l8tussndaQu8D2nD7dBVUO/p+k7r9N1agAZ9/h3zrIqWo+JpJ57iUYb8tbCSw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.3-0.tgz", - "integrity": "sha512-L4/OJLcnSnPIUIPaTZR6K7+mjXDPkHFNixioefJZQvJerOZdo9LTML6zkc2j21dWleSHiOVaLAfUdoLMyWzaVg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz", + "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.3-0.tgz", - "integrity": "sha512-3zGP9UuQAh7goXo7Ae2jm1SPpHWmNJw3iW6oEIhTocYm+xUecYdny7AbDAQs491fZcVGYea22Jqyynlcj1lH/g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz", + "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.3-0.tgz", - "integrity": "sha512-cdxGofsF7LHjw5mO0uvmsK4wl1QnW3cd2rhwc14XgWMXbenlgyBTmwamGbVdlYtZRIAYgKNQAo3PpZSsyPXw8A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz", + "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.3-0.tgz", - "integrity": "sha512-ZjUDdE7IOi6EeUEb8hJvRu5RqPrY5kuPzdqMAiIqwDervBdNJwy9AkCNtg0jJ2fPamoQgKSFcAX7QaUX4kMx3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz", + "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.3-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.3-0.tgz", - "integrity": "sha512-mNoeF4hwbxXxDtGZPWe78jEfAwdQbG1Zeyztme7Z19NjZF4bUI/iDaifKUfn+fMzGHZyykoaPl9mLrTSYr77Cw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz", + "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 4b4071270b..214ef34666 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -44,7 +44,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.3-0", + "@github/copilot": "^1.0.4", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 36f8b70390..4f93a271c6 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.421", + "@github/copilot": "^1.0.4", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index ec40bfa695..e5ba9ad4ca 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -44,16 +44,34 @@ export interface ModelsListResult { * Model capabilities and limits */ capabilities: { + /** + * Feature flags indicating what the model supports + */ supports: { + /** + * Whether this model supports vision/image input + */ vision?: boolean; /** * Whether this model supports reasoning effort configuration */ reasoningEffort?: boolean; }; + /** + * Token limits for prompts, outputs, and context window + */ limits: { + /** + * Maximum number of prompt/input tokens + */ max_prompt_tokens?: number; + /** + * Maximum number of output/completion tokens + */ max_output_tokens?: number; + /** + * Maximum total context window size in tokens + */ max_context_window_tokens: number; }; }; @@ -61,13 +79,22 @@ export interface ModelsListResult { * Policy state (if applicable) */ policy?: { + /** + * Current policy state for this model + */ state: string; + /** + * Usage terms or conditions for this model + */ terms: string; }; /** * Billing information */ billing?: { + /** + * Billing cost multiplier relative to the base rate + */ multiplier: number; }; /** @@ -153,6 +180,9 @@ export interface AccountGetQuotaResult { } export interface SessionModelGetCurrentResult { + /** + * Currently active model identifier + */ modelId?: string; } @@ -164,6 +194,9 @@ export interface SessionModelGetCurrentParams { } export interface SessionModelSwitchToResult { + /** + * Currently active model identifier after the switch + */ modelId?: string; } @@ -172,8 +205,14 @@ export interface SessionModelSwitchToParams { * Target session identifier */ sessionId: string; + /** + * Model identifier to switch to + */ modelId: string; - reasoningEffort?: "low" | "medium" | "high" | "xhigh"; + /** + * Reasoning effort level to use for the model + */ + reasoningEffort?: string; } export interface SessionModeGetResult { @@ -436,6 +475,9 @@ export interface SessionCompactionCompactParams { } export interface SessionToolsHandlePendingToolCallResult { + /** + * Whether the tool call result was handled successfully + */ success: boolean; } @@ -459,6 +501,9 @@ export interface SessionToolsHandlePendingToolCallParams { } export interface SessionPermissionsHandlePendingPermissionRequestResult { + /** + * Whether the permission request was handled successfully + */ success: boolean; } @@ -516,6 +561,54 @@ export interface SessionLogParams { ephemeral?: boolean; } +export interface SessionShellExecResult { + /** + * Unique identifier for tracking streamed output + */ + processId: string; +} + +export interface SessionShellExecParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Shell command to execute + */ + command: string; + /** + * Working directory (defaults to session working directory) + */ + cwd?: string; + /** + * Timeout in milliseconds (default: 30000) + */ + timeout?: number; +} + +export interface SessionShellKillResult { + /** + * Whether the signal was sent successfully + */ + killed: boolean; +} + +export interface SessionShellKillParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Process identifier returned by shell.exec + */ + processId: string; + /** + * Signal to send (default: SIGTERM) + */ + signal?: "SIGTERM" | "SIGKILL" | "SIGINT"; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -595,5 +688,11 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin }, log: async (params: Omit): Promise => connection.sendRequest("session.log", { sessionId, ...params }), + shell: { + exec: async (params: Omit): Promise => + connection.sendRequest("session.shell.exec", { sessionId, ...params }), + kill: async (params: Omit): Promise => + connection.sendRequest("session.shell.kill", { sessionId, ...params }), + }, }; } diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index f5329cc88c..e9d48bc57e 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -22,6 +22,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.start"; + /** + * Session initialization metadata including context and configuration + */ data: { /** * Unique identifier for the session @@ -47,6 +50,10 @@ export type SessionEvent = * Model selected at session creation time, if any */ selectedModel?: string; + /** + * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh") + */ + reasoningEffort?: string; /** * Working directory and git context at session start */ @@ -60,14 +67,29 @@ export type SessionEvent = */ gitRoot?: string; /** - * Repository identifier in "owner/name" format, derived from the git remote URL + * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ repository?: string; + /** + * Hosting platform type of the repository (github or ado) + */ + hostType?: "github" | "ado"; /** * Current git branch name */ branch?: string; + /** + * Head commit of current git branch at session start time + */ + headCommit?: string; + /** + * Base commit of current git branch at session start time + */ + baseCommit?: string; }; + /** + * Whether the session was already in use by another client at start time + */ alreadyInUse?: boolean; }; } @@ -89,6 +111,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.resume"; + /** + * Session resume metadata including current context and event count + */ data: { /** * ISO 8601 timestamp when the session was resumed @@ -98,6 +123,14 @@ export type SessionEvent = * Total number of persisted events in the session at the time of resume */ eventCount: number; + /** + * Model currently selected at resume time + */ + selectedModel?: string; + /** + * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh") + */ + reasoningEffort?: string; /** * Updated working directory and git context at resume time */ @@ -111,14 +144,29 @@ export type SessionEvent = */ gitRoot?: string; /** - * Repository identifier in "owner/name" format, derived from the git remote URL + * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ repository?: string; + /** + * Hosting platform type of the repository (github or ado) + */ + hostType?: "github" | "ado"; /** * Current git branch name */ branch?: string; + /** + * Head commit of current git branch at session start time + */ + headCommit?: string; + /** + * Base commit of current git branch at session start time + */ + baseCommit?: string; }; + /** + * Whether the session was already in use by another client at resume time + */ alreadyInUse?: boolean; }; } @@ -140,6 +188,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.error"; + /** + * Error details for timeline display including message and optional diagnostic information + */ data: { /** * Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query") @@ -234,6 +285,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "session.title_changed"; + /** + * Session title change payload containing the new display title + */ data: { /** * The new display title for the session @@ -259,6 +313,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.info"; + /** + * Informational message for timeline display with categorization + */ data: { /** * Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model") @@ -288,6 +345,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.warning"; + /** + * Warning message for timeline display with categorization + */ data: { /** * Category of warning (e.g., "subscription", "policy", "mcp") @@ -317,6 +377,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.model_change"; + /** + * Model change details including previous and new model identifiers + */ data: { /** * Model that was previously selected, if any @@ -326,6 +389,14 @@ export type SessionEvent = * Newly selected model identifier */ newModel: string; + /** + * Reasoning effort level before the model change, if applicable + */ + previousReasoningEffort?: string; + /** + * Reasoning effort level after the model change, if applicable + */ + reasoningEffort?: string; }; } | { @@ -346,6 +417,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.mode_changed"; + /** + * Agent mode change details including previous and new modes + */ data: { /** * Agent mode before the change (e.g., "interactive", "plan", "autopilot") @@ -375,6 +449,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.plan_changed"; + /** + * Plan file operation details indicating what changed + */ data: { /** * The type of operation performed on the plan file @@ -400,6 +477,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.workspace_file_changed"; + /** + * Workspace file change details including path and operation type + */ data: { /** * Relative path within the session workspace files directory @@ -429,6 +509,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.handoff"; + /** + * Session handoff metadata including source, context, and repository information + */ data: { /** * ISO 8601 timestamp when the handoff occurred @@ -487,6 +570,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.truncation"; + /** + * Conversation truncation statistics including token counts and removed content metrics + */ data: { /** * Maximum token count for the model's context window @@ -537,6 +623,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "session.snapshot_rewind"; + /** + * Session rewind details including target event and count of removed events + */ data: { /** * Event ID that was rewound to; all events after this one were removed @@ -566,6 +655,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.shutdown"; + /** + * Session termination metrics including usage statistics, code changes, and shutdown reason + */ data: { /** * Whether the session ended normally ("routine") or due to a crash/fatal error ("error") @@ -669,6 +761,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.context_changed"; + /** + * Updated working directory and git context after the change + */ data: { /** * Current working directory path @@ -679,13 +774,25 @@ export type SessionEvent = */ gitRoot?: string; /** - * Repository identifier in "owner/name" format, derived from the git remote URL + * Repository identifier derived from the git remote URL ("owner/name" for GitHub, "org/project/repo" for Azure DevOps) */ repository?: string; + /** + * Hosting platform type of the repository (github or ado) + */ + hostType?: "github" | "ado"; /** * Current git branch name */ branch?: string; + /** + * Head commit of current git branch at session start time + */ + headCommit?: string; + /** + * Base commit of current git branch at session start time + */ + baseCommit?: string; }; } | { @@ -703,6 +810,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "session.usage_info"; + /** + * Current context window usage statistics including token and message counts + */ data: { /** * Maximum token count for the model's context window @@ -759,6 +869,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.compaction_complete"; + /** + * Conversation compaction results including success status, metrics, and optional error details + */ data: { /** * Whether compaction completed successfully @@ -841,6 +954,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "session.task_complete"; + /** + * Task completion notification with optional summary from the agent + */ data: { /** * Optional summary of the completed task, provided by the agent @@ -866,6 +982,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "user.message"; + /** + * User message content with optional attachments, source information, and interaction metadata + */ data: { /** * The user's message text as displayed in the timeline @@ -880,9 +999,12 @@ export type SessionEvent = */ attachments?: ( | { + /** + * Attachment type discriminator + */ type: "file"; /** - * Absolute file or directory path + * Absolute file path */ path: string; /** @@ -904,28 +1026,18 @@ export type SessionEvent = }; } | { + /** + * Attachment type discriminator + */ type: "directory"; /** - * Absolute file or directory path + * Absolute directory path */ path: string; /** * User-facing display name for the attachment */ displayName: string; - /** - * Optional line range to scope the attachment to a specific section of the file - */ - lineRange?: { - /** - * Start line number (1-based) - */ - start: number; - /** - * End line number (1-based, inclusive) - */ - end: number; - }; } | { /** @@ -948,6 +1060,9 @@ export type SessionEvent = * Position range of the selection within the file */ selection: { + /** + * Start position of the selection + */ start: { /** * Start line number (0-based) @@ -958,6 +1073,9 @@ export type SessionEvent = */ character: number; }; + /** + * End position of the selection + */ end: { /** * End line number (0-based) @@ -996,11 +1114,39 @@ export type SessionEvent = */ url: string; } + | { + /** + * Attachment type discriminator + */ + type: "blob"; + /** + * Base64-encoded content + */ + data: string; + /** + * MIME type of the inline data + */ + mimeType: string; + /** + * User-facing display name for the attachment + */ + displayName?: string; + } )[]; /** - * Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) + * Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command") */ - source?: string; + source?: + | "user" + | "autopilot" + | "skill" + | "system" + | "command" + | "immediate-prompt" + | "jit-instruction" + | "snippy-blocking" + | "thinking-exhausted-continuation" + | "other"; /** * The agent mode that was active when this message was sent */ @@ -1049,6 +1195,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "assistant.turn_start"; + /** + * Turn initialization metadata including identifier and interaction tracking + */ data: { /** * Identifier for this turn within the agentic loop, typically a stringified turn number @@ -1075,6 +1224,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "assistant.intent"; + /** + * Agent intent description for current activity or plan + */ data: { /** * Short description of what the agent is currently doing or planning to do @@ -1100,6 +1252,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "assistant.reasoning"; + /** + * Assistant reasoning content for timeline display with complete thinking text + */ data: { /** * Unique identifier for this reasoning block @@ -1126,6 +1281,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "assistant.reasoning_delta"; + /** + * Streaming reasoning delta for incremental extended thinking updates + */ data: { /** * Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event @@ -1152,6 +1310,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "assistant.streaming_delta"; + /** + * Streaming response progress with cumulative byte count + */ data: { /** * Cumulative total bytes received from the streaming response so far @@ -1177,6 +1338,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "assistant.message"; + /** + * Assistant response containing text content, optional tool requests, and interaction metadata + */ data: { /** * Unique identifier for this assistant message @@ -1208,6 +1372,14 @@ export type SessionEvent = * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ type?: "function" | "custom"; + /** + * Human-readable display title for the tool + */ + toolTitle?: string; + /** + * Resolved intention summary describing what this specific call does + */ + intentionSummary?: string | null; }[]; /** * Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. @@ -1254,6 +1426,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "assistant.message_delta"; + /** + * Streaming assistant message delta for incremental response updates + */ data: { /** * Message ID this delta belongs to, matching the corresponding assistant.message event @@ -1287,6 +1462,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "assistant.turn_end"; + /** + * Turn completion metadata including the turn identifier + */ data: { /** * Identifier of the turn that has ended, matching the corresponding assistant.turn_start event @@ -1309,6 +1487,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "assistant.usage"; + /** + * LLM API call usage metrics including tokens, costs, quotas, and billing information + */ data: { /** * Model identifier used for this API call @@ -1423,6 +1604,10 @@ export type SessionEvent = */ totalNanoAiu: number; }; + /** + * Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", "xhigh") + */ + reasoningEffort?: string; }; } | { @@ -1443,6 +1628,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "abort"; + /** + * Turn abort information including the reason for termination + */ data: { /** * Reason the current turn was aborted (e.g., "user initiated") @@ -1468,6 +1656,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "tool.user_requested"; + /** + * User-initiated tool invocation request with tool name and arguments + */ data: { /** * Unique identifier for this tool call @@ -1503,6 +1694,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "tool.execution_start"; + /** + * Tool execution startup details including MCP server information when applicable + */ data: { /** * Unique identifier for this tool call @@ -1547,6 +1741,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "tool.execution_partial_result"; + /** + * Streaming tool execution output for incremental result display + */ data: { /** * Tool call ID this partial result belongs to @@ -1573,6 +1770,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "tool.execution_progress"; + /** + * Tool execution progress notification with status message + */ data: { /** * Tool call ID this progress notification belongs to @@ -1602,6 +1802,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "tool.execution_complete"; + /** + * Tool execution completion results including success status, detailed output, and error information + */ data: { /** * Unique identifier for the completed tool call @@ -1829,6 +2032,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "skill.invoked"; + /** + * Skill invocation details including content, allowed tools, and plugin metadata + */ data: { /** * Name of the invoked skill @@ -1874,6 +2080,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "subagent.started"; + /** + * Sub-agent startup details including parent tool call and agent information + */ data: { /** * Tool call ID of the parent tool invocation that spawned this sub-agent @@ -1911,6 +2120,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "subagent.completed"; + /** + * Sub-agent completion details for successful execution + */ data: { /** * Tool call ID of the parent tool invocation that spawned this sub-agent @@ -1944,6 +2156,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "subagent.failed"; + /** + * Sub-agent failure details including error message and agent information + */ data: { /** * Tool call ID of the parent tool invocation that spawned this sub-agent @@ -1981,6 +2196,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "subagent.selected"; + /** + * Custom agent selection details including name and available tools + */ data: { /** * Internal name of the selected custom agent @@ -2037,6 +2255,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "hook.start"; + /** + * Hook invocation start details including type and input data + */ data: { /** * Unique identifier for this hook invocation @@ -2072,6 +2293,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "hook.end"; + /** + * Hook invocation completion details including output, success status, and error information + */ data: { /** * Identifier matching the corresponding hook.start event @@ -2124,6 +2348,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "system.message"; + /** + * System or developer message content with role and optional template metadata + */ data: { /** * The system or developer prompt text @@ -2172,6 +2399,9 @@ export type SessionEvent = */ ephemeral?: boolean; type: "system.notification"; + /** + * System-generated notification for runtime events like background task completion + */ data: { /** * The notification text, typically wrapped in XML tags @@ -2247,6 +2477,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "permission.requested"; + /** + * Permission request notification requiring client approval with request details + */ data: { /** * Unique identifier for this permission request; used to respond via session.respondToPermission() @@ -2451,6 +2684,30 @@ export type SessionEvent = args?: { [k: string]: unknown; }; + } + | { + /** + * Permission kind discriminator + */ + kind: "hook"; + /** + * Tool call ID that triggered this permission request + */ + toolCallId?: string; + /** + * Name of the tool the hook is gating + */ + toolName: string; + /** + * Arguments of the tool call being gated + */ + toolArgs?: { + [k: string]: unknown; + }; + /** + * Optional message from the hook explaining why confirmation is needed + */ + hookMessage?: string; }; }; } @@ -2469,6 +2726,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "permission.completed"; + /** + * Permission request completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved permission request; clients should dismiss any UI for this request @@ -2505,6 +2765,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "user_input.requested"; + /** + * User input request notification with question and optional predefined choices + */ data: { /** * Unique identifier for this input request; used to respond via session.respondToUserInput() @@ -2539,6 +2802,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "user_input.completed"; + /** + * User input request completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved user input request; clients should dismiss any UI for this request @@ -2561,6 +2827,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "elicitation.requested"; + /** + * Structured form elicitation request with JSON schema definition for form fields + */ data: { /** * Unique identifier for this elicitation request; used to respond via session.respondToElicitation() @@ -2578,6 +2847,9 @@ export type SessionEvent = * JSON Schema describing the form fields to present to the user */ requestedSchema: { + /** + * Schema type indicator (always 'object') + */ type: "object"; /** * Form field definitions, keyed by field name @@ -2608,6 +2880,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "elicitation.completed"; + /** + * Elicitation request completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved elicitation request; clients should dismiss any UI for this request @@ -2630,6 +2905,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "external_tool.requested"; + /** + * External tool invocation request for client-side tool execution + */ data: { /** * Unique identifier for this request; used to respond via session.respondToExternalTool() @@ -2653,6 +2931,14 @@ export type SessionEvent = arguments?: { [k: string]: unknown; }; + /** + * W3C Trace Context traceparent header for the execute_tool span + */ + traceparent?: string; + /** + * W3C Trace Context tracestate header for the execute_tool span + */ + tracestate?: string; }; } | { @@ -2670,6 +2956,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "external_tool.completed"; + /** + * External tool completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved external tool request; clients should dismiss any UI for this request @@ -2692,6 +2981,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "command.queued"; + /** + * Queued slash command dispatch request for client execution + */ data: { /** * Unique identifier for this request; used to respond via session.respondToQueuedCommand() @@ -2718,6 +3010,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "command.completed"; + /** + * Queued command completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved command request; clients should dismiss any UI for this request @@ -2740,6 +3035,9 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "exit_plan_mode.requested"; + /** + * Plan approval request with plan content and available user actions + */ data: { /** * Unique identifier for this request; used to respond via session.respondToExitPlanMode() @@ -2778,10 +3076,49 @@ export type SessionEvent = parentId: string | null; ephemeral: true; type: "exit_plan_mode.completed"; + /** + * Plan mode exit completion notification signaling UI dismissal + */ data: { /** * Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request */ requestId: string; }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.tools_updated"; + data: { + model: string; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.background_tasks_changed"; + data: {}; }; diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index d5fa7b73b1..29b7463dfe 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -124,6 +124,7 @@ class Billing: """Billing information""" multiplier: float + """Billing cost multiplier relative to the base rate""" @staticmethod def from_dict(obj: Any) -> 'Billing': @@ -139,9 +140,16 @@ def to_dict(self) -> dict: @dataclass class Limits: + """Token limits for prompts, outputs, and context window""" + max_context_window_tokens: float + """Maximum total context window size in tokens""" + max_output_tokens: float | None = None + """Maximum number of output/completion tokens""" + max_prompt_tokens: float | None = None + """Maximum number of prompt/input tokens""" @staticmethod def from_dict(obj: Any) -> 'Limits': @@ -163,10 +171,13 @@ def to_dict(self) -> dict: @dataclass class Supports: + """Feature flags indicating what the model supports""" + reasoning_effort: bool | None = None """Whether this model supports reasoning effort configuration""" vision: bool | None = None + """Whether this model supports vision/image input""" @staticmethod def from_dict(obj: Any) -> 'Supports': @@ -189,7 +200,10 @@ class Capabilities: """Model capabilities and limits""" limits: Limits + """Token limits for prompts, outputs, and context window""" + supports: Supports + """Feature flags indicating what the model supports""" @staticmethod def from_dict(obj: Any) -> 'Capabilities': @@ -210,7 +224,10 @@ class Policy: """Policy state (if applicable)""" state: str + """Current policy state for this model""" + terms: str + """Usage terms or conditions for this model""" @staticmethod def from_dict(obj: Any) -> 'Policy': @@ -435,6 +452,7 @@ def to_dict(self) -> dict: @dataclass class SessionModelGetCurrentResult: model_id: str | None = None + """Currently active model identifier""" @staticmethod def from_dict(obj: Any) -> 'SessionModelGetCurrentResult': @@ -452,6 +470,7 @@ def to_dict(self) -> dict: @dataclass class SessionModelSwitchToResult: model_id: str | None = None + """Currently active model identifier after the switch""" @staticmethod def from_dict(obj: Any) -> 'SessionModelSwitchToResult': @@ -466,30 +485,26 @@ def to_dict(self) -> dict: return result -class ReasoningEffort(Enum): - HIGH = "high" - LOW = "low" - MEDIUM = "medium" - XHIGH = "xhigh" - - @dataclass class SessionModelSwitchToParams: model_id: str - reasoning_effort: ReasoningEffort | None = None + """Model identifier to switch to""" + + reasoning_effort: str | None = None + """Reasoning effort level to use for the model""" @staticmethod def from_dict(obj: Any) -> 'SessionModelSwitchToParams': assert isinstance(obj, dict) model_id = from_str(obj.get("modelId")) - reasoning_effort = from_union([ReasoningEffort, from_none], obj.get("reasoningEffort")) + reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) return SessionModelSwitchToParams(model_id, reasoning_effort) def to_dict(self) -> dict: result: dict = {} result["modelId"] = from_str(self.model_id) if self.reasoning_effort is not None: - result["reasoningEffort"] = from_union([lambda x: to_enum(ReasoningEffort, x), from_none], self.reasoning_effort) + result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result @@ -937,6 +952,7 @@ def to_dict(self) -> dict: @dataclass class SessionToolsHandlePendingToolCallResult: success: bool + """Whether the tool call result was handled successfully""" @staticmethod def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult': @@ -1005,6 +1021,7 @@ def to_dict(self) -> dict: @dataclass class SessionPermissionsHandlePendingPermissionRequestResult: success: bool + """Whether the permission request was handled successfully""" @staticmethod def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult': @@ -1134,6 +1151,100 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionShellExecResult: + process_id: str + """Unique identifier for tracking streamed output""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellExecResult': + assert isinstance(obj, dict) + process_id = from_str(obj.get("processId")) + return SessionShellExecResult(process_id) + + def to_dict(self) -> dict: + result: dict = {} + result["processId"] = from_str(self.process_id) + return result + + +@dataclass +class SessionShellExecParams: + command: str + """Shell command to execute""" + + cwd: str | None = None + """Working directory (defaults to session working directory)""" + + timeout: float | None = None + """Timeout in milliseconds (default: 30000)""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellExecParams': + assert isinstance(obj, dict) + command = from_str(obj.get("command")) + cwd = from_union([from_str, from_none], obj.get("cwd")) + timeout = from_union([from_float, from_none], obj.get("timeout")) + return SessionShellExecParams(command, cwd, timeout) + + def to_dict(self) -> dict: + result: dict = {} + result["command"] = from_str(self.command) + if self.cwd is not None: + result["cwd"] = from_union([from_str, from_none], self.cwd) + if self.timeout is not None: + result["timeout"] = from_union([to_float, from_none], self.timeout) + return result + + +@dataclass +class SessionShellKillResult: + killed: bool + """Whether the signal was sent successfully""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellKillResult': + assert isinstance(obj, dict) + killed = from_bool(obj.get("killed")) + return SessionShellKillResult(killed) + + def to_dict(self) -> dict: + result: dict = {} + result["killed"] = from_bool(self.killed) + return result + + +class Signal(Enum): + """Signal to send (default: SIGTERM)""" + + SIGINT = "SIGINT" + SIGKILL = "SIGKILL" + SIGTERM = "SIGTERM" + + +@dataclass +class SessionShellKillParams: + process_id: str + """Process identifier returned by shell.exec""" + + signal: Signal | None = None + """Signal to send (default: SIGTERM)""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellKillParams': + assert isinstance(obj, dict) + process_id = from_str(obj.get("processId")) + signal = from_union([Signal, from_none], obj.get("signal")) + return SessionShellKillParams(process_id, signal) + + def to_dict(self) -> dict: + result: dict = {} + result["processId"] = from_str(self.process_id) + if self.signal is not None: + result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal) + return result + + def ping_result_from_dict(s: Any) -> PingResult: return PingResult.from_dict(s) @@ -1414,6 +1525,38 @@ def session_log_params_to_dict(x: SessionLogParams) -> Any: return to_class(SessionLogParams, x) +def session_shell_exec_result_from_dict(s: Any) -> SessionShellExecResult: + return SessionShellExecResult.from_dict(s) + + +def session_shell_exec_result_to_dict(x: SessionShellExecResult) -> Any: + return to_class(SessionShellExecResult, x) + + +def session_shell_exec_params_from_dict(s: Any) -> SessionShellExecParams: + return SessionShellExecParams.from_dict(s) + + +def session_shell_exec_params_to_dict(x: SessionShellExecParams) -> Any: + return to_class(SessionShellExecParams, x) + + +def session_shell_kill_result_from_dict(s: Any) -> SessionShellKillResult: + return SessionShellKillResult.from_dict(s) + + +def session_shell_kill_result_to_dict(x: SessionShellKillResult) -> Any: + return to_class(SessionShellKillResult, x) + + +def session_shell_kill_params_from_dict(s: Any) -> SessionShellKillParams: + return SessionShellKillParams.from_dict(s) + + +def session_shell_kill_params_to_dict(x: SessionShellKillParams) -> Any: + return to_class(SessionShellKillParams, x) + + def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" if timeout is not None: @@ -1585,6 +1728,22 @@ async def handle_pending_permission_request(self, params: SessionPermissionsHand return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(await self._client.request("session.permissions.handlePendingPermissionRequest", params_dict, **_timeout_kwargs(timeout))) +class ShellApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def exec(self, params: SessionShellExecParams, *, timeout: float | None = None) -> SessionShellExecResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionShellExecResult.from_dict(await self._client.request("session.shell.exec", params_dict, **_timeout_kwargs(timeout))) + + async def kill(self, params: SessionShellKillParams, *, timeout: float | None = None) -> SessionShellKillResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionShellKillResult.from_dict(await self._client.request("session.shell.kill", params_dict, **_timeout_kwargs(timeout))) + + class SessionRpc: """Typed session-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient", session_id: str): @@ -1599,6 +1758,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.compaction = CompactionApi(client, session_id) self.tools = ToolsApi(client, session_id) self.permissions = PermissionsApi(client, session_id) + self.shell = ShellApi(client, session_id) async def log(self, params: SessionLogParams, *, timeout: float | None = None) -> SessionLogResult: params_dict = {k: v for k, v in params.to_dict().items() if v is not None} diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 69d07f77b9..3fc3133991 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -122,6 +122,8 @@ class ReferenceType(Enum): @dataclass class End: + """End position of the selection""" + character: float """End character offset within the line (0-based)""" @@ -144,6 +146,8 @@ def to_dict(self) -> dict: @dataclass class Start: + """Start position of the selection""" + character: float """Start character offset within the line (0-based)""" @@ -169,7 +173,10 @@ class Selection: """Position range of the selection within the file""" end: End + """End position of the selection""" + start: Start + """Start position of the selection""" @staticmethod def from_dict(obj: Any) -> 'Selection': @@ -186,6 +193,7 @@ def to_dict(self) -> dict: class AttachmentType(Enum): + BLOB = "blob" DIRECTORY = "directory" FILE = "file" GITHUB_REFERENCE = "github_reference" @@ -194,6 +202,18 @@ class AttachmentType(Enum): @dataclass class Attachment: + """A user message attachment — a file, directory, code selection, blob, or GitHub reference + + File attachment + + Directory attachment + + Code selection attachment from an editor + + GitHub issue, pull request, or discussion reference + + Blob attachment with inline base64-encoded data + """ type: AttachmentType """Attachment type discriminator""" @@ -206,8 +226,10 @@ class Attachment: """Optional line range to scope the attachment to a specific section of the file""" path: str | None = None - """Absolute file or directory path""" - + """Absolute file path + + Absolute directory path + """ file_path: str | None = None """Absolute path to the file containing the selection""" @@ -232,6 +254,12 @@ class Attachment: url: str | None = None """URL to the referenced item on GitHub""" + data: str | None = None + """Base64-encoded content""" + + mime_type: str | None = None + """MIME type of the inline data""" + @staticmethod def from_dict(obj: Any) -> 'Attachment': assert isinstance(obj, dict) @@ -247,7 +275,9 @@ def from_dict(obj: Any) -> 'Attachment': state = from_union([from_str, from_none], obj.get("state")) title = from_union([from_str, from_none], obj.get("title")) url = from_union([from_str, from_none], obj.get("url")) - return Attachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url) + data = from_union([from_str, from_none], obj.get("data")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + return Attachment(type, display_name, line_range, path, file_path, selection, text, number, reference_type, state, title, url, data, mime_type) def to_dict(self) -> dict: result: dict = {} @@ -274,11 +304,17 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) if self.url is not None: result["url"] = from_union([from_str, from_none], self.url) + if self.data is not None: + result["data"] = from_union([from_str, from_none], self.data) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) return result @dataclass class Agent: + """A background agent task""" + agent_id: str """Unique identifier of the background agent""" @@ -307,6 +343,8 @@ def to_dict(self) -> dict: @dataclass class Shell: + """A background shell command""" + shell_id: str """Unique identifier of the background shell""" @@ -410,6 +448,13 @@ def to_dict(self) -> dict: return result +class HostType(Enum): + """Hosting platform type of the repository (github or ado)""" + + ADO = "ado" + GITHUB = "github" + + @dataclass class ContextClass: """Working directory and git context at session start @@ -419,31 +464,51 @@ class ContextClass: cwd: str """Current working directory path""" + base_commit: str | None = None + """Base commit of current git branch at session start time""" + branch: str | None = None """Current git branch name""" git_root: str | None = None """Root directory of the git repository, resolved via git rev-parse""" + head_commit: str | None = None + """Head commit of current git branch at session start time""" + + host_type: HostType | None = None + """Hosting platform type of the repository (github or ado)""" + repository: str | None = None - """Repository identifier in "owner/name" format, derived from the git remote URL""" + """Repository identifier derived from the git remote URL ("owner/name" for GitHub, + "org/project/repo" for Azure DevOps) + """ @staticmethod def from_dict(obj: Any) -> 'ContextClass': assert isinstance(obj, dict) cwd = from_str(obj.get("cwd")) + base_commit = from_union([from_str, from_none], obj.get("baseCommit")) branch = from_union([from_str, from_none], obj.get("branch")) git_root = from_union([from_str, from_none], obj.get("gitRoot")) + head_commit = from_union([from_str, from_none], obj.get("headCommit")) + host_type = from_union([HostType, from_none], obj.get("hostType")) repository = from_union([from_str, from_none], obj.get("repository")) - return ContextClass(cwd, branch, git_root, repository) + return ContextClass(cwd, base_commit, branch, git_root, head_commit, host_type, repository) def to_dict(self) -> dict: result: dict = {} result["cwd"] = from_str(self.cwd) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_str, from_none], self.base_commit) if self.branch is not None: result["branch"] = from_union([from_str, from_none], self.branch) if self.git_root is not None: result["gitRoot"] = from_union([from_str, from_none], self.git_root) + if self.head_commit is not None: + result["headCommit"] = from_union([from_str, from_none], self.head_commit) + if self.host_type is not None: + result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type) if self.repository is not None: result["repository"] = from_union([from_str, from_none], self.repository) return result @@ -451,6 +516,8 @@ def to_dict(self) -> dict: @dataclass class TokenDetail: + """Token usage detail for a single billing category""" + batch_size: float """Number of tokens in this billing batch""" @@ -759,6 +826,7 @@ def to_dict(self) -> dict: class PermissionRequestKind(Enum): CUSTOM_TOOL = "custom-tool" + HOOK = "hook" MCP = "mcp" MEMORY = "memory" READ = "read" @@ -786,8 +854,24 @@ def to_dict(self) -> dict: @dataclass class PermissionRequest: - """Details of the permission being requested""" - + """Details of the permission being requested + + Shell command permission request + + File write permission request + + File or directory read permission request + + MCP tool invocation permission request + + URL access permission request + + Memory storage permission request + + Custom tool invocation permission request + + Hook confirmation permission request + """ kind: PermissionRequestKind """Permission kind discriminator""" @@ -851,6 +935,8 @@ class PermissionRequest: """Internal name of the MCP tool Name of the custom tool + + Name of the tool the hook is gating """ tool_title: str | None = None """Human-readable title of the MCP tool""" @@ -870,6 +956,12 @@ class PermissionRequest: tool_description: str | None = None """Description of what the custom tool does""" + hook_message: str | None = None + """Optional message from the hook explaining why confirmation is needed""" + + tool_args: Any = None + """Arguments of the tool call being gated""" + @staticmethod def from_dict(obj: Any) -> 'PermissionRequest': assert isinstance(obj, dict) @@ -897,7 +989,9 @@ def from_dict(obj: Any) -> 'PermissionRequest': fact = from_union([from_str, from_none], obj.get("fact")) subject = from_union([from_str, from_none], obj.get("subject")) tool_description = from_union([from_str, from_none], obj.get("toolDescription")) - return PermissionRequest(kind, can_offer_session_approval, commands, full_command_text, has_write_file_redirection, intention, possible_paths, possible_urls, tool_call_id, warning, diff, file_name, new_file_contents, path, args, read_only, server_name, tool_name, tool_title, url, citations, fact, subject, tool_description) + hook_message = from_union([from_str, from_none], obj.get("hookMessage")) + tool_args = obj.get("toolArgs") + return PermissionRequest(kind, can_offer_session_approval, commands, full_command_text, has_write_file_redirection, intention, possible_paths, possible_urls, tool_call_id, warning, diff, file_name, new_file_contents, path, args, read_only, server_name, tool_name, tool_title, url, citations, fact, subject, tool_description, hook_message, tool_args) def to_dict(self) -> dict: result: dict = {} @@ -948,6 +1042,10 @@ def to_dict(self) -> dict: result["subject"] = from_union([from_str, from_none], self.subject) if self.tool_description is not None: result["toolDescription"] = from_union([from_str, from_none], self.tool_description) + if self.hook_message is not None: + result["hookMessage"] = from_union([from_str, from_none], self.hook_message) + if self.tool_args is not None: + result["toolArgs"] = self.tool_args return result @@ -1046,6 +1144,8 @@ class RequestedSchema: """Form field definitions, keyed by field name""" type: RequestedSchemaType + """Schema type indicator (always 'object')""" + required: list[str] | None = None """List of required field names""" @@ -1075,6 +1175,8 @@ class Theme(Enum): @dataclass class Icon: + """Icon image for a resource""" + src: str """URL or path to the icon image""" @@ -1158,6 +1260,21 @@ class ContentType(Enum): @dataclass class Content: + """A content block within a tool result, which may be text, terminal output, image, audio, + or a resource + + Plain text content block + + Terminal/shell output content block with optional exit code and working directory + + Image content block with base64-encoded data + + Audio content block with base64-encoded data + + Resource link content block referencing an external resource + + Embedded resource content block with inline text or binary data + """ type: ContentType """Content block type discriminator""" @@ -1320,6 +1437,22 @@ class ShutdownType(Enum): ROUTINE = "routine" +class Source(Enum): + """Origin of this message, used for timeline filtering and telemetry (e.g., "user", + "autopilot", "skill", or "command") + """ + AUTOPILOT = "autopilot" + COMMAND = "command" + IMMEDIATE_PROMPT = "immediate-prompt" + JIT_INSTRUCTION = "jit-instruction" + OTHER = "other" + SKILL = "skill" + SNIPPY_BLOCKING = "snippy-blocking" + SYSTEM = "system" + THINKING_EXHAUSTED_CONTINUATION = "thinking-exhausted-continuation" + USER = "user" + + class SourceType(Enum): """Origin type of the session being handed off""" @@ -1337,6 +1470,8 @@ class ToolRequestType(Enum): @dataclass class ToolRequest: + """A tool invocation request from the assistant""" + name: str """Name of the tool being invoked""" @@ -1346,6 +1481,12 @@ class ToolRequest: arguments: Any = None """Arguments to pass to the tool, format depends on the tool""" + intention_summary: str | None = None + """Resolved intention summary describing what this specific call does""" + + tool_title: str | None = None + """Human-readable display title for the tool""" + type: ToolRequestType | None = None """Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. @@ -1357,8 +1498,10 @@ def from_dict(obj: Any) -> 'ToolRequest': name = from_str(obj.get("name")) tool_call_id = from_str(obj.get("toolCallId")) arguments = obj.get("arguments") + intention_summary = from_union([from_none, from_str], obj.get("intentionSummary")) + tool_title = from_union([from_str, from_none], obj.get("toolTitle")) type = from_union([ToolRequestType, from_none], obj.get("type")) - return ToolRequest(name, tool_call_id, arguments, type) + return ToolRequest(name, tool_call_id, arguments, intention_summary, tool_title, type) def to_dict(self) -> dict: result: dict = {} @@ -1366,6 +1509,10 @@ def to_dict(self) -> dict: result["toolCallId"] = from_str(self.tool_call_id) if self.arguments is not None: result["arguments"] = self.arguments + if self.intention_summary is not None: + result["intentionSummary"] = from_union([from_none, from_str], self.intention_summary) + if self.tool_title is not None: + result["toolTitle"] = from_union([from_str, from_none], self.tool_title) if self.type is not None: result["type"] = from_union([lambda x: to_enum(ToolRequestType, x), from_none], self.type) return result @@ -1373,16 +1520,136 @@ def to_dict(self) -> dict: @dataclass class Data: - """Payload indicating the agent is idle; includes any background tasks still in flight + """Session initialization metadata including context and configuration + + Session resume metadata including current context and event count + + Error details for timeline display including message and optional diagnostic information + + Payload indicating the agent is idle; includes any background tasks still in flight + + Session title change payload containing the new display title + + Informational message for timeline display with categorization + + Warning message for timeline display with categorization + + Model change details including previous and new model identifiers + + Agent mode change details including previous and new modes + + Plan file operation details indicating what changed + + Workspace file change details including path and operation type + + Session handoff metadata including source, context, and repository information + + Conversation truncation statistics including token counts and removed content metrics + + Session rewind details including target event and count of removed events + + Session termination metrics including usage statistics, code changes, and shutdown + reason + + Updated working directory and git context after the change + + Current context window usage statistics including token and message counts Empty payload; the event signals that LLM-powered conversation compaction has begun + Conversation compaction results including success status, metrics, and optional error + details + + Task completion notification with optional summary from the agent + + User message content with optional attachments, source information, and interaction + metadata + Empty payload; the event signals that the pending message queue has changed + Turn initialization metadata including identifier and interaction tracking + + Agent intent description for current activity or plan + + Assistant reasoning content for timeline display with complete thinking text + + Streaming reasoning delta for incremental extended thinking updates + + Streaming response progress with cumulative byte count + + Assistant response containing text content, optional tool requests, and interaction + metadata + + Streaming assistant message delta for incremental response updates + + Turn completion metadata including the turn identifier + + LLM API call usage metrics including tokens, costs, quotas, and billing information + + Turn abort information including the reason for termination + + User-initiated tool invocation request with tool name and arguments + + Tool execution startup details including MCP server information when applicable + + Streaming tool execution output for incremental result display + + Tool execution progress notification with status message + + Tool execution completion results including success status, detailed output, and error + information + + Skill invocation details including content, allowed tools, and plugin metadata + + Sub-agent startup details including parent tool call and agent information + + Sub-agent completion details for successful execution + + Sub-agent failure details including error message and agent information + + Custom agent selection details including name and available tools + Empty payload; the event signals that the custom agent was deselected, returning to the default agent + + Hook invocation start details including type and input data + + Hook invocation completion details including output, success status, and error + information + + System or developer message content with role and optional template metadata + + System-generated notification for runtime events like background task completion + + Permission request notification requiring client approval with request details + + Permission request completion notification signaling UI dismissal + + User input request notification with question and optional predefined choices + + User input request completion notification signaling UI dismissal + + Structured form elicitation request with JSON schema definition for form fields + + Elicitation request completion notification signaling UI dismissal + + External tool invocation request for client-side tool execution + + External tool completion notification signaling UI dismissal + + Queued slash command dispatch request for client execution + + Queued command completion notification signaling UI dismissal + + Plan approval request with plan content and available user actions + + Plan mode exit completion notification signaling UI dismissal """ already_in_use: bool | None = None + """Whether the session was already in use by another client at start time + + Whether the session was already in use by another client at resume time + """ context: ContextClass | str | None = None """Working directory and git context at session start @@ -1396,9 +1663,17 @@ class Data: producer: str | None = None """Identifier of the software producing the events (e.g., "copilot-agent")""" + reasoning_effort: str | None = None + """Reasoning effort level used for model calls, if applicable (e.g. "low", "medium", "high", + "xhigh") + + Reasoning effort level after the model change, if applicable + """ selected_model: str | None = None - """Model selected at session creation time, if any""" - + """Model selected at session creation time, if any + + Model currently selected at resume time + """ session_id: str | None = None """Unique identifier for the session @@ -1460,6 +1735,9 @@ class Data: previous_model: str | None = None """Model that was previously selected, if any""" + previous_reasoning_effort: str | None = None + """Reasoning effort level before the model change, if applicable""" + new_mode: str | None = None """Agent mode after the change (e.g., "interactive", "plan", "autopilot")""" @@ -1485,7 +1763,8 @@ class Data: repository: RepositoryClass | str | None = None """Repository context for the handed-off session - Repository identifier in "owner/name" format, derived from the git remote URL + Repository identifier derived from the git remote URL ("owner/name" for GitHub, + "org/project/repo" for Azure DevOps) """ source_type: SourceType | None = None """Origin type of the session being handed off""" @@ -1551,6 +1830,9 @@ class Data: total_premium_requests: float | None = None """Total number of premium API requests used during the session""" + base_commit: str | None = None + """Base commit of current git branch at session start time""" + branch: str | None = None """Current git branch name""" @@ -1560,6 +1842,12 @@ class Data: git_root: str | None = None """Root directory of the git repository, resolved via git rev-parse""" + head_commit: str | None = None + """Head commit of current git branch at session start time""" + + host_type: HostType | None = None + """Hosting platform type of the repository (github or ado)""" + current_tokens: float | None = None """Current number of tokens in the context window""" @@ -1673,9 +1961,9 @@ class Data: CAPI interaction ID for correlating this tool execution with upstream telemetry """ - source: str | None = None - """Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected - messages that should be hidden from the user) + source: Source | None = None + """Origin of this message, used for timeline filtering and telemetry (e.g., "user", + "autopilot", "skill", or "command") """ transformed_content: str | None = None """Transformed version of the message sent to the model, with XML wrapping, timestamps, and @@ -1894,6 +2182,12 @@ class Data: requested_schema: RequestedSchema | None = None """JSON Schema describing the form fields to present to the user""" + traceparent: str | None = None + """W3C Trace Context traceparent header for the execute_tool span""" + + tracestate: str | None = None + """W3C Trace Context tracestate header for the execute_tool span""" + command: str | None = None """The slash command text to be executed (e.g., /help, /clear)""" @@ -1913,6 +2207,7 @@ def from_dict(obj: Any) -> 'Data': context = from_union([ContextClass.from_dict, from_str, from_none], obj.get("context")) copilot_version = from_union([from_str, from_none], obj.get("copilotVersion")) producer = from_union([from_str, from_none], obj.get("producer")) + reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) selected_model = from_union([from_str, from_none], obj.get("selectedModel")) session_id = from_union([from_str, from_none], obj.get("sessionId")) start_time = from_union([from_datetime, from_none], obj.get("startTime")) @@ -1930,6 +2225,7 @@ def from_dict(obj: Any) -> 'Data': warning_type = from_union([from_str, from_none], obj.get("warningType")) new_model = from_union([from_str, from_none], obj.get("newModel")) previous_model = from_union([from_str, from_none], obj.get("previousModel")) + previous_reasoning_effort = from_union([from_str, from_none], obj.get("previousReasoningEffort")) new_mode = from_union([from_str, from_none], obj.get("newMode")) previous_mode = from_union([from_str, from_none], obj.get("previousMode")) operation = from_union([Operation, from_none], obj.get("operation")) @@ -1957,9 +2253,12 @@ def from_dict(obj: Any) -> 'Data': shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType")) total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs")) total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests")) + base_commit = from_union([from_str, from_none], obj.get("baseCommit")) branch = from_union([from_str, from_none], obj.get("branch")) cwd = from_union([from_str, from_none], obj.get("cwd")) git_root = from_union([from_str, from_none], obj.get("gitRoot")) + head_commit = from_union([from_str, from_none], obj.get("headCommit")) + host_type = from_union([HostType, from_none], obj.get("hostType")) current_tokens = from_union([from_float, from_none], obj.get("currentTokens")) messages_length = from_union([from_float, from_none], obj.get("messagesLength")) checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber")) @@ -1978,7 +2277,7 @@ def from_dict(obj: Any) -> 'Data': attachments = from_union([lambda x: from_list(Attachment.from_dict, x), from_none], obj.get("attachments")) content = from_union([from_str, from_none], obj.get("content")) interaction_id = from_union([from_str, from_none], obj.get("interactionId")) - source = from_union([from_str, from_none], obj.get("source")) + source = from_union([Source, from_none], obj.get("source")) transformed_content = from_union([from_str, from_none], obj.get("transformedContent")) turn_id = from_union([from_str, from_none], obj.get("turnId")) intent = from_union([from_str, from_none], obj.get("intent")) @@ -2035,11 +2334,13 @@ def from_dict(obj: Any) -> 'Data': question = from_union([from_str, from_none], obj.get("question")) mode = from_union([Mode, from_none], obj.get("mode")) requested_schema = from_union([RequestedSchema.from_dict, from_none], obj.get("requestedSchema")) + traceparent = from_union([from_str, from_none], obj.get("traceparent")) + tracestate = from_union([from_str, from_none], obj.get("tracestate")) command = from_union([from_str, from_none], obj.get("command")) actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions")) plan_content = from_union([from_str, from_none], obj.get("planContent")) recommended_action = from_union([from_str, from_none], obj.get("recommendedAction")) - return Data(already_in_use, context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, mode, requested_schema, command, actions, plan_content, recommended_action) + return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, mode, requested_schema, traceparent, tracestate, command, actions, plan_content, recommended_action) def to_dict(self) -> dict: result: dict = {} @@ -2051,6 +2352,8 @@ def to_dict(self) -> dict: result["copilotVersion"] = from_union([from_str, from_none], self.copilot_version) if self.producer is not None: result["producer"] = from_union([from_str, from_none], self.producer) + if self.reasoning_effort is not None: + result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) if self.selected_model is not None: result["selectedModel"] = from_union([from_str, from_none], self.selected_model) if self.session_id is not None: @@ -2085,6 +2388,8 @@ def to_dict(self) -> dict: result["newModel"] = from_union([from_str, from_none], self.new_model) if self.previous_model is not None: result["previousModel"] = from_union([from_str, from_none], self.previous_model) + if self.previous_reasoning_effort is not None: + result["previousReasoningEffort"] = from_union([from_str, from_none], self.previous_reasoning_effort) if self.new_mode is not None: result["newMode"] = from_union([from_str, from_none], self.new_mode) if self.previous_mode is not None: @@ -2139,12 +2444,18 @@ def to_dict(self) -> dict: result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms) if self.total_premium_requests is not None: result["totalPremiumRequests"] = from_union([to_float, from_none], self.total_premium_requests) + if self.base_commit is not None: + result["baseCommit"] = from_union([from_str, from_none], self.base_commit) if self.branch is not None: result["branch"] = from_union([from_str, from_none], self.branch) if self.cwd is not None: result["cwd"] = from_union([from_str, from_none], self.cwd) if self.git_root is not None: result["gitRoot"] = from_union([from_str, from_none], self.git_root) + if self.head_commit is not None: + result["headCommit"] = from_union([from_str, from_none], self.head_commit) + if self.host_type is not None: + result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type) if self.current_tokens is not None: result["currentTokens"] = from_union([to_float, from_none], self.current_tokens) if self.messages_length is not None: @@ -2182,7 +2493,7 @@ def to_dict(self) -> dict: if self.interaction_id is not None: result["interactionId"] = from_union([from_str, from_none], self.interaction_id) if self.source is not None: - result["source"] = from_union([from_str, from_none], self.source) + result["source"] = from_union([lambda x: to_enum(Source, x), from_none], self.source) if self.transformed_content is not None: result["transformedContent"] = from_union([from_str, from_none], self.transformed_content) if self.turn_id is not None: @@ -2295,6 +2606,10 @@ def to_dict(self) -> dict: result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode) if self.requested_schema is not None: result["requestedSchema"] = from_union([lambda x: to_class(RequestedSchema, x), from_none], self.requested_schema) + if self.traceparent is not None: + result["traceparent"] = from_union([from_str, from_none], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_str, from_none], self.tracestate) if self.command is not None: result["command"] = from_union([from_str, from_none], self.command) if self.actions is not None: @@ -2330,6 +2645,7 @@ class SessionEventType(Enum): PENDING_MESSAGES_MODIFIED = "pending_messages.modified" PERMISSION_COMPLETED = "permission.completed" PERMISSION_REQUESTED = "permission.requested" + SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_COMPACTION_START = "session.compaction_start" SESSION_CONTEXT_CHANGED = "session.context_changed" @@ -2346,6 +2662,7 @@ class SessionEventType(Enum): SESSION_START = "session.start" SESSION_TASK_COMPLETE = "session.task_complete" SESSION_TITLE_CHANGED = "session.title_changed" + SESSION_TOOLS_UPDATED = "session.tools_updated" SESSION_TRUNCATION = "session.truncation" SESSION_USAGE_INFO = "session.usage_info" SESSION_WARNING = "session.warning" @@ -2379,14 +2696,130 @@ def _missing_(cls, value: object) -> "SessionEventType": @dataclass class SessionEvent: data: Data - """Payload indicating the agent is idle; includes any background tasks still in flight + """Session initialization metadata including context and configuration + + Session resume metadata including current context and event count + + Error details for timeline display including message and optional diagnostic information + + Payload indicating the agent is idle; includes any background tasks still in flight + + Session title change payload containing the new display title + + Informational message for timeline display with categorization + + Warning message for timeline display with categorization + + Model change details including previous and new model identifiers + + Agent mode change details including previous and new modes + + Plan file operation details indicating what changed + + Workspace file change details including path and operation type + + Session handoff metadata including source, context, and repository information + + Conversation truncation statistics including token counts and removed content metrics + + Session rewind details including target event and count of removed events + + Session termination metrics including usage statistics, code changes, and shutdown + reason + + Updated working directory and git context after the change + + Current context window usage statistics including token and message counts Empty payload; the event signals that LLM-powered conversation compaction has begun + Conversation compaction results including success status, metrics, and optional error + details + + Task completion notification with optional summary from the agent + + User message content with optional attachments, source information, and interaction + metadata + Empty payload; the event signals that the pending message queue has changed + Turn initialization metadata including identifier and interaction tracking + + Agent intent description for current activity or plan + + Assistant reasoning content for timeline display with complete thinking text + + Streaming reasoning delta for incremental extended thinking updates + + Streaming response progress with cumulative byte count + + Assistant response containing text content, optional tool requests, and interaction + metadata + + Streaming assistant message delta for incremental response updates + + Turn completion metadata including the turn identifier + + LLM API call usage metrics including tokens, costs, quotas, and billing information + + Turn abort information including the reason for termination + + User-initiated tool invocation request with tool name and arguments + + Tool execution startup details including MCP server information when applicable + + Streaming tool execution output for incremental result display + + Tool execution progress notification with status message + + Tool execution completion results including success status, detailed output, and error + information + + Skill invocation details including content, allowed tools, and plugin metadata + + Sub-agent startup details including parent tool call and agent information + + Sub-agent completion details for successful execution + + Sub-agent failure details including error message and agent information + + Custom agent selection details including name and available tools + Empty payload; the event signals that the custom agent was deselected, returning to the default agent + + Hook invocation start details including type and input data + + Hook invocation completion details including output, success status, and error + information + + System or developer message content with role and optional template metadata + + System-generated notification for runtime events like background task completion + + Permission request notification requiring client approval with request details + + Permission request completion notification signaling UI dismissal + + User input request notification with question and optional predefined choices + + User input request completion notification signaling UI dismissal + + Structured form elicitation request with JSON schema definition for form fields + + Elicitation request completion notification signaling UI dismissal + + External tool invocation request for client-side tool execution + + External tool completion notification signaling UI dismissal + + Queued slash command dispatch request for client execution + + Queued command completion notification signaling UI dismissal + + Plan approval request with plan content and available user actions + + Plan mode exit completion notification signaling UI dismissal """ id: UUID """Unique event identifier (UUID v4), generated when the event is emitted""" diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 38616186dd..bf9564d9a2 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^0.0.421", + "@github/copilot": "^1.0.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", @@ -462,27 +462,27 @@ } }, "node_modules/@github/copilot": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.421.tgz", - "integrity": "sha512-nDUt9f5al7IgBOTc7AwLpqvaX61VsRDYDQ9D5iR0QQzHo4pgDcyOXIjXUQUKsJwObXHfh6qR+Jm1vnlbw5cacg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz", + "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "0.0.421", - "@github/copilot-darwin-x64": "0.0.421", - "@github/copilot-linux-arm64": "0.0.421", - "@github/copilot-linux-x64": "0.0.421", - "@github/copilot-win32-arm64": "0.0.421", - "@github/copilot-win32-x64": "0.0.421" + "@github/copilot-darwin-arm64": "1.0.4", + "@github/copilot-darwin-x64": "1.0.4", + "@github/copilot-linux-arm64": "1.0.4", + "@github/copilot-linux-x64": "1.0.4", + "@github/copilot-win32-arm64": "1.0.4", + "@github/copilot-win32-x64": "1.0.4" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.421.tgz", - "integrity": "sha512-S4plFsxH7W8X1gEkGNcfyKykIji4mNv8BP/GpPs2Ad84qWoJpZzfZsjrjF0BQ8mvFObWp6Ft2SZOnJzFZW1Ftw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==", "cpu": [ "arm64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.421.tgz", - "integrity": "sha512-h+Dbfq8ByAielLYIeJbjkN/9Abs6AKHFi+XuuzEy4YA9jOA42uKMFsWYwaoYH8ZLK9Y+4wagYI9UewVPnyIWPA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz", + "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.421.tgz", - "integrity": "sha512-cxlqDRR/wKfbdzd456N2h7sZOZY069wU2ycSYSmo7cC75U5DyhMGYAZwyAhvQ7UKmS5gJC/wgSgye0njuK22Xg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz", + "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.421.tgz", - "integrity": "sha512-7np5b6EEemJ3U3jnl92buJ88nlpqOAIrLaJxx3pJGrP9SVFMBD/6EAlfIQ5m5QTfs+/vIuTKWBrq1wpFVZZUcQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz", + "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==", "cpu": [ "x64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.421.tgz", - "integrity": "sha512-T6qCqOnijD5pmC0ytVsahX3bpDnXtLTgo9xFGo/BGaPEvX02ePkzcRZkfkOclkzc8QlkVji6KqZYB+qMZTliwg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz", + "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==", "cpu": [ "arm64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "0.0.421", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.421.tgz", - "integrity": "sha512-KDfy3wsRQFIcOQDdd5Mblvh+DWRq+UGbTQ34wyW36ws1BsdWkV++gk9bTkeJRsPbQ51wsJ0V/jRKEZv4uK5dTA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz", + "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 6998dc74a3..9f336dfd4c 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^0.0.421", + "@github/copilot": "^1.0.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", From df59a0ecb8ed5e8a4067a52a9483038d94b51706 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 12 Mar 2026 16:02:39 +0000 Subject: [PATCH 27/61] Add no-result permission handling for extensions (#802) --- dotnet/src/Client.cs | 11 +++++ dotnet/src/Session.cs | 4 ++ dotnet/src/Types.cs | 1 + .../test/PermissionRequestResultKindTests.cs | 2 + go/client.go | 5 ++ go/session.go | 3 ++ go/types_test.go | 2 + nodejs/docs/agent-author.md | 2 - nodejs/docs/examples.md | 17 +------ nodejs/docs/extensions.md | 2 - nodejs/src/client.ts | 7 ++- nodejs/src/extension.ts | 19 +++++--- nodejs/src/session.ts | 14 +++++- nodejs/src/types.ts | 3 +- nodejs/test/client.test.ts | 32 +++++++++++++ nodejs/test/e2e/client.test.ts | 47 ++++++++++--------- nodejs/test/extension.test.ts | 47 +++++++++++++++++++ python/copilot/client.py | 14 ++++++ python/copilot/session.py | 21 +++++---- python/copilot/types.py | 1 + python/e2e/test_client.py | 11 +++-- python/test_client.py | 24 +++++++++- 22 files changed, 224 insertions(+), 65 deletions(-) create mode 100644 nodejs/test/extension.test.ts diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 5b7474a64e..0794043d8a 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -54,6 +54,9 @@ namespace GitHub.Copilot.SDK; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { + internal const string NoResultPermissionV2ErrorMessage = + "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; + /// /// Minimum protocol version this SDK can communicate with. /// @@ -1394,8 +1397,16 @@ public async Task OnPermissionRequestV2(string sess try { var result = await session.HandlePermissionRequestAsync(permissionRequest); + if (result.Kind == new PermissionRequestResultKind("no-result")) + { + throw new InvalidOperationException(NoResultPermissionV2ErrorMessage); + } return new PermissionRequestResponseV2(result); } + catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage) + { + throw; + } catch (Exception) { return new PermissionRequestResponseV2(new PermissionRequestResult diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 324b3df6df..f1438d82bc 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -467,6 +467,10 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission }; var result = await handler(permissionRequest, invocation); + if (result.Kind == new PermissionRequestResultKind("no-result")) + { + return; + } await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result); } catch (Exception) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 633a976548..908c3e46e3 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -350,6 +350,7 @@ public class PermissionRequestResult /// "denied-by-rules" — denied by configured permission rules. /// "denied-interactively-by-user" — the user explicitly denied the request. /// "denied-no-approval-rule-and-could-not-request-from-user" — no rule matched and user approval was unavailable. + /// "no-result" — leave the pending permission request unanswered. /// /// [JsonPropertyName("kind")] diff --git a/dotnet/test/PermissionRequestResultKindTests.cs b/dotnet/test/PermissionRequestResultKindTests.cs index d0cfed6f01..ea77295e25 100644 --- a/dotnet/test/PermissionRequestResultKindTests.cs +++ b/dotnet/test/PermissionRequestResultKindTests.cs @@ -21,6 +21,7 @@ public void WellKnownKinds_HaveExpectedValues() Assert.Equal("denied-by-rules", PermissionRequestResultKind.DeniedByRules.Value); Assert.Equal("denied-no-approval-rule-and-could-not-request-from-user", PermissionRequestResultKind.DeniedCouldNotRequestFromUser.Value); Assert.Equal("denied-interactively-by-user", PermissionRequestResultKind.DeniedInteractivelyByUser.Value); + Assert.Equal("no-result", new PermissionRequestResultKind("no-result").Value); } [Fact] @@ -115,6 +116,7 @@ public void JsonRoundTrip_PreservesAllKinds() PermissionRequestResultKind.DeniedByRules, PermissionRequestResultKind.DeniedCouldNotRequestFromUser, PermissionRequestResultKind.DeniedInteractivelyByUser, + new PermissionRequestResultKind("no-result"), }; foreach (var kind in kinds) diff --git a/go/client.go b/go/client.go index 021de2b142..af1ce590e5 100644 --- a/go/client.go +++ b/go/client.go @@ -51,6 +51,8 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) +const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server" + // Client manages the connection to the Copilot CLI server and provides session management. // // The Client can either spawn a CLI server process or connect to an existing server. @@ -1531,6 +1533,9 @@ func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permission }, }, nil } + if result.Kind == "no-result" { + return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error} + } return &permissionResponseV2{Result: result}, nil } diff --git a/go/session.go b/go/session.go index 74529c5235..8358ea7c02 100644 --- a/go/session.go +++ b/go/session.go @@ -562,6 +562,9 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) return } + if result.Kind == "no-result" { + return + } s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{ RequestID: requestID, diff --git a/go/types_test.go b/go/types_test.go index 190cd913d8..80b0cc5458 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -15,6 +15,7 @@ func TestPermissionRequestResultKind_Constants(t *testing.T) { {"DeniedByRules", PermissionRequestResultKindDeniedByRules, "denied-by-rules"}, {"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "denied-no-approval-rule-and-could-not-request-from-user"}, {"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "denied-interactively-by-user"}, + {"NoResult", PermissionRequestResultKind("no-result"), "no-result"}, } for _, tt := range tests { @@ -42,6 +43,7 @@ func TestPermissionRequestResult_JSONRoundTrip(t *testing.T) { {"DeniedByRules", PermissionRequestResultKindDeniedByRules}, {"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser}, {"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser}, + {"NoResult", PermissionRequestResultKind("no-result")}, {"Custom", PermissionRequestResultKind("custom")}, } diff --git a/nodejs/docs/agent-author.md b/nodejs/docs/agent-author.md index 4c1e32f69f..8b3d935939 100644 --- a/nodejs/docs/agent-author.md +++ b/nodejs/docs/agent-author.md @@ -59,11 +59,9 @@ Discovery rules: ## Minimal Skeleton ```js -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; await joinSession({ - onPermissionRequest: approveAll, // Required — handle permission requests tools: [], // Optional — custom tools hooks: {}, // Optional — lifecycle hooks }); diff --git a/nodejs/docs/examples.md b/nodejs/docs/examples.md index a5b03f87ea..1461a2f392 100644 --- a/nodejs/docs/examples.md +++ b/nodejs/docs/examples.md @@ -7,11 +7,9 @@ A practical guide to writing extensions using the `@github/copilot-sdk` extensio Every extension starts with the same boilerplate: ```js -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; const session = await joinSession({ - onPermissionRequest: approveAll, hooks: { /* ... */ }, tools: [ /* ... */ ], }); @@ -33,7 +31,6 @@ Use `session.log()` to surface messages to the user in the CLI timeline: ```js const session = await joinSession({ - onPermissionRequest: approveAll, hooks: { onSessionStart: async () => { await session.log("My extension loaded"); @@ -383,7 +380,6 @@ function copyToClipboard(text) { } const session = await joinSession({ - onPermissionRequest: approveAll, hooks: { onUserPromptSubmitted: async (input) => { if (/\\bcopy\\b/i.test(input.prompt)) { @@ -425,15 +421,12 @@ Correlate `tool.execution_start` / `tool.execution_complete` events by `toolCall ```js import { existsSync, watchFile, readFileSync } from "node:fs"; import { join } from "node:path"; -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; const agentEdits = new Set(); // toolCallIds for in-flight agent edits const recentAgentPaths = new Set(); // paths recently written by the agent -const session = await joinSession({ - onPermissionRequest: approveAll, -}); +const session = await joinSession(); const workspace = session.workspacePath; // e.g. ~/.copilot/session-state/ if (workspace) { @@ -480,14 +473,11 @@ Filter out agent edits by tracking `tool.execution_start` / `tool.execution_comp ```js import { watch, readFileSync, statSync } from "node:fs"; import { join, relative, resolve } from "node:path"; -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; const agentEditPaths = new Set(); -const session = await joinSession({ - onPermissionRequest: approveAll, -}); +const session = await joinSession(); const cwd = process.cwd(); const IGNORE = new Set(["node_modules", ".git", "dist"]); @@ -582,7 +572,6 @@ Register `onUserInputRequest` to enable the agent's `ask_user` tool: ```js const session = await joinSession({ - onPermissionRequest: approveAll, onUserInputRequest: async (request) => { // request.question has the agent's question // request.choices has the options (if multiple choice) @@ -599,7 +588,6 @@ An extension that combines tools, hooks, and events. ```js import { execFile, exec } from "node:child_process"; -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; const isWindows = process.platform === "win32"; @@ -617,7 +605,6 @@ function openInEditor(filePath) { } const session = await joinSession({ - onPermissionRequest: approveAll, hooks: { onUserPromptSubmitted: async (input) => { if (/\\bcopy this\\b/i.test(input.prompt)) { diff --git a/nodejs/docs/extensions.md b/nodejs/docs/extensions.md index 5eff9135b7..8b36de8a50 100644 --- a/nodejs/docs/extensions.md +++ b/nodejs/docs/extensions.md @@ -39,11 +39,9 @@ Extensions add custom tools, hooks, and behaviors to the Copilot CLI. They run a Extensions use `@github/copilot-sdk` for all interactions with the CLI: ```js -import { approveAll } from "@github/copilot-sdk"; import { joinSession } from "@github/copilot-sdk/extension"; const session = await joinSession({ - onPermissionRequest: approveAll, tools: [ /* ... */ ], diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 954d88b599..c96d4b6912 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -25,7 +25,7 @@ import { } from "vscode-jsonrpc/node.js"; import { createServerRpc } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; -import { CopilotSession } from "./session.js"; +import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; import type { ConnectionState, CopilotClientOptions, @@ -1604,7 +1604,10 @@ export class CopilotClient { try { const result = await session._handlePermissionRequestV2(params.permissionRequest); return { result }; - } catch (_error) { + } catch (error) { + if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { + throw error; + } return { result: { kind: "denied-no-approval-rule-and-could-not-request-from-user", diff --git a/nodejs/src/extension.ts b/nodejs/src/extension.ts index 0a9b7b05d6..b7c2da3a84 100644 --- a/nodejs/src/extension.ts +++ b/nodejs/src/extension.ts @@ -4,7 +4,15 @@ import { CopilotClient } from "./client.js"; import type { CopilotSession } from "./session.js"; -import type { ResumeSessionConfig } from "./types.js"; +import type { PermissionHandler, PermissionRequestResult, ResumeSessionConfig } from "./types.js"; + +const defaultJoinSessionPermissionHandler: PermissionHandler = (): PermissionRequestResult => ({ + kind: "no-result", +}); + +export type JoinSessionConfig = Omit & { + onPermissionRequest?: PermissionHandler; +}; /** * Joins the current foreground session. @@ -14,16 +22,12 @@ import type { ResumeSessionConfig } from "./types.js"; * * @example * ```typescript - * import { approveAll } from "@github/copilot-sdk"; * import { joinSession } from "@github/copilot-sdk/extension"; * - * const session = await joinSession({ - * onPermissionRequest: approveAll, - * tools: [myTool], - * }); + * const session = await joinSession({ tools: [myTool] }); * ``` */ -export async function joinSession(config: ResumeSessionConfig): Promise { +export async function joinSession(config: JoinSessionConfig = {}): Promise { const sessionId = process.env.SESSION_ID; if (!sessionId) { throw new Error( @@ -34,6 +38,7 @@ export async function joinSession(config: ResumeSessionConfig): Promise; @@ -400,6 +403,9 @@ export class CopilotSession { const result = await this.permissionHandler!(permissionRequest, { sessionId: this.sessionId, }); + if (result.kind === "no-result") { + return; + } await this.rpc.permissions.handlePendingPermissionRequest({ requestId, result }); } catch (_error) { try { @@ -505,8 +511,14 @@ export class CopilotSession { const result = await this.permissionHandler(request as PermissionRequest, { sessionId: this.sessionId, }); + if (result.kind === "no-result") { + throw new Error(NO_RESULT_PERMISSION_V2_ERROR); + } return result; - } catch (_error) { + } catch (error) { + if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { + throw error; + } return { kind: "denied-no-approval-rule-and-could-not-request-from-user" }; } } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 99b9af75cc..cbc8b10ed1 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -241,7 +241,8 @@ export interface PermissionRequest { import type { SessionPermissionsHandlePendingPermissionRequestParams } from "./generated/rpc.js"; export type PermissionRequestResult = - SessionPermissionsHandlePendingPermissionRequestParams["result"]; + | SessionPermissionsHandlePendingPermissionRequestParams["result"] + | { kind: "no-result" }; export type PermissionHandler = ( request: PermissionRequest, diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 7206c903b3..6f3e4ef98e 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -26,6 +26,38 @@ describe("CopilotClient", () => { ); }); + it("does not respond to v3 permission requests when handler returns no-result", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ + onPermissionRequest: () => ({ kind: "no-result" }), + }); + const spy = vi.spyOn(session.rpc.permissions, "handlePendingPermissionRequest"); + + await (session as any)._executePermissionAndRespond("request-1", { kind: "write" }); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("throws when a v2 permission handler returns no-result", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ + onPermissionRequest: () => ({ kind: "no-result" }), + }); + + await expect( + (client as any).handlePermissionRequestV2({ + sessionId: session.sessionId, + permissionRequest: { kind: "write" }, + }) + ).rejects.toThrow(/protocol v2 server/); + }); + it("forwards clientName in session.create request", async () => { const client = new CopilotClient(); await client.start(); diff --git a/nodejs/test/e2e/client.test.ts b/nodejs/test/e2e/client.test.ts index 9d71ee726a..594607cd13 100644 --- a/nodejs/test/e2e/client.test.ts +++ b/nodejs/test/e2e/client.test.ts @@ -43,27 +43,32 @@ describe("Client", () => { expect(client.getState()).toBe("disconnected"); }); - it.skipIf(process.platform === "darwin")("should return errors on failed cleanup", async () => { - // Use TCP mode to avoid stdin stream destruction issues - // Without this, on macOS there are intermittent test failures - // saying "Cannot call write after a stream was destroyed" - // because the JSON-RPC logic is still trying to write to stdin after - // the process has exited. - const client = new CopilotClient({ useStdio: false }); - - await client.createSession({ onPermissionRequest: approveAll }); - - // Kill the server processto force cleanup to fail - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const cliProcess = (client as any).cliProcess as ChildProcess; - expect(cliProcess).toBeDefined(); - cliProcess.kill("SIGKILL"); - await new Promise((resolve) => setTimeout(resolve, 100)); - - const errors = await client.stop(); - expect(errors.length).toBeGreaterThan(0); - expect(errors[0].message).toContain("Failed to disconnect session"); - }); + it.skipIf(process.platform === "darwin")( + "should stop cleanly when the server exits during cleanup", + async () => { + // Use TCP mode to avoid stdin stream destruction issues + // Without this, on macOS there are intermittent test failures + // saying "Cannot call write after a stream was destroyed" + // because the JSON-RPC logic is still trying to write to stdin after + // the process has exited. + const client = new CopilotClient({ useStdio: false }); + + await client.createSession({ onPermissionRequest: approveAll }); + + // Kill the server processto force cleanup to fail + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cliProcess = (client as any).cliProcess as ChildProcess; + expect(cliProcess).toBeDefined(); + cliProcess.kill("SIGKILL"); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const errors = await client.stop(); + expect(client.getState()).toBe("disconnected"); + if (errors.length > 0) { + expect(errors[0].message).toContain("Failed to disconnect session"); + } + } + ); it("should forceStop without cleanup", async () => { const client = new CopilotClient({}); diff --git a/nodejs/test/extension.test.ts b/nodejs/test/extension.test.ts new file mode 100644 index 0000000000..d9fcf8dfdb --- /dev/null +++ b/nodejs/test/extension.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CopilotClient } from "../src/client.js"; +import { approveAll } from "../src/index.js"; +import { joinSession } from "../src/extension.js"; + +describe("joinSession", () => { + const originalSessionId = process.env.SESSION_ID; + + afterEach(() => { + if (originalSessionId === undefined) { + delete process.env.SESSION_ID; + } else { + process.env.SESSION_ID = originalSessionId; + } + vi.restoreAllMocks(); + }); + + it("defaults onPermissionRequest to no-result", async () => { + process.env.SESSION_ID = "session-123"; + const resumeSession = vi + .spyOn(CopilotClient.prototype, "resumeSession") + .mockResolvedValue({} as any); + + await joinSession({ tools: [] }); + + const [, config] = resumeSession.mock.calls[0]!; + expect(config.onPermissionRequest).toBeDefined(); + const result = await Promise.resolve( + config.onPermissionRequest!({ kind: "write" }, { sessionId: "session-123" }) + ); + expect(result).toEqual({ kind: "no-result" }); + expect(config.disableResume).toBe(true); + }); + + it("preserves an explicit onPermissionRequest handler", async () => { + process.env.SESSION_ID = "session-123"; + const resumeSession = vi + .spyOn(CopilotClient.prototype, "resumeSession") + .mockResolvedValue({} as any); + + await joinSession({ onPermissionRequest: approveAll, disableResume: false }); + + const [, config] = resumeSession.mock.calls[0]!; + expect(config.onPermissionRequest).toBe(approveAll); + expect(config.disableResume).toBe(false); + }); +}); diff --git a/python/copilot/client.py b/python/copilot/client.py index df09a755b8..a7b558ad57 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -50,6 +50,10 @@ ToolResult, ) +NO_RESULT_PERMISSION_V2_ERROR = ( + "Permission handlers cannot return 'no-result' when connected to a protocol v2 server." +) + # Minimum protocol version this SDK can communicate with. # Servers reporting a version below this are rejected. MIN_PROTOCOL_VERSION = 2 @@ -1660,6 +1664,8 @@ async def _handle_permission_request_v2(self, params: dict) -> dict: try: perm_request = PermissionRequest.from_dict(permission_request) result = await session._handle_permission_request(perm_request) + if result.kind == "no-result": + raise ValueError(NO_RESULT_PERMISSION_V2_ERROR) result_payload: dict = {"kind": result.kind} if result.rules is not None: result_payload["rules"] = result.rules @@ -1670,6 +1676,14 @@ async def _handle_permission_request_v2(self, params: dict) -> dict: if result.path is not None: result_payload["path"] = result.path return {"result": result_payload} + except ValueError as exc: + if str(exc) == NO_RESULT_PERMISSION_V2_ERROR: + raise + return { + "result": { + "kind": "denied-no-approval-rule-and-could-not-request-from-user", + } + } except Exception: # pylint: disable=broad-except return { "result": { diff --git a/python/copilot/session.py b/python/copilot/session.py index ee46cbd7b5..b4ae210df5 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -139,15 +139,16 @@ async def send(self, options: MessageOptions) -> str: ... "attachments": [{"type": "file", "path": "./src/main.py"}] ... }) """ - response = await self._client.request( - "session.send", - { - "sessionId": self.session_id, - "prompt": options["prompt"], - "attachments": options.get("attachments"), - "mode": options.get("mode"), - }, - ) + params: dict[str, Any] = { + "sessionId": self.session_id, + "prompt": options["prompt"], + } + if "attachments" in options: + params["attachments"] = options["attachments"] + if "mode" in options: + params["mode"] = options["mode"] + + response = await self._client.request("session.send", params) return response["messageId"] async def send_and_wait( @@ -387,6 +388,8 @@ async def _execute_permission_and_respond( result = await result result = cast(PermissionRequestResult, result) + if result.kind == "no-result": + return perm_result = SessionPermissionsHandlePendingPermissionRequestParamsResult( kind=Kind(result.kind), diff --git a/python/copilot/types.py b/python/copilot/types.py index 33764e5d17..9a397c7088 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -187,6 +187,7 @@ class SystemMessageReplaceConfig(TypedDict): "denied-by-content-exclusion-policy", "denied-no-approval-rule-and-could-not-request-from-user", "denied-interactively-by-user", + "no-result", ] diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index 1f7c76c04b..1395a3888a 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -57,11 +57,14 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): process.kill() await asyncio.sleep(0.1) - with pytest.raises(ExceptionGroup) as exc_info: + try: await client.stop() - assert len(exc_info.value.exceptions) > 0 - assert isinstance(exc_info.value.exceptions[0], StopError) - assert "Failed to disconnect session" in exc_info.value.exceptions[0].message + except ExceptionGroup as exc: + assert len(exc.exceptions) > 0 + assert isinstance(exc.exceptions[0], StopError) + assert "Failed to disconnect session" in exc.exceptions[0].message + else: + assert client.get_state() == "disconnected" finally: await client.force_stop() diff --git a/python/test_client.py b/python/test_client.py index 4a06966d4e..62ae7b1886 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,7 +6,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler, define_tool +from copilot import CopilotClient, PermissionHandler, PermissionRequestResult, define_tool from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports from e2e.testharness import CLI_PATH @@ -22,6 +22,28 @@ async def test_create_session_raises_without_permission_handler(self): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_v2_permission_adapter_rejects_no_result(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + try: + session = await client.create_session( + { + "on_permission_request": lambda request, invocation: PermissionRequestResult( + kind="no-result" + ) + } + ) + with pytest.raises(ValueError, match="protocol v2 server"): + await client._handle_permission_request_v2( + { + "sessionId": session.session_id, + "permissionRequest": {"kind": "write"}, + } + ) + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_resume_session_raises_without_permission_handler(self): client = CopilotClient({"cli_path": CLI_PATH}) From 5a4153295e91f1c55667efcdeb174bf559c3f42a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 12 Mar 2026 22:28:30 +0000 Subject: [PATCH 28/61] Remove autoRestart feature across all SDKs (#803) * Remove autoRestart feature across all SDKs The autoRestart option never worked correctly. This removes it from: - Node.js: types, client options, reconnect logic - Python: types, client options - Go: types, client options, struct field - .NET: types, clone copy, tests - Docs: setup, troubleshooting, READMEs - Agent config: docs-maintenance validation lists Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mark client as disconnected on connection close/error Instead of leaving onClose/onError as no-ops (which would leave the client in a stale 'connected' state), transition to 'disconnected' so callers fail fast or can re-start cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix numbered list after removing auto-restart step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Transition to disconnected state on unexpected process/connection death All SDKs now properly transition their connection state to 'disconnected' when the child process exits unexpectedly or the TCP connection drops: - Node.js: onClose/onError handlers in attachConnectionHandlers() - Go: onClose callback fired from readLoop() on unexpected exit - Python: on_close callback fired from _read_loop() on unexpected exit - .NET: rpc.Completion continuation sets _disconnected flag Includes unit tests for all four SDKs verifying the state transition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove .NET disconnection test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Re-add autoRestart as deprecated no-op to avoid source-breaking change Mark the option as obsolete/deprecated in Go, .NET, and TypeScript so existing consumers continue to compile without changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix go fmt alignment in Client struct Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Go onClose deadlock by running state update in goroutine The onClose callback acquires startStopMux, but Stop/ForceStop already hold that lock while waiting for readLoop to finish via wg.Wait(). Running the state update in a goroutine allows readLoop to complete, breaking the circular wait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/docs-maintenance.agent.md | 9 ++-- docs/setup/local-cli.md | 3 -- docs/troubleshooting/debugging.md | 9 +--- dotnet/README.md | 1 - dotnet/src/Client.cs | 6 +++ dotnet/src/Types.cs | 9 ++-- dotnet/test/CloneTests.cs | 4 +- go/README.md | 3 +- go/client.go | 46 ++++++++++------ go/internal/jsonrpc2/jsonrpc2.go | 14 +++++ go/internal/jsonrpc2/jsonrpc2_test.go | 69 ++++++++++++++++++++++++ go/types.go | 5 +- nodejs/README.md | 1 - nodejs/src/client.ts | 24 ++------- nodejs/src/types.ts | 3 +- nodejs/test/client.test.ts | 19 +++++++ python/README.md | 2 - python/copilot/client.py | 3 +- python/copilot/jsonrpc.py | 3 ++ python/copilot/types.py | 2 - python/test_jsonrpc.py | 62 +++++++++++++++++++++ 21 files changed, 225 insertions(+), 72 deletions(-) create mode 100644 go/internal/jsonrpc2/jsonrpc2_test.go diff --git a/.github/agents/docs-maintenance.agent.md b/.github/agents/docs-maintenance.agent.md index 9b97fecf4d..c5363e3699 100644 --- a/.github/agents/docs-maintenance.agent.md +++ b/.github/agents/docs-maintenance.agent.md @@ -122,7 +122,6 @@ Every major SDK feature should be documented. Core features include: - Client initialization and configuration - Connection modes (stdio vs TCP) - Authentication options -- Auto-start and auto-restart behavior **Session Management:** - Creating sessions @@ -342,7 +341,7 @@ cat nodejs/src/types.ts | grep -A 10 "export interface ExportSessionOptions" ``` **Must match:** -- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `autoRestart`, `env`, `githubToken`, `useLoggedInUser` +- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `env`, `githubToken`, `useLoggedInUser` - `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory` - `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `disconnect()`, `abort()`, `on()`, `once()`, `off()` - Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred` @@ -360,7 +359,7 @@ cat python/copilot/types.py | grep -A 15 "class SessionHooks" ``` **Must match (snake_case):** -- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `auto_restart`, `env`, `github_token`, `use_logged_in_user` +- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `env`, `github_token`, `use_logged_in_user` - `create_session()` config keys: `model`, `tools`, `hooks`, `system_message`, `mcp_servers`, `available_tools`, `excluded_tools`, `streaming`, `reasoning_effort`, `provider`, `infinite_sessions`, `custom_agents`, `working_directory` - `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `disconnect()`, `abort()`, `export_session()` - Hook names: `on_pre_tool_use`, `on_post_tool_use`, `on_user_prompt_submitted`, `on_session_start`, `on_session_end`, `on_error_occurred` @@ -378,7 +377,7 @@ cat go/types.go | grep -A 15 "type SessionHooks struct" ``` **Must match (PascalCase for exported):** -- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Env`, `GithubToken`, `UseLoggedInUser` +- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Env`, `GithubToken`, `UseLoggedInUser` - `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory` - `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Disconnect()`, `Abort()`, `ExportSession()` - Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred` @@ -396,7 +395,7 @@ cat dotnet/src/Types.cs | grep -A 15 "public class SessionHooks" ``` **Must match (PascalCase):** -- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Environment`, `GithubToken`, `UseLoggedInUser` +- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Environment`, `GithubToken`, `UseLoggedInUser` - `SessionConfig` properties: `Model`, `Tools`, `Hooks`, `SystemMessage`, `McpServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory` - `CopilotSession` methods: `SendAsync()`, `SendAndWaitAsync()`, `GetMessagesAsync()`, `DisposeAsync()`, `AbortAsync()`, `ExportSessionAsync()` - Hook properties: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred` diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index 188c511d45..c9074af675 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -171,9 +171,6 @@ const client = new CopilotClient({ // Set working directory cwd: "/path/to/project", - - // Auto-restart CLI if it crashes (default: true) - autoRestart: true, }); ``` diff --git a/docs/troubleshooting/debugging.md b/docs/troubleshooting/debugging.md index 4bb2616217..146d3fd5aa 100644 --- a/docs/troubleshooting/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -297,14 +297,7 @@ var client = new CopilotClient(new CopilotClientOptions copilot --server --stdio ``` -2. Enable auto-restart (enabled by default): - ```typescript - const client = new CopilotClient({ - autoRestart: true, - }); - ``` - -3. Check for port conflicts if using TCP mode: +2. Check for port conflicts if using TCP mode: ```typescript const client = new CopilotClient({ useStdio: false, diff --git a/dotnet/README.md b/dotnet/README.md index bdb3e8dab4..1321130382 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -73,7 +73,6 @@ new CopilotClient(CopilotClientOptions? options = null) - `UseStdio` - Use stdio transport instead of TCP (default: true) - `LogLevel` - Log level (default: "info") - `AutoStart` - Auto-start server (default: true) -- `AutoRestart` - Auto-restart on crash (default: true) - `Cwd` - Working directory for the CLI process - `Environment` - Environment variables to pass to the CLI process - `Logger` - `ILogger` instance for SDK logging diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 0794043d8a..f37591b481 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -66,6 +66,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable private readonly CopilotClientOptions _options; private readonly ILogger _logger; private Task? _connectionTask; + private volatile bool _disconnected; private bool _disposed; private readonly int? _optionsPort; private readonly string? _optionsHost; @@ -202,6 +203,7 @@ public Task StartAsync(CancellationToken cancellationToken = default) async Task StartCoreAsync(CancellationToken ct) { _logger.LogDebug("Starting Copilot client"); + _disconnected = false; Task result; @@ -593,6 +595,7 @@ public ConnectionState State if (_connectionTask == null) return ConnectionState.Disconnected; if (_connectionTask.IsFaulted) return ConnectionState.Error; if (!_connectionTask.IsCompleted) return ConnectionState.Connecting; + if (_disconnected) return ConnectionState.Disconnected; return ConnectionState.Connected; } } @@ -1201,6 +1204,9 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.StartListening(); + // Transition state to Disconnected if the JSON-RPC connection drops + _ = rpc.Completion.ContinueWith(_ => _disconnected = true, TaskScheduler.Default); + _rpc = new ServerRpc(rpc); return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer); diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 908c3e46e3..952309f3e4 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -50,8 +50,10 @@ protected CopilotClientOptions(CopilotClientOptions? other) { if (other is null) return; - AutoRestart = other.AutoRestart; AutoStart = other.AutoStart; +#pragma warning disable CS0618 // Obsolete member + AutoRestart = other.AutoRestart; +#pragma warning restore CS0618 CliArgs = (string[]?)other.CliArgs?.Clone(); CliPath = other.CliPath; CliUrl = other.CliUrl; @@ -99,9 +101,10 @@ protected CopilotClientOptions(CopilotClientOptions? other) /// public bool AutoStart { get; set; } = true; /// - /// Whether to automatically restart the CLI server if it exits unexpectedly. + /// Obsolete. This option has no effect. /// - public bool AutoRestart { get; set; } = true; + [Obsolete("AutoRestart has no effect and will be removed in a future release.")] + public bool AutoRestart { get; set; } /// /// Environment variables to pass to the CLI process. /// diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs index cc6e5ad56d..a0051ffbc5 100644 --- a/dotnet/test/CloneTests.cs +++ b/dotnet/test/CloneTests.cs @@ -22,7 +22,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() CliUrl = "http://localhost:8080", LogLevel = "debug", AutoStart = false, - AutoRestart = false, + Environment = new Dictionary { ["KEY"] = "value" }, GitHubToken = "ghp_test", UseLoggedInUser = false, @@ -38,7 +38,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() Assert.Equal(original.CliUrl, clone.CliUrl); Assert.Equal(original.LogLevel, clone.LogLevel); Assert.Equal(original.AutoStart, clone.AutoStart); - Assert.Equal(original.AutoRestart, clone.AutoRestart); + Assert.Equal(original.Environment, clone.Environment); Assert.Equal(original.GitHubToken, clone.GitHubToken); Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser); diff --git a/go/README.md b/go/README.md index 4cc73398c7..a13cbbaccb 100644 --- a/go/README.md +++ b/go/README.md @@ -138,7 +138,6 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `UseStdio` (bool): Use stdio transport instead of TCP (default: true) - `LogLevel` (string): Log level (default: "info") - `AutoStart` (\*bool): Auto-start server on first use (default: true). Use `Bool(false)` to disable. -- `AutoRestart` (\*bool): Auto-restart on crash (default: true). Use `Bool(false)` to disable. - `Env` ([]string): Environment variables for CLI process (default: inherits from current process) - `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CLIUrl`. @@ -174,7 +173,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec ### Helper Functions -- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart`/`AutoRestart` options +- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart` option ## Image Support diff --git a/go/client.go b/go/client.go index af1ce590e5..afd0e70c72 100644 --- a/go/client.go +++ b/go/client.go @@ -73,19 +73,19 @@ const noResultPermissionV2Error = "permission handlers cannot return 'no-result' // } // defer client.Stop() type Client struct { - options ClientOptions - process *exec.Cmd - client *jsonrpc2.Client - actualPort int - actualHost string - state ConnectionState - sessions map[string]*Session - sessionsMux sync.Mutex - isExternalServer bool - conn net.Conn // stores net.Conn for external TCP connections - useStdio bool // resolved value from options - autoStart bool // resolved value from options - autoRestart bool // resolved value from options + options ClientOptions + process *exec.Cmd + client *jsonrpc2.Client + actualPort int + actualHost string + state ConnectionState + sessions map[string]*Session + sessionsMux sync.Mutex + isExternalServer bool + conn net.Conn // stores net.Conn for external TCP connections + useStdio bool // resolved value from options + autoStart bool // resolved value from options + modelsCache []ModelInfo modelsCacheMux sync.Mutex lifecycleHandlers []SessionLifecycleHandler @@ -134,7 +134,6 @@ func NewClient(options *ClientOptions) *Client { isExternalServer: false, useStdio: true, autoStart: true, // default - autoRestart: true, // default } if options != nil { @@ -184,9 +183,6 @@ func NewClient(options *ClientOptions) *Client { if options.AutoStart != nil { client.autoStart = *options.AutoStart } - if options.AutoRestart != nil { - client.autoRestart = *options.AutoRestart - } if options.GitHubToken != "" { opts.GitHubToken = options.GitHubToken } @@ -1233,6 +1229,15 @@ func (c *Client) startCLIServer(ctx context.Context) error { // Create JSON-RPC client immediately c.client = jsonrpc2.NewClient(stdin, stdout) c.client.SetProcessDone(c.processDone, c.processErrorPtr) + c.client.SetOnClose(func() { + // Run in a goroutine to avoid deadlocking with Stop/ForceStop, + // which hold startStopMux while waiting for readLoop to finish. + go func() { + c.startStopMux.Lock() + defer c.startStopMux.Unlock() + c.state = StateDisconnected + }() + }) c.RPC = rpc.NewServerRpc(c.client) c.setupNotificationHandler() c.client.Start() @@ -1348,6 +1353,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error { if c.processDone != nil { c.client.SetProcessDone(c.processDone, c.processErrorPtr) } + c.client.SetOnClose(func() { + go func() { + c.startStopMux.Lock() + defer c.startStopMux.Unlock() + c.state = StateDisconnected + }() + }) c.RPC = rpc.NewServerRpc(c.client) c.setupNotificationHandler() c.client.Start() diff --git a/go/internal/jsonrpc2/jsonrpc2.go b/go/internal/jsonrpc2/jsonrpc2.go index 09505c06d8..827a15cb4a 100644 --- a/go/internal/jsonrpc2/jsonrpc2.go +++ b/go/internal/jsonrpc2/jsonrpc2.go @@ -61,6 +61,7 @@ type Client struct { processDone chan struct{} // closed when the underlying process exits processError error // set before processDone is closed processErrorMu sync.RWMutex // protects processError + onClose func() // called when the read loop exits unexpectedly } // NewClient creates a new JSON-RPC client @@ -293,9 +294,22 @@ func (c *Client) sendMessage(message any) error { return nil } +// SetOnClose sets a callback invoked when the read loop exits unexpectedly +// (e.g. the underlying connection or process was lost). +func (c *Client) SetOnClose(fn func()) { + c.onClose = fn +} + // readLoop reads messages from stdout in a background goroutine func (c *Client) readLoop() { defer c.wg.Done() + defer func() { + // If still running, the read loop exited unexpectedly (process died or + // connection dropped). Notify the caller so it can update its state. + if c.onClose != nil && c.running.Load() { + c.onClose() + } + }() reader := bufio.NewReader(c.stdout) diff --git a/go/internal/jsonrpc2/jsonrpc2_test.go b/go/internal/jsonrpc2/jsonrpc2_test.go new file mode 100644 index 0000000000..9f542049d5 --- /dev/null +++ b/go/internal/jsonrpc2/jsonrpc2_test.go @@ -0,0 +1,69 @@ +package jsonrpc2 + +import ( + "io" + "sync" + "testing" + "time" +) + +func TestOnCloseCalledOnUnexpectedExit(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + + client := NewClient(stdinW, stdoutR) + + var called bool + var mu sync.Mutex + client.SetOnClose(func() { + mu.Lock() + called = true + mu.Unlock() + }) + + client.Start() + + // Simulate unexpected process death by closing the stdout writer + stdoutW.Close() + + // Wait for readLoop to detect the close and invoke the callback + time.Sleep(200 * time.Millisecond) + + mu.Lock() + defer mu.Unlock() + if !called { + t.Error("expected onClose to be called when read loop exits unexpectedly") + } +} + +func TestOnCloseNotCalledOnIntentionalStop(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdoutW.Close() + + client := NewClient(stdinW, stdoutR) + + var called bool + var mu sync.Mutex + client.SetOnClose(func() { + mu.Lock() + called = true + mu.Unlock() + }) + + client.Start() + + // Intentional stop — should set running=false before closing stdout, + // so the readLoop should NOT invoke onClose. + client.Stop() + + time.Sleep(200 * time.Millisecond) + + mu.Lock() + defer mu.Unlock() + if called { + t.Error("onClose should not be called on intentional Stop()") + } +} diff --git a/go/types.go b/go/types.go index a139f294f4..bf02e0eb84 100644 --- a/go/types.go +++ b/go/types.go @@ -38,8 +38,7 @@ type ClientOptions struct { // AutoStart automatically starts the CLI server on first use (default: true). // Use Bool(false) to disable. AutoStart *bool - // AutoRestart automatically restarts the CLI server if it crashes (default: true). - // Use Bool(false) to disable. + // Deprecated: AutoRestart has no effect and will be removed in a future release. AutoRestart *bool // Env is the environment variables for the CLI process (default: inherits from current process). // Each entry is of the form "key=value". @@ -65,7 +64,7 @@ type ClientOptions struct { } // Bool returns a pointer to the given bool value. -// Use for setting AutoStart or AutoRestart: AutoStart: Bool(false) +// Use for setting AutoStart: AutoStart: Bool(false) func Bool(v bool) *bool { return &v } diff --git a/nodejs/README.md b/nodejs/README.md index 78a535b76a..13169e7b7b 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -82,7 +82,6 @@ new CopilotClient(options?: CopilotClientOptions) - `useStdio?: boolean` - Use stdio transport instead of TCP (default: true) - `logLevel?: string` - Log level (default: "info") - `autoStart?: boolean` - Auto-start server (default: true) -- `autoRestart?: boolean` - Auto-restart on crash (default: true) - `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. - `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index c96d4b6912..038b6291ff 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -243,7 +243,8 @@ export class CopilotClient { cliUrl: options.cliUrl, logLevel: options.logLevel || "debug", autoStart: options.autoStart ?? true, - autoRestart: options.autoRestart ?? true, + autoRestart: false, + env: options.env ?? process.env, githubToken: options.githubToken, // Default useLoggedInUser to false when githubToken is provided, otherwise true @@ -1259,8 +1260,6 @@ export class CopilotClient { } else { reject(new Error(`CLI server exited with code ${code}`)); } - } else if (this.options.autoRestart && this.state === "connected") { - void this.reconnect(); } }); @@ -1412,13 +1411,11 @@ export class CopilotClient { ); this.connection.onClose(() => { - if (this.state === "connected" && this.options.autoRestart) { - void this.reconnect(); - } + this.state = "disconnected"; }); this.connection.onError((_error) => { - // Connection errors are handled via autoRestart if enabled + this.state = "disconnected"; }); } @@ -1647,17 +1644,4 @@ export class CopilotClient { "resultType" in value ); } - - /** - * Attempt to reconnect to the server - */ - private async reconnect(): Promise { - this.state = "disconnected"; - try { - await this.stop(); - await this.start(); - } catch (_error) { - // Reconnection failed - } - } } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index cbc8b10ed1..dcf70bc93f 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -72,8 +72,7 @@ export interface CopilotClientOptions { autoStart?: boolean; /** - * Auto-restart the CLI server if it crashes - * @default true + * @deprecated This option has no effect and will be removed in a future release. */ autoRestart?: boolean; diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 6f3e4ef98e..c54e0fc2ca 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -516,4 +516,23 @@ describe("CopilotClient", () => { expect(models).toEqual(customModels); }); }); + + describe("unexpected disconnection", () => { + it("transitions to disconnected when child process is killed", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + expect(client.getState()).toBe("connected"); + + // Kill the child process to simulate unexpected termination + const proc = (client as any).cliProcess as import("node:child_process").ChildProcess; + proc.kill(); + + // Wait for the connection.onClose handler to fire + await vi.waitFor(() => { + expect(client.getState()).toBe("disconnected"); + }); + }); + }); }); diff --git a/python/README.md b/python/README.md index 5b87bb04e9..a585ea114b 100644 --- a/python/README.md +++ b/python/README.md @@ -84,7 +84,6 @@ client = CopilotClient({ "cli_url": None, # Optional: URL of existing server (e.g., "localhost:8080") "log_level": "info", # Optional: log level (default: "info") "auto_start": True, # Optional: auto-start server (default: True) - "auto_restart": True, # Optional: auto-restart on crash (default: True) }) await client.start() @@ -111,7 +110,6 @@ await client.stop() - `use_stdio` (bool): Use stdio transport instead of TCP (default: True) - `log_level` (str): Log level (default: "info") - `auto_start` (bool): Auto-start server on first use (default: True) -- `auto_restart` (bool): Auto-restart on crash (default: True) - `github_token` (str): GitHub token for authentication. When provided, takes priority over other auth methods. - `use_logged_in_user` (bool): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). Cannot be used with `cli_url`. diff --git a/python/copilot/client.py b/python/copilot/client.py index a7b558ad57..239c4f796e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -193,7 +193,6 @@ def __init__(self, options: CopilotClientOptions | None = None): "use_stdio": False if opts.get("cli_url") else opts.get("use_stdio", True), "log_level": opts.get("log_level", "info"), "auto_start": opts.get("auto_start", True), - "auto_restart": opts.get("auto_restart", True), "use_logged_in_user": use_logged_in_user, } if opts.get("cli_args"): @@ -1410,6 +1409,7 @@ async def _connect_via_stdio(self) -> None: # Create JSON-RPC client with the process self._client = JsonRpcClient(self._process) + self._client.on_close = lambda: setattr(self, "_state", "disconnected") self._rpc = ServerRpc(self._client) # Set up notification handler for session events @@ -1497,6 +1497,7 @@ def wait(self, timeout=None): self._process = SocketWrapper(sock_file, sock) # type: ignore self._client = JsonRpcClient(self._process) + self._client.on_close = lambda: setattr(self, "_state", "disconnected") self._rpc = ServerRpc(self._client) # Set up notification handler for session events diff --git a/python/copilot/jsonrpc.py b/python/copilot/jsonrpc.py index fc8255274d..287f1b965d 100644 --- a/python/copilot/jsonrpc.py +++ b/python/copilot/jsonrpc.py @@ -60,6 +60,7 @@ def __init__(self, process): self._process_exit_error: str | None = None self._stderr_output: list[str] = [] self._stderr_lock = threading.Lock() + self.on_close: Callable[[], None] | None = None def start(self, loop: asyncio.AbstractEventLoop | None = None): """Start listening for messages in background thread""" @@ -211,6 +212,8 @@ def _read_loop(self): # Process exited or read failed - fail all pending requests if self._running: self._fail_pending_requests() + if self.on_close is not None: + self.on_close() def _fail_pending_requests(self): """Fail all pending requests when process exits""" diff --git a/python/copilot/types.py b/python/copilot/types.py index 9a397c7088..e8b8d1d472 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -86,8 +86,6 @@ class CopilotClientOptions(TypedDict, total=False): # Mutually exclusive with cli_path, use_stdio log_level: LogLevel # Log level auto_start: bool # Auto-start the CLI server on first use (default: True) - # Auto-restart the CLI server if it crashes (default: True) - auto_restart: bool env: dict[str, str] # Environment variables for the CLI process # GitHub token to use for authentication. # When provided, the token is passed to the CLI server via environment variable. diff --git a/python/test_jsonrpc.py b/python/test_jsonrpc.py index 2533fc8a79..7c3c8dab2a 100644 --- a/python/test_jsonrpc.py +++ b/python/test_jsonrpc.py @@ -7,6 +7,9 @@ import io import json +import os +import threading +import time import pytest @@ -265,3 +268,62 @@ def test_read_message_multiple_messages_in_sequence(self): result2 = client._read_message() assert result2 == message2 + + +class ClosingStream: + """Stream that immediately returns empty bytes (simulates process death / EOF).""" + + def readline(self): + return b"" + + def read(self, n: int) -> bytes: + return b"" + + +class TestOnClose: + """Tests for the on_close callback when the read loop exits unexpectedly.""" + + def test_on_close_called_on_unexpected_exit(self): + """on_close fires when the stream closes while client is still running.""" + import asyncio + + process = MockProcess() + process.stdout = ClosingStream() + + client = JsonRpcClient(process) + + called = threading.Event() + client.on_close = lambda: called.set() + + loop = asyncio.new_event_loop() + try: + client.start(loop=loop) + assert called.wait(timeout=2), "on_close was not called within 2 seconds" + finally: + loop.close() + + def test_on_close_not_called_on_intentional_stop(self): + """on_close should not fire when stop() is called intentionally.""" + import asyncio + + r_fd, w_fd = os.pipe() + process = MockProcess() + process.stdout = os.fdopen(r_fd, "rb") + + client = JsonRpcClient(process) + + called = threading.Event() + client.on_close = lambda: called.set() + + loop = asyncio.new_event_loop() + try: + client.start(loop=loop) + + # Intentional stop sets _running = False before the thread sees EOF + loop.run_until_complete(client.stop()) + os.close(w_fd) + + time.sleep(0.5) + assert not called.is_set(), "on_close should not be called on intentional stop" + finally: + loop.close() From e1ea0086c86c7c942d5d3a754631a6072c79e96b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:38:44 +0000 Subject: [PATCH 29/61] docs: prohibit InternalsVisibleTo in .NET test guidance (#804) * Initial plan * Add .NET InternalsVisibleTo note to copilot instructions Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7c362ab537..0133053997 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,6 +25,7 @@ - Python: `cd python && uv pip install -e ".[dev]"` → `uv run pytest` (E2E tests use the test harness) - Go: `cd go && go test ./...` - .NET: `cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj` + - **.NET testing note:** Never add `InternalsVisibleTo` to any project file when writing tests. Tests must only access public APIs. ## Testing & E2E tips ⚙️ From b67e3e576d1a0d1314300fa076f470cd4f30184e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:39:23 +0000 Subject: [PATCH 30/61] Replace `Task.WhenAny` + `Task.Delay` timeout pattern with `.WaitAsync(TimeSpan)` (#805) * Initial plan * Replace Task.WhenAny+Task.Delay with .WaitAsync(TimeSpan) Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- dotnet/test/MultiClientTests.cs | 6 ++---- dotnet/test/SessionTests.cs | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dotnet/test/MultiClientTests.cs b/dotnet/test/MultiClientTests.cs index 131fd31d05..ba139337a1 100644 --- a/dotnet/test/MultiClientTests.cs +++ b/dotnet/test/MultiClientTests.cs @@ -134,11 +134,9 @@ public async Task Both_Clients_See_Tool_Request_And_Completion_Events() Assert.Contains("MAGIC_hello_42", response!.Data.Content ?? string.Empty); // Wait for all broadcast events to arrive on both clients - var timeout = Task.Delay(TimeSpan.FromSeconds(10)); - var allEvents = Task.WhenAll( + await Task.WhenAll( client1Requested.Task, client2Requested.Task, - client1Completed.Task, client2Completed.Task); - Assert.Equal(allEvents, await Task.WhenAny(allEvents, timeout)); + client1Completed.Task, client2Completed.Task).WaitAsync(TimeSpan.FromSeconds(10)); await session2.DisposeAsync(); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 8004395846..5dcda707df 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -272,8 +272,7 @@ public async Task Should_Receive_Session_Events() await session.SendAsync(new MessageOptions { Prompt = "What is 100+200?" }); // Wait for session to become idle (indicating message processing is complete) - var completed = await Task.WhenAny(idleReceived.Task, Task.Delay(TimeSpan.FromSeconds(60))); - Assert.Equal(idleReceived.Task, completed); + await idleReceived.Task.WaitAsync(TimeSpan.FromSeconds(60)); // Should have received multiple events (user message, assistant message, idle, etc.) Assert.NotEmpty(receivedEvents); From 2a67ecc02a213e045491891da8333f68b760dd07 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 13 Mar 2026 12:27:39 +0000 Subject: [PATCH 31/61] go: change LogOptions.Ephemeral from bool to *bool (#827) Previously, LogOptions.Ephemeral was a plain bool, so callers could not distinguish between 'not set' (zero value false) and an explicit false. This meant the SDK could never send ephemeral=false on the wire, always deferring to the server default. Change the field to *bool so nil means 'not set' (omitted from the JSON-RPC payload) and a non-nil value is forwarded as-is. This is consistent with how the other SDKs (Node, Python, .NET) handle the field. --- go/internal/e2e/session_test.go | 2 +- go/session.go | 11 ++++++----- go/types.go | 5 ++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 40f62d4c6d..fe23dab175 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -981,7 +981,7 @@ func TestSessionLog(t *testing.T) { }) t.Run("should log ephemeral message", func(t *testing.T) { - if err := session.Log(t.Context(), "Ephemeral message", &copilot.LogOptions{Ephemeral: true}); err != nil { + if err := session.Log(t.Context(), "Ephemeral message", &copilot.LogOptions{Ephemeral: copilot.Bool(true)}); err != nil { t.Fatalf("Log failed: %v", err) } diff --git a/go/session.go b/go/session.go index 8358ea7c02..70c07bb886 100644 --- a/go/session.go +++ b/go/session.go @@ -711,8 +711,9 @@ type LogOptions struct { // [rpc.Warning], and [rpc.Error]. Level rpc.Level // Ephemeral marks the message as transient so it is not persisted - // to the session event log on disk. - Ephemeral bool + // to the session event log on disk. When nil the server decides the + // default; set to a non-nil value to explicitly control persistence. + Ephemeral *bool } // Log sends a log message to the session timeline. @@ -730,7 +731,7 @@ type LogOptions struct { // session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.Warning}) // // // Ephemeral message (not persisted) -// session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: true}) +// session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: copilot.Bool(true)}) func (s *Session) Log(ctx context.Context, message string, opts *LogOptions) error { params := &rpc.SessionLogParams{Message: message} @@ -738,8 +739,8 @@ func (s *Session) Log(ctx context.Context, message string, opts *LogOptions) err if opts.Level != "" { params.Level = &opts.Level } - if opts.Ephemeral { - params.Ephemeral = &opts.Ephemeral + if opts.Ephemeral != nil { + params.Ephemeral = opts.Ephemeral } } diff --git a/go/types.go b/go/types.go index bf02e0eb84..733268a236 100644 --- a/go/types.go +++ b/go/types.go @@ -64,7 +64,10 @@ type ClientOptions struct { } // Bool returns a pointer to the given bool value. -// Use for setting AutoStart: AutoStart: Bool(false) +// Use for option fields such as AutoStart, AutoRestart, or LogOptions.Ephemeral: +// +// AutoStart: Bool(false) +// Ephemeral: Bool(true) func Bool(v bool) *bool { return &v } From 10c4d029fcf6e27b366336eccf3d72ae32ae9aeb Mon Sep 17 00:00:00 2001 From: Matthew Rayermann Date: Fri, 13 Mar 2026 05:48:02 -0700 Subject: [PATCH 32/61] Allow tools to set `skipPermission` (#808) --- dotnet/README.md | 18 ++++++++ dotnet/src/Client.cs | 7 ++- dotnet/src/Types.cs | 3 ++ dotnet/test/ToolsTests.cs | 36 +++++++++++++++ go/README.md | 12 +++++ go/internal/e2e/tools_test.go | 46 +++++++++++++++++++ go/types.go | 4 ++ nodejs/README.md | 13 ++++++ nodejs/src/client.ts | 2 + nodejs/src/types.ts | 5 ++ nodejs/test/e2e/tools.test.ts | 26 +++++++++++ python/README.md | 10 ++++ python/copilot/client.py | 4 ++ python/copilot/tools.py | 8 ++++ python/copilot/types.py | 1 + python/e2e/test_tools.py | 28 +++++++++++ ...kippermission_sent_in_tool_definition.yaml | 35 ++++++++++++++ 17 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 test/snapshots/tools/skippermission_sent_in_tool_definition.yaml diff --git a/dotnet/README.md b/dotnet/README.md index 1321130382..441904de7a 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -448,6 +448,24 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +#### Skipping Permission Prompts + +Set `skip_permission` in the tool's `AdditionalProperties` to allow it to execute without triggering a permission prompt: + +```csharp +var safeLookup = AIFunctionFactory.Create( + async ([Description("Lookup ID")] string id) => { + // your logic + }, + "safe_lookup", + "A read-only lookup that needs no confirmation", + new AIFunctionFactoryOptions + { + AdditionalProperties = new ReadOnlyDictionary( + new Dictionary { ["skip_permission"] = true }) + }); +``` + ### System Message Customization Control the system prompt using `SystemMessage` in session config: diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f37591b481..55491bd963 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1476,13 +1476,16 @@ internal record ToolDefinition( string Name, string? Description, JsonElement Parameters, /* JSON schema */ - bool? OverridesBuiltInTool = null) + bool? OverridesBuiltInTool = null, + bool? SkipPermission = null) { public static ToolDefinition FromAIFunction(AIFunction function) { var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true; + var skipPerm = function.AdditionalProperties.TryGetValue("skip_permission", out var skipVal) && skipVal is true; return new ToolDefinition(function.Name, function.Description, function.JsonSchema, - overrides ? true : null); + overrides ? true : null, + skipPerm ? true : null); } } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 952309f3e4..cdc0818054 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -286,6 +286,9 @@ public class ToolInvocation /// Gets the kind indicating the permission was denied interactively by the user. public static PermissionRequestResultKind DeniedInteractivelyByUser { get; } = new("denied-interactively-by-user"); + /// Gets the kind indicating the permission was denied interactively by the user. + public static PermissionRequestResultKind NoResult { get; } = new("no-result"); + /// Gets the underlying string value of this . public string Value => _value ?? string.Empty; diff --git a/dotnet/test/ToolsTests.cs b/dotnet/test/ToolsTests.cs index 0956598895..c2350cbffe 100644 --- a/dotnet/test/ToolsTests.cs +++ b/dotnet/test/ToolsTests.cs @@ -181,6 +181,42 @@ static string CustomGrep([Description("Search query")] string query) => $"CUSTOM_GREP_RESULT: {query}"; } + [Fact] + public async Task SkipPermission_Sent_In_Tool_Definition() + { + [Description("A tool that skips permission")] + static string SafeLookup([Description("Lookup ID")] string id) + => $"RESULT: {id}"; + + var tool = AIFunctionFactory.Create((Delegate)SafeLookup, new AIFunctionFactoryOptions + { + Name = "safe_lookup", + AdditionalProperties = new ReadOnlyDictionary( + new Dictionary { ["skip_permission"] = true }) + }); + + var didRunPermissionRequest = false; + var session = await CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = (_, _) => + { + didRunPermissionRequest = true; + return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult }); + } + }); + + await session.SendAsync(new MessageOptions + { + Prompt = "Use safe_lookup to look up 'test123'" + }); + + var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session); + Assert.NotNull(assistantMessage); + Assert.Contains("RESULT", assistantMessage!.Data.Content ?? string.Empty); + Assert.False(didRunPermissionRequest); + } + [Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")] public async Task Can_Return_Binary_Result() { diff --git a/go/README.md b/go/README.md index a13cbbaccb..060acc61b5 100644 --- a/go/README.md +++ b/go/README.md @@ -280,6 +280,18 @@ editFile := copilot.DefineTool("edit_file", "Custom file editor with project-spe editFile.OverridesBuiltInTool = true ``` +#### Skipping Permission Prompts + +Set `SkipPermission = true` on a tool to allow it to execute without triggering a permission prompt: + +```go +safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs no confirmation", + func(params LookupParams, inv copilot.ToolInvocation) (any, error) { + // your logic + }) +safeLookup.SkipPermission = true +``` + ## Streaming Enable streaming to receive assistant response chunks as they're generated: diff --git a/go/internal/e2e/tools_test.go b/go/internal/e2e/tools_test.go index 83f3780c15..c9676363f1 100644 --- a/go/internal/e2e/tools_test.go +++ b/go/internal/e2e/tools_test.go @@ -264,6 +264,52 @@ func TestTools(t *testing.T) { } }) + t.Run("skipPermission sent in tool definition", func(t *testing.T) { + ctx.ConfigureForTest(t) + + type LookupParams struct { + ID string `json:"id" jsonschema:"ID to look up"` + } + + safeLookupTool := copilot.DefineTool("safe_lookup", "A safe lookup that skips permission", + func(params LookupParams, inv copilot.ToolInvocation) (string, error) { + return "RESULT: " + params.ID, nil + }) + safeLookupTool.SkipPermission = true + + didRunPermissionRequest := false + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + didRunPermissionRequest = true + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + }, + Tools: []copilot.Tool{ + safeLookupTool, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use safe_lookup to look up 'test123'"}) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + answer, err := testharness.GetFinalAssistantMessage(t.Context(), session) + if err != nil { + t.Fatalf("Failed to get assistant message: %v", err) + } + + if answer.Data.Content == nil || !strings.Contains(*answer.Data.Content, "RESULT: test123") { + t.Errorf("Expected answer to contain 'RESULT: test123', got %v", answer.Data.Content) + } + + if didRunPermissionRequest { + t.Errorf("Expected permission handler to NOT be called for skipPermission tool") + } + }) + t.Run("overrides built-in tool with custom tool", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/types.go b/go/types.go index 733268a236..3ccbd0cc91 100644 --- a/go/types.go +++ b/go/types.go @@ -125,6 +125,9 @@ const ( // PermissionRequestResultKindDeniedInteractivelyByUser indicates the permission was denied interactively by the user. PermissionRequestResultKindDeniedInteractivelyByUser PermissionRequestResultKind = "denied-interactively-by-user" + + // PermissionRequestResultKindNoResult indicates no permission decision was made. + PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result" ) // PermissionRequestResult represents the result of a permission request @@ -416,6 +419,7 @@ type Tool struct { Description string `json:"description,omitempty"` Parameters map[string]any `json:"parameters,omitempty"` OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"` + SkipPermission bool `json:"skipPermission,omitempty"` Handler ToolHandler `json:"-"` } diff --git a/nodejs/README.md b/nodejs/README.md index 13169e7b7b..dc2acaf3e9 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -425,6 +425,19 @@ defineTool("edit_file", { }) ``` +#### Skipping Permission Prompts + +Set `skipPermission: true` on a tool definition to allow it to execute without triggering a permission prompt: + +```ts +defineTool("safe_lookup", { + description: "A read-only lookup that needs no confirmation", + parameters: z.object({ id: z.string() }), + skipPermission: true, + handler: async ({ id }) => { /* your logic */ }, +}) +``` + ### System Message Customization Control the system prompt using `systemMessage` in session config: diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 038b6291ff..783177c950 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -581,6 +581,7 @@ export class CopilotClient { description: tool.description, parameters: toJsonSchema(tool.parameters), overridesBuiltInTool: tool.overridesBuiltInTool, + skipPermission: tool.skipPermission, })), systemMessage: config.systemMessage, availableTools: config.availableTools, @@ -683,6 +684,7 @@ export class CopilotClient { description: tool.description, parameters: toJsonSchema(tool.parameters), overridesBuiltInTool: tool.overridesBuiltInTool, + skipPermission: tool.skipPermission, })), provider: config.provider, requestPermission: true, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index dcf70bc93f..c7756a21c0 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -166,6 +166,10 @@ export interface Tool { * will return an error. */ overridesBuiltInTool?: boolean; + /** + * When true, the tool can execute without a permission prompt. + */ + skipPermission?: boolean; } /** @@ -179,6 +183,7 @@ export function defineTool( parameters?: ZodSchema | Record; handler: ToolHandler; overridesBuiltInTool?: boolean; + skipPermission?: boolean; } ): Tool { return { name, ...config }; diff --git a/nodejs/test/e2e/tools.test.ts b/nodejs/test/e2e/tools.test.ts index 3f5c3e09fb..83d7336862 100644 --- a/nodejs/test/e2e/tools.test.ts +++ b/nodejs/test/e2e/tools.test.ts @@ -159,6 +159,32 @@ describe("Custom tools", async () => { expect(customToolRequests[0].toolName).toBe("encrypt_string"); }); + it("skipPermission sent in tool definition", async () => { + let didRunPermissionRequest = false; + const session = await client.createSession({ + onPermissionRequest: () => { + didRunPermissionRequest = true; + return { kind: "no-result" }; + }, + tools: [ + defineTool("safe_lookup", { + description: "A safe lookup that skips permission", + parameters: z.object({ + id: z.string().describe("ID to look up"), + }), + handler: ({ id }) => `RESULT: ${id}`, + skipPermission: true, + }), + ], + }); + + const assistantMessage = await session.sendAndWait({ + prompt: "Use safe_lookup to look up 'test123'", + }); + expect(assistantMessage?.data.content).toContain("RESULT: test123"); + expect(didRunPermissionRequest).toBe(false); + }); + it("overrides built-in tool with custom tool", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, diff --git a/python/README.md b/python/README.md index a585ea114b..497c92d93f 100644 --- a/python/README.md +++ b/python/README.md @@ -230,6 +230,16 @@ async def edit_file(params: EditFileParams) -> str: # your logic ``` +#### Skipping Permission Prompts + +Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt: + +```python +@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True) +async def safe_lookup(params: LookupParams) -> str: + # your logic +``` + ## Image Support The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: diff --git a/python/copilot/client.py b/python/copilot/client.py index 239c4f796e..815faf0e93 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -506,6 +506,8 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: definition["parameters"] = tool.parameters if tool.overrides_built_in_tool: definition["overridesBuiltInTool"] = True + if tool.skip_permission: + definition["skipPermission"] = True tool_defs.append(definition) payload: dict[str, Any] = {} @@ -696,6 +698,8 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> definition["parameters"] = tool.parameters if tool.overrides_built_in_tool: definition["overridesBuiltInTool"] = True + if tool.skip_permission: + definition["skipPermission"] = True tool_defs.append(definition) payload: dict[str, Any] = {"sessionId": session_id} diff --git a/python/copilot/tools.py b/python/copilot/tools.py index 573992cd52..58e58d97e2 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -26,6 +26,7 @@ def define_tool( *, description: str | None = None, overrides_built_in_tool: bool = False, + skip_permission: bool = False, ) -> Callable[[Callable[..., Any]], Tool]: ... @@ -37,6 +38,7 @@ def define_tool( handler: Callable[[T, ToolInvocation], R], params_type: type[T], overrides_built_in_tool: bool = False, + skip_permission: bool = False, ) -> Tool: ... @@ -47,6 +49,7 @@ def define_tool( handler: Callable[[Any, ToolInvocation], Any] | None = None, params_type: type[BaseModel] | None = None, overrides_built_in_tool: bool = False, + skip_permission: bool = False, ) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]: """ Define a tool with automatic JSON schema generation from Pydantic models. @@ -79,6 +82,10 @@ def lookup_issue(params: LookupIssueParams) -> str: handler: Optional handler function (if not using as decorator) params_type: Optional Pydantic model type for parameters (inferred from type hints when using as decorator) + overrides_built_in_tool: When True, explicitly indicates this tool is intended + to override a built-in tool of the same name. If not set and the + name clashes with a built-in tool, the runtime will return an error. + skip_permission: When True, the tool can execute without a permission prompt. Returns: A Tool instance @@ -154,6 +161,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: parameters=schema, handler=wrapped_handler, overrides_built_in_tool=overrides_built_in_tool, + skip_permission=skip_permission, ) # If handler is provided, call decorator immediately diff --git a/python/copilot/types.py b/python/copilot/types.py index e8b8d1d472..6ac66f76cb 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -148,6 +148,7 @@ class Tool: handler: ToolHandler parameters: dict[str, Any] | None = None overrides_built_in_tool: bool = False + skip_permission: bool = False # System message configuration (discriminated union) diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index b692e3f655..9bd7abbf07 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -138,6 +138,34 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: assert "135460" in response_content.replace(",", "") assert "204356" in response_content.replace(",", "") + async def test_skippermission_sent_in_tool_definition(self, ctx: E2ETestContext): + class LookupParams(BaseModel): + id: str = Field(description="ID to look up") + + @define_tool( + "safe_lookup", + description="A safe lookup that skips permission", + skip_permission=True, + ) + def safe_lookup(params: LookupParams, invocation: ToolInvocation) -> str: + return f"RESULT: {params.id}" + + did_run_permission_request = False + + def tracking_handler(request, invocation): + nonlocal did_run_permission_request + did_run_permission_request = True + return PermissionRequestResult(kind="no-result") + + session = await ctx.client.create_session( + {"tools": [safe_lookup], "on_permission_request": tracking_handler} + ) + + await session.send({"prompt": "Use safe_lookup to look up 'test123'"}) + assistant_message = await get_final_assistant_message(session) + assert "RESULT: test123" in assistant_message.data.content + assert not did_run_permission_request + async def test_overrides_built_in_tool_with_custom_tool(self, ctx: E2ETestContext): class GrepParams(BaseModel): query: str = Field(description="Search query") diff --git a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml new file mode 100644 index 0000000000..dfdfa63fa7 --- /dev/null +++ b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml @@ -0,0 +1,35 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use safe_lookup to look up 'test123' + - role: assistant + content: I'll look up 'test123' for you. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: safe_lookup + arguments: '{"id":"test123"}' + - messages: + - role: system + content: ${system} + - role: user + content: Use safe_lookup to look up 'test123' + - role: assistant + content: I'll look up 'test123' for you. + tool_calls: + - id: toolcall_0 + type: function + function: + name: safe_lookup + arguments: '{"id":"test123"}' + - role: tool + tool_call_id: toolcall_0 + content: "RESULT: test123" + - role: assistant + content: 'The lookup for "test123" returned: RESULT: test123' From b10033921659b337a971dc76daa8fdaa8523ec75 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Mar 2026 09:33:15 -0400 Subject: [PATCH 33/61] Serialize event dispatch in .NET and Go SDKs (#791) * Serialize event dispatch in .NET and Go SDKs In .NET, StreamJsonRpc dispatches notifications concurrently on the thread pool. The old code invoked user event handlers inline from DispatchEvent, which meant handlers could run concurrently and out of order. In Go, the JSON-RPC read loop is single-threaded, so user handlers were already serialized. However, broadcast handlers (tool calls, permission requests) ran inline on the read loop, which deadlocked when a handler issued an RPC request back through the same connection. This PR decouples user handler dispatch from the transport by routing events through a channel (Go) / Channel (.NET). A single consumer goroutine/task drains the channel and invokes user handlers serially, in FIFO order. This matches the guarantees provided by the Node.js and Python SDKs (which get natural serialization from their single-threaded event loops) while fitting Go's and .NET's multi-threaded runtimes. Broadcast handlers (tool calls, permission requests) are fired as fire-and-forget directly from the dispatch entry point, outside the channel, so a stalled handler cannot block event delivery. This matches the existing Node.js (void this._executeToolAndRespond()) and Python (asyncio.ensure_future()) behavior. Go changes: - Add eventCh channel to Session; start processEvents consumer goroutine - dispatchEvent enqueues to channel and fires broadcast handler goroutine - Close channel on Disconnect to stop the consumer - Update unit tests and E2E tests for async delivery .NET changes: - Add unbounded Channel to CopilotSession; start ProcessEventsAsync consumer task in constructor - DispatchEvent enqueues to channel and fires broadcast handler task - Complete channel on DisposeAsync - Per-handler error catching via ImmutableArray iteration - Cache handler array snapshot to avoid repeated allocation - Inline broadcast error handling into HandleBroadcastEventAsync - Update Should_Receive_Session_Events test to await async delivery - Add Handler_Exception_Does_Not_Halt_Event_Delivery test - Add DisposeAsync_From_Handler_Does_Not_Deadlock test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix panic on send to closed event channel in Go SDK Protect dispatchEvent with a recover guard so that a notification arriving after Disconnect does not crash the process. Also wrap the channel close in sync.Once so Disconnect is safe to call more than once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson --- dotnet/src/Client.cs | 4 +- dotnet/src/Session.cs | 165 ++++++++++++------ dotnet/test/Harness/TestHelper.cs | 4 +- dotnet/test/MultiClientTests.cs | 8 +- dotnet/test/SessionTests.cs | 77 +++++++- go/internal/e2e/session_test.go | 24 +-- go/session.go | 85 ++++++--- go/session_test.go | 159 +++++++++++++---- ...easync_from_handler_does_not_deadlock.yaml | 10 ++ ...xception_does_not_halt_event_delivery.yaml | 10 ++ 10 files changed, 411 insertions(+), 135 deletions(-) create mode 100644 test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml create mode 100644 test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 55491bd963..fd56674b20 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -412,7 +412,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. - var session = new CopilotSession(sessionId, connection.Rpc); + var session = new CopilotSession(sessionId, connection.Rpc, _logger); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) @@ -516,7 +516,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. - var session = new CopilotSession(sessionId, connection.Rpc); + var session = new CopilotSession(sessionId, connection.Rpc, _logger); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f1438d82bc..5f83ef6a02 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -2,12 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.SDK.Rpc; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using StreamJsonRpc; +using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using GitHub.Copilot.SDK.Rpc; +using System.Threading.Channels; namespace GitHub.Copilot.SDK; @@ -52,22 +55,27 @@ namespace GitHub.Copilot.SDK; /// public sealed partial class CopilotSession : IAsyncDisposable { - /// - /// Multicast delegate used as a thread-safe, insertion-ordered handler list. - /// The compiler-generated add/remove accessors use a lock-free CAS loop over the backing field. - /// Dispatch reads the field once (inherent snapshot, no allocation). - /// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible. - /// - private event SessionEventHandler? EventHandlers; private readonly Dictionary _toolHandlers = []; private readonly JsonRpc _rpc; + private readonly ILogger _logger; + private volatile PermissionRequestHandler? _permissionHandler; private volatile UserInputHandler? _userInputHandler; + private ImmutableArray _eventHandlers = ImmutableArray.Empty; + private SessionHooks? _hooks; private readonly SemaphoreSlim _hooksLock = new(1, 1); private SessionRpc? _sessionRpc; private int _isDisposed; + /// + /// Channel that serializes event dispatch. enqueues; + /// a single background consumer () dequeues and + /// invokes handlers one at a time, preserving arrival order. + /// + private readonly Channel _eventChannel = Channel.CreateUnbounded( + new() { SingleReader = true }); + /// /// Gets the unique identifier for this session. /// @@ -93,15 +101,20 @@ public sealed partial class CopilotSession : IAsyncDisposable /// /// The unique identifier for this session. /// The JSON-RPC connection to the Copilot CLI. + /// Logger for diagnostics. /// The workspace path if infinite sessions are enabled. /// /// This constructor is internal. Use to create sessions. /// - internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = null) + internal CopilotSession(string sessionId, JsonRpc rpc, ILogger logger, string? workspacePath = null) { SessionId = sessionId; _rpc = rpc; + _logger = logger; WorkspacePath = workspacePath; + + // Start the asynchronous processing loop. + _ = ProcessEventsAsync(); } private Task InvokeRpcAsync(string method, object?[]? args, CancellationToken cancellationToken) @@ -186,7 +199,7 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca CancellationToken cancellationToken = default) { var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(60); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); AssistantMessageEvent? lastAssistantMessage = null; void Handler(SessionEvent evt) @@ -236,7 +249,9 @@ void Handler(SessionEvent evt) /// Multiple handlers can be registered and will all receive events. /// /// - /// Handler exceptions are allowed to propagate so they are not lost. + /// Handlers are invoked serially in event-arrival order on a background thread. + /// A handler will never be called concurrently with itself or with other handlers + /// on the same session. /// /// /// @@ -259,27 +274,53 @@ void Handler(SessionEvent evt) /// public IDisposable On(SessionEventHandler handler) { - EventHandlers += handler; - return new ActionDisposable(() => EventHandlers -= handler); + ImmutableInterlocked.Update(ref _eventHandlers, array => array.Add(handler)); + return new ActionDisposable(() => ImmutableInterlocked.Update(ref _eventHandlers, array => array.Remove(handler))); } /// - /// Dispatches an event to all registered handlers. + /// Enqueues an event for serial dispatch to all registered handlers. /// /// The session event to dispatch. /// - /// This method is internal. Handler exceptions are allowed to propagate so they are not lost. - /// Broadcast request events (external_tool.requested, permission.requested) are handled - /// internally before being forwarded to user handlers. + /// This method is non-blocking. Broadcast request events (external_tool.requested, + /// permission.requested) are fired concurrently so that a stalled handler does not + /// block event delivery. The event is then placed into an in-memory channel and + /// processed by a single background consumer (), + /// which guarantees user handlers see events one at a time, in order. /// internal void DispatchEvent(SessionEvent sessionEvent) { - // Handle broadcast request events (protocol v3) before dispatching to user handlers. - // Fire-and-forget: the response is sent asynchronously via RPC. - HandleBroadcastEventAsync(sessionEvent); + // Fire broadcast work concurrently (fire-and-forget with error logging). + // This is done outside the channel so broadcast handlers don't block the + // consumer loop — important when a secondary client's handler intentionally + // never completes (multi-client permission scenario). + _ = HandleBroadcastEventAsync(sessionEvent); + + // Queue the event for serial processing by user handlers. + _eventChannel.Writer.TryWrite(sessionEvent); + } - // Reading the field once gives us a snapshot; delegates are immutable. - EventHandlers?.Invoke(sessionEvent); + /// + /// Single-reader consumer loop that processes events from the channel. + /// Ensures user event handlers are invoked serially and in FIFO order. + /// + private async Task ProcessEventsAsync() + { + await foreach (var sessionEvent in _eventChannel.Reader.ReadAllAsync()) + { + foreach (var handler in _eventHandlers) + { + try + { + handler(sessionEvent); + } + catch (Exception ex) + { + LogEventHandlerError(ex); + } + } + } } /// @@ -355,37 +396,44 @@ internal async Task HandlePermissionRequestAsync(JsonEl /// Implements the protocol v3 broadcast model where tool calls and permission requests /// are broadcast as session events to all clients. /// - private async void HandleBroadcastEventAsync(SessionEvent sessionEvent) + private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) { - switch (sessionEvent) + try { - case ExternalToolRequestedEvent toolEvent: - { - var data = toolEvent.Data; - if (string.IsNullOrEmpty(data.RequestId) || string.IsNullOrEmpty(data.ToolName)) - return; - - var tool = GetTool(data.ToolName); - if (tool is null) - return; // This client doesn't handle this tool; another client will. - - await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool); - break; - } - - case PermissionRequestedEvent permEvent: - { - var data = permEvent.Data; - if (string.IsNullOrEmpty(data.RequestId) || data.PermissionRequest is null) - return; - - var handler = _permissionHandler; - if (handler is null) - return; // This client doesn't handle permissions; another client will. - - await ExecutePermissionAndRespondAsync(data.RequestId, data.PermissionRequest, handler); - break; - } + switch (sessionEvent) + { + case ExternalToolRequestedEvent toolEvent: + { + var data = toolEvent.Data; + if (string.IsNullOrEmpty(data.RequestId) || string.IsNullOrEmpty(data.ToolName)) + return; + + var tool = GetTool(data.ToolName); + if (tool is null) + return; // This client doesn't handle this tool; another client will. + + await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool); + break; + } + + case PermissionRequestedEvent permEvent: + { + var data = permEvent.Data; + if (string.IsNullOrEmpty(data.RequestId) || data.PermissionRequest is null) + return; + + var handler = _permissionHandler; + if (handler is null) + return; // This client doesn't handle permissions; another client will. + + await ExecutePermissionAndRespondAsync(data.RequestId, data.PermissionRequest, handler); + break; + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + LogBroadcastHandlerError(ex); } } @@ -707,6 +755,11 @@ public async Task LogAsync(string message, SessionLogRequestLevel? level = null, /// A task representing the dispose operation. /// /// + /// The caller should ensure the session is idle (e.g., + /// has returned) before disposing. If the session is not idle, in-flight event handlers + /// or tool handlers may observe failures. + /// + /// /// Session state on disk (conversation history, planning state, artifacts) is /// preserved, so the conversation can be resumed later by calling /// with the session ID. To @@ -735,6 +788,8 @@ public async ValueTask DisposeAsync() return; } + _eventChannel.Writer.TryComplete(); + try { await InvokeRpcAsync( @@ -749,12 +804,18 @@ await InvokeRpcAsync( // Connection is broken or closed } - EventHandlers = null; + _eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray.Empty); _toolHandlers.Clear(); _permissionHandler = null; } + [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")] + private partial void LogBroadcastHandlerError(Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in session event handler")] + private partial void LogEventHandlerError(Exception exception); + internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; diff --git a/dotnet/test/Harness/TestHelper.cs b/dotnet/test/Harness/TestHelper.cs index 6dd919bc74..a04e436569 100644 --- a/dotnet/test/Harness/TestHelper.cs +++ b/dotnet/test/Harness/TestHelper.cs @@ -10,7 +10,7 @@ public static class TestHelper CopilotSession session, TimeSpan? timeout = null) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(60)); AssistantMessageEvent? finalAssistantMessage = null; @@ -78,7 +78,7 @@ public static async Task GetNextEventOfTypeAsync( CopilotSession session, TimeSpan? timeout = null) where T : SessionEvent { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var cts = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(60)); using var subscription = session.On(evt => diff --git a/dotnet/test/MultiClientTests.cs b/dotnet/test/MultiClientTests.cs index ba139337a1..bdd264a4a5 100644 --- a/dotnet/test/MultiClientTests.cs +++ b/dotnet/test/MultiClientTests.cs @@ -109,10 +109,10 @@ public async Task Both_Clients_See_Tool_Request_And_Completion_Events() }); // Set up event waiters BEFORE sending the prompt to avoid race conditions - var client1Requested = new TaskCompletionSource(); - var client2Requested = new TaskCompletionSource(); - var client1Completed = new TaskCompletionSource(); - var client2Completed = new TaskCompletionSource(); + var client1Requested = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var client2Requested = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var client1Completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var client2Completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using var sub1 = session1.On(evt => { diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 5dcda707df..ea9d0da802 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -249,18 +249,40 @@ public async Task Should_Receive_Session_Events() // session.start is emitted during the session.create RPC; if the session // weren't registered in the sessions map before the RPC, it would be dropped. var earlyEvents = new List(); + var sessionStartReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var session = await CreateSessionAsync(new SessionConfig { - OnEvent = evt => earlyEvents.Add(evt), + OnEvent = evt => + { + earlyEvents.Add(evt); + if (evt is SessionStartEvent) + sessionStartReceived.TrySetResult(true); + }, }); + // session.start is dispatched asynchronously via the event channel; + // wait briefly for the consumer to deliver it. + var started = await Task.WhenAny(sessionStartReceived.Task, Task.Delay(TimeSpan.FromSeconds(5))); + Assert.Equal(sessionStartReceived.Task, started); Assert.Contains(earlyEvents, evt => evt is SessionStartEvent); var receivedEvents = new List(); - var idleReceived = new TaskCompletionSource(); + var idleReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var concurrentCount = 0; + var maxConcurrent = 0; session.On(evt => { + // Track concurrent handler invocations to verify serial dispatch. + var current = Interlocked.Increment(ref concurrentCount); + var seenMax = Volatile.Read(ref maxConcurrent); + if (current > seenMax) + Interlocked.CompareExchange(ref maxConcurrent, current, seenMax); + + Thread.Sleep(10); + + Interlocked.Decrement(ref concurrentCount); + receivedEvents.Add(evt); if (evt is SessionIdleEvent) { @@ -280,6 +302,9 @@ public async Task Should_Receive_Session_Events() Assert.Contains(receivedEvents, evt => evt is AssistantMessageEvent); Assert.Contains(receivedEvents, evt => evt is SessionIdleEvent); + // Events must be dispatched serially — never more than one handler invocation at a time. + Assert.Equal(1, maxConcurrent); + // Verify the assistant response contains the expected answer var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session); Assert.NotNull(assistantMessage); @@ -451,6 +476,54 @@ await WaitForAsync(() => Assert.Equal("notification", ephemeralEvent.Data.InfoType); } + [Fact] + public async Task Handler_Exception_Does_Not_Halt_Event_Delivery() + { + var session = await CreateSessionAsync(); + var eventCount = 0; + var gotIdle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + session.On(evt => + { + eventCount++; + + // Throw on the first event to verify the loop keeps going. + if (eventCount == 1) + throw new InvalidOperationException("boom"); + + if (evt is SessionIdleEvent) + gotIdle.TrySetResult(); + }); + + await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); + + await gotIdle.Task.WaitAsync(TimeSpan.FromSeconds(30)); + + // Handler saw more than just the first (throwing) event. + Assert.True(eventCount > 1); + } + + [Fact] + public async Task DisposeAsync_From_Handler_Does_Not_Deadlock() + { + var session = await CreateSessionAsync(); + var disposed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + session.On(evt => + { + if (evt is UserMessageEvent) + { + // Call DisposeAsync from within a handler — must not deadlock. + session.DisposeAsync().AsTask().ContinueWith(_ => disposed.TrySetResult()); + } + }); + + await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" }); + + // If this times out, we deadlocked. + await disposed.Task.WaitAsync(TimeSpan.FromSeconds(10)); + } + private static async Task WaitForAsync(Func condition, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index fe23dab175..4590301d04 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -589,27 +589,27 @@ func TestSession(t *testing.T) { ctx.ConfigureForTest(t) // Use OnEvent to capture events dispatched during session creation. - // session.start is emitted during the session.create RPC; if the session - // weren't registered in the sessions map before the RPC, it would be dropped. - var earlyEvents []copilot.SessionEvent + // session.start is emitted during the session.create RPC; with channel-based + // dispatch it may not have been delivered by the time CreateSession returns. + sessionStartCh := make(chan bool, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, OnEvent: func(event copilot.SessionEvent) { - earlyEvents = append(earlyEvents, event) + if event.Type == "session.start" { + select { + case sessionStartCh <- true: + default: + } + } }, }) if err != nil { t.Fatalf("Failed to create session: %v", err) } - hasSessionStart := false - for _, evt := range earlyEvents { - if evt.Type == "session.start" { - hasSessionStart = true - break - } - } - if !hasSessionStart { + select { + case <-sessionStartCh: + case <-time.After(5 * time.Second): t.Error("Expected session.start event via OnEvent during creation") } diff --git a/go/session.go b/go/session.go index 70c07bb886..2205ecb111 100644 --- a/go/session.go +++ b/go/session.go @@ -65,6 +65,11 @@ type Session struct { hooks *SessionHooks hooksMux sync.RWMutex + // eventCh serializes user event handler dispatch. dispatchEvent enqueues; + // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. + eventCh chan SessionEvent + closeOnce sync.Once // guards eventCh close so Disconnect is safe to call more than once + // RPC provides typed session-scoped RPC methods. RPC *rpc.SessionRpc } @@ -78,14 +83,17 @@ func (s *Session) WorkspacePath() string { // newSession creates a new session wrapper with the given session ID and client. func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string) *Session { - return &Session{ + s := &Session{ SessionID: sessionID, workspacePath: workspacePath, client: client, handlers: make([]sessionHandler, 0), toolHandlers: make(map[string]ToolHandler), + eventCh: make(chan SessionEvent, 128), RPC: rpc.NewSessionRpc(client, sessionID), } + go s.processEvents() + return s } // Send sends a message to this session and waits for the response. @@ -435,36 +443,59 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( } } -// dispatchEvent dispatches an event to all registered handlers. -// This is an internal method; handlers are called synchronously and any panics -// are recovered to prevent crashing the event dispatcher. +// dispatchEvent enqueues an event for delivery to user handlers and fires +// broadcast handlers concurrently. +// +// Broadcast work (tool calls, permission requests) is fired in a separate +// goroutine so it does not block the JSON-RPC read loop. User event handlers +// are delivered by a single consumer goroutine (processEvents), guaranteeing +// serial, FIFO dispatch without blocking the read loop. func (s *Session) dispatchEvent(event SessionEvent) { - // Handle broadcast request events internally (fire-and-forget) - s.handleBroadcastEvent(event) + go s.handleBroadcastEvent(event) + + // Send to the event channel in a closure with a recover guard. + // Disconnect closes eventCh, and in Go sending on a closed channel + // panics — there is no non-panicking send primitive. We only want + // to suppress that specific panic; other panics are not expected here. + func() { + defer func() { recover() }() + s.eventCh <- event + }() +} - s.handlerMutex.RLock() - handlers := make([]SessionEventHandler, 0, len(s.handlers)) - for _, h := range s.handlers { - handlers = append(handlers, h.fn) - } - s.handlerMutex.RUnlock() - - for _, handler := range handlers { - // Call handler - don't let panics crash the dispatcher - func() { - defer func() { - if r := recover(); r != nil { - fmt.Printf("Error in session event handler: %v\n", r) - } +// processEvents is the single consumer goroutine for the event channel. +// It invokes user handlers serially, in arrival order. Panics in individual +// handlers are recovered so that one misbehaving handler does not prevent +// others from receiving the event. +func (s *Session) processEvents() { + for event := range s.eventCh { + s.handlerMutex.RLock() + handlers := make([]SessionEventHandler, 0, len(s.handlers)) + for _, h := range s.handlers { + handlers = append(handlers, h.fn) + } + s.handlerMutex.RUnlock() + + for _, handler := range handlers { + func() { + defer func() { + if r := recover(); r != nil { + fmt.Printf("Error in session event handler: %v\n", r) + } + }() + handler(event) }() - handler(event) - }() + } } } // handleBroadcastEvent handles broadcast request events by executing local handlers // and responding via RPC. This implements the protocol v3 broadcast model where tool // calls and permission requests are broadcast as session events to all clients. +// +// Handlers are executed in their own goroutine (not the JSON-RPC read loop or the +// event consumer loop) so that a stalled handler does not block event delivery or +// cause RPC deadlocks. func (s *Session) handleBroadcastEvent(event SessionEvent) { switch event.Type { case ExternalToolRequested: @@ -481,7 +512,7 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { if event.Data.ToolCallID != nil { toolCallID = *event.Data.ToolCallID } - go s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler) + s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler) case PermissionRequested: requestID := event.Data.RequestID @@ -492,7 +523,7 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { if handler == nil { return } - go s.executePermissionAndRespond(*requestID, *event.Data.PermissionRequest, handler) + s.executePermissionAndRespond(*requestID, *event.Data.PermissionRequest, handler) } } @@ -613,6 +644,10 @@ func (s *Session) GetMessages(ctx context.Context) ([]SessionEvent, error) { // Disconnect closes this session and releases all in-memory resources (event // handlers, tool handlers, permission handlers). // +// The caller should ensure the session is idle (e.g., [Session.SendAndWait] has +// returned) before disconnecting. If the session is not idle, in-flight event +// handlers or tool handlers may observe failures. +// // Session state on disk (conversation history, planning state, artifacts) is // preserved, so the conversation can be resumed later by calling // [Client.ResumeSession] with the session ID. To permanently remove all @@ -634,6 +669,8 @@ func (s *Session) Disconnect() error { return fmt.Errorf("failed to disconnect session: %w", err) } + s.closeOnce.Do(func() { close(s.eventCh) }) + // Clear handlers s.handlerMutex.Lock() s.handlers = nil diff --git a/go/session_test.go b/go/session_test.go index 40874a6540..664c06e558 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -2,21 +2,36 @@ package copilot import ( "sync" + "sync/atomic" "testing" + "time" ) +// newTestSession creates a session with an event channel and starts the consumer goroutine. +// Returns a cleanup function that closes the channel (stopping the consumer). +func newTestSession() (*Session, func()) { + s := &Session{ + handlers: make([]sessionHandler, 0), + eventCh: make(chan SessionEvent, 128), + } + go s.processEvents() + return s, func() { close(s.eventCh) } +} + func TestSession_On(t *testing.T) { t.Run("multiple handlers all receive events", func(t *testing.T) { - session := &Session{ - handlers: make([]sessionHandler, 0), - } + session, cleanup := newTestSession() + defer cleanup() + var wg sync.WaitGroup + wg.Add(3) var received1, received2, received3 bool - session.On(func(event SessionEvent) { received1 = true }) - session.On(func(event SessionEvent) { received2 = true }) - session.On(func(event SessionEvent) { received3 = true }) + session.On(func(event SessionEvent) { received1 = true; wg.Done() }) + session.On(func(event SessionEvent) { received2 = true; wg.Done() }) + session.On(func(event SessionEvent) { received3 = true; wg.Done() }) session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() if !received1 || !received2 || !received3 { t.Errorf("Expected all handlers to receive event, got received1=%v, received2=%v, received3=%v", @@ -25,68 +40,81 @@ func TestSession_On(t *testing.T) { }) t.Run("unsubscribing one handler does not affect others", func(t *testing.T) { - session := &Session{ - handlers: make([]sessionHandler, 0), - } + session, cleanup := newTestSession() + defer cleanup() + + var count1, count2, count3 atomic.Int32 + var wg sync.WaitGroup - var count1, count2, count3 int - session.On(func(event SessionEvent) { count1++ }) - unsub2 := session.On(func(event SessionEvent) { count2++ }) - session.On(func(event SessionEvent) { count3++ }) + wg.Add(3) + session.On(func(event SessionEvent) { count1.Add(1); wg.Done() }) + unsub2 := session.On(func(event SessionEvent) { count2.Add(1); wg.Done() }) + session.On(func(event SessionEvent) { count3.Add(1); wg.Done() }) // First event - all handlers receive it session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() // Unsubscribe handler 2 unsub2() // Second event - only handlers 1 and 3 should receive it + wg.Add(2) session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() - if count1 != 2 { - t.Errorf("Expected handler 1 to receive 2 events, got %d", count1) + if count1.Load() != 2 { + t.Errorf("Expected handler 1 to receive 2 events, got %d", count1.Load()) } - if count2 != 1 { - t.Errorf("Expected handler 2 to receive 1 event (before unsubscribe), got %d", count2) + if count2.Load() != 1 { + t.Errorf("Expected handler 2 to receive 1 event (before unsubscribe), got %d", count2.Load()) } - if count3 != 2 { - t.Errorf("Expected handler 3 to receive 2 events, got %d", count3) + if count3.Load() != 2 { + t.Errorf("Expected handler 3 to receive 2 events, got %d", count3.Load()) } }) t.Run("calling unsubscribe multiple times is safe", func(t *testing.T) { - session := &Session{ - handlers: make([]sessionHandler, 0), - } + session, cleanup := newTestSession() + defer cleanup() + + var count atomic.Int32 + var wg sync.WaitGroup - var count int - unsub := session.On(func(event SessionEvent) { count++ }) + wg.Add(1) + unsub := session.On(func(event SessionEvent) { count.Add(1); wg.Done() }) session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() - // Call unsubscribe multiple times - should not panic unsub() unsub() unsub() + // Dispatch again and wait for it to be processed via a sentinel handler + wg.Add(1) + session.On(func(event SessionEvent) { wg.Done() }) session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() - if count != 1 { - t.Errorf("Expected handler to receive 1 event, got %d", count) + if count.Load() != 1 { + t.Errorf("Expected handler to receive 1 event, got %d", count.Load()) } }) t.Run("handlers are called in registration order", func(t *testing.T) { - session := &Session{ - handlers: make([]sessionHandler, 0), - } + session, cleanup := newTestSession() + defer cleanup() var order []int - session.On(func(event SessionEvent) { order = append(order, 1) }) - session.On(func(event SessionEvent) { order = append(order, 2) }) - session.On(func(event SessionEvent) { order = append(order, 3) }) + var wg sync.WaitGroup + wg.Add(3) + session.On(func(event SessionEvent) { order = append(order, 1); wg.Done() }) + session.On(func(event SessionEvent) { order = append(order, 2); wg.Done() }) + session.On(func(event SessionEvent) { order = append(order, 3); wg.Done() }) session.dispatchEvent(SessionEvent{Type: "test"}) + wg.Wait() if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 { t.Errorf("Expected handlers to be called in order [1,2,3], got %v", order) @@ -94,9 +122,8 @@ func TestSession_On(t *testing.T) { }) t.Run("concurrent subscribe and unsubscribe is safe", func(t *testing.T) { - session := &Session{ - handlers: make([]sessionHandler, 0), - } + session, cleanup := newTestSession() + defer cleanup() var wg sync.WaitGroup for i := 0; i < 100; i++ { @@ -109,7 +136,6 @@ func TestSession_On(t *testing.T) { } wg.Wait() - // Should not panic and handlers should be empty session.handlerMutex.RLock() count := len(session.handlers) session.handlerMutex.RUnlock() @@ -118,4 +144,63 @@ func TestSession_On(t *testing.T) { t.Errorf("Expected 0 handlers after all unsubscribes, got %d", count) } }) + + t.Run("events are dispatched serially", func(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + + var concurrentCount atomic.Int32 + var maxConcurrent atomic.Int32 + var done sync.WaitGroup + const totalEvents = 5 + done.Add(totalEvents) + + session.On(func(event SessionEvent) { + current := concurrentCount.Add(1) + if current > maxConcurrent.Load() { + maxConcurrent.Store(current) + } + + time.Sleep(10 * time.Millisecond) + + concurrentCount.Add(-1) + done.Done() + }) + + for i := 0; i < totalEvents; i++ { + session.dispatchEvent(SessionEvent{Type: "test"}) + } + + done.Wait() + + if max := maxConcurrent.Load(); max != 1 { + t.Errorf("Expected max concurrent count of 1, got %d", max) + } + }) + + t.Run("handler panic does not halt delivery", func(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + + var eventCount atomic.Int32 + var done sync.WaitGroup + done.Add(2) + + session.On(func(event SessionEvent) { + count := eventCount.Add(1) + defer done.Done() + if count == 1 { + panic("boom") + } + }) + + session.dispatchEvent(SessionEvent{Type: "test"}) + session.dispatchEvent(SessionEvent{Type: "test"}) + + done.Wait() + + if eventCount.Load() != 2 { + t.Errorf("Expected 2 events dispatched, got %d", eventCount.Load()) + } + }) } diff --git a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml new file mode 100644 index 0000000000..7c4d469970 --- /dev/null +++ b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 1+1? + - role: assistant + content: 1+1 = 2 diff --git a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml new file mode 100644 index 0000000000..7c4d469970 --- /dev/null +++ b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 1+1? + - role: assistant + content: 1+1 = 2 From 05dd60eb696fa2cb009e5afa152e55fe3a45d89c Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 13 Mar 2026 06:34:13 -0700 Subject: [PATCH 34/61] [python] Change `CopilotClient.__init__` to take config objects (#793) * Change `CopilotClient.__init__` to take config objects * Improve CLI path verification in CopilotClient to use `shutil.which` * Fix changes made in `main` * Fix shutil.which error message showing None instead of original path The walrus operator reassigned cli_path to None before the f-string was evaluated, losing the original path value in the error message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove dead auto_restart field from SubprocessConfig This field was defined but never read anywhere in client.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/README.md | 48 +++-- python/copilot/__init__.py | 4 + python/copilot/client.py | 181 ++++++++---------- python/copilot/types.py | 95 +++++---- python/e2e/test_agent_and_compact_rpc.py | 12 +- python/e2e/test_client.py | 28 +-- python/e2e/test_multi_client.py | 24 +-- python/e2e/test_rpc.py | 14 +- python/e2e/test_session.py | 14 +- python/e2e/test_streaming_fidelity.py | 14 +- python/e2e/testharness/context.py | 14 +- python/test_client.py | 148 +++++++------- .../auth/byok-anthropic/python/main.py | 9 +- test/scenarios/auth/byok-azure/python/main.py | 9 +- .../scenarios/auth/byok-ollama/python/main.py | 9 +- .../scenarios/auth/byok-openai/python/main.py | 9 +- test/scenarios/auth/gh-app/python/main.py | 10 +- .../app-backend-to-server/python/main.py | 4 +- .../bundling/app-direct-server/python/main.py | 8 +- .../bundling/container-proxy/python/main.py | 8 +- .../bundling/fully-bundled/python/main.py | 10 +- test/scenarios/callbacks/hooks/python/main.py | 10 +- .../callbacks/permissions/python/main.py | 10 +- .../callbacks/user-input/python/main.py | 10 +- test/scenarios/modes/default/python/main.py | 10 +- test/scenarios/modes/minimal/python/main.py | 10 +- .../prompts/attachments/python/main.py | 10 +- .../prompts/reasoning-effort/python/main.py | 10 +- .../prompts/system-message/python/main.py | 10 +- .../concurrent-sessions/python/main.py | 10 +- .../sessions/infinite-sessions/python/main.py | 10 +- .../sessions/session-resume/python/main.py | 10 +- .../sessions/streaming/python/main.py | 10 +- .../tools/custom-agents/python/main.py | 10 +- .../tools/mcp-servers/python/main.py | 10 +- test/scenarios/tools/no-tools/python/main.py | 10 +- test/scenarios/tools/skills/python/main.py | 10 +- .../tools/tool-filtering/python/main.py | 10 +- .../tools/tool-overrides/python/main.py | 10 +- .../tools/virtual-filesystem/python/main.py | 10 +- .../transport/reconnect/python/main.py | 8 +- test/scenarios/transport/stdio/python/main.py | 10 +- test/scenarios/transport/tcp/python/main.py | 8 +- 43 files changed, 461 insertions(+), 427 deletions(-) diff --git a/python/README.md b/python/README.md index 497c92d93f..3f542fe982 100644 --- a/python/README.md +++ b/python/README.md @@ -79,12 +79,10 @@ async with await client.create_session({"model": "gpt-5"}) as session: ### CopilotClient ```python -client = CopilotClient({ - "cli_path": "copilot", # Optional: path to CLI executable - "cli_url": None, # Optional: URL of existing server (e.g., "localhost:8080") - "log_level": "info", # Optional: log level (default: "info") - "auto_start": True, # Optional: auto-start server (default: True) -}) +from copilot import CopilotClient, SubprocessConfig + +# Spawn a local CLI process (default) +client = CopilotClient() # uses bundled CLI, stdio transport await client.start() session = await client.create_session({"model": "gpt-5"}) @@ -101,17 +99,39 @@ await session.disconnect() await client.stop() ``` -**CopilotClient Options:** +```python +from copilot import CopilotClient, ExternalServerConfig -- `cli_path` (str): Path to CLI executable (default: "copilot" or `COPILOT_CLI_PATH` env var) -- `cli_url` (str): URL of existing CLI server (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process. -- `cwd` (str): Working directory for CLI process -- `port` (int): Server port for TCP mode (default: 0 for random) +# Connect to an existing CLI server +client = CopilotClient(ExternalServerConfig(url="localhost:3000")) +``` + +**CopilotClient Constructor:** + +```python +CopilotClient( + config=None, # SubprocessConfig | ExternalServerConfig | None + *, + auto_start=True, # auto-start server on first use + on_list_models=None, # custom handler for list_models() +) +``` + +**SubprocessConfig** — spawn a local CLI process: + +- `cli_path` (str | None): Path to CLI executable (default: bundled binary) +- `cli_args` (list[str]): Extra arguments for the CLI executable +- `cwd` (str | None): Working directory for CLI process (default: current dir) - `use_stdio` (bool): Use stdio transport instead of TCP (default: True) +- `port` (int): Server port for TCP mode (default: 0 for random) - `log_level` (str): Log level (default: "info") -- `auto_start` (bool): Auto-start server on first use (default: True) -- `github_token` (str): GitHub token for authentication. When provided, takes priority over other auth methods. -- `use_logged_in_user` (bool): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). Cannot be used with `cli_url`. +- `env` (dict | None): Environment variables for the CLI process +- `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. +- `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). + +**ExternalServerConfig** — connect to an existing CLI server: + +- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). **SessionConfig Options (for `create_session`):** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index f5f7ed0b18..99c14b3314 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -11,6 +11,7 @@ AzureProviderOptions, ConnectionState, CustomAgentConfig, + ExternalServerConfig, GetAuthStatusResponse, GetStatusResponse, MCPLocalServerConfig, @@ -33,6 +34,7 @@ SessionListFilter, SessionMetadata, StopError, + SubprocessConfig, Tool, ToolHandler, ToolInvocation, @@ -47,6 +49,7 @@ "CopilotSession", "ConnectionState", "CustomAgentConfig", + "ExternalServerConfig", "GetAuthStatusResponse", "GetStatusResponse", "MCPLocalServerConfig", @@ -69,6 +72,7 @@ "SessionListFilter", "SessionMetadata", "StopError", + "SubprocessConfig", "Tool", "ToolHandler", "ToolInvocation", diff --git a/python/copilot/client.py b/python/copilot/client.py index 815faf0e93..fd8b62bd04 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -16,11 +16,12 @@ import inspect import os import re +import shutil import subprocess import sys import threading import uuid -from collections.abc import Callable +from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any, cast @@ -31,8 +32,8 @@ from .session import CopilotSession from .types import ( ConnectionState, - CopilotClientOptions, CustomAgentConfig, + ExternalServerConfig, GetAuthStatusResponse, GetStatusResponse, ModelInfo, @@ -46,6 +47,7 @@ SessionListFilter, SessionMetadata, StopError, + SubprocessConfig, ToolInvocation, ToolResult, ) @@ -90,9 +92,6 @@ class CopilotClient: The client supports both stdio (default) and TCP transport modes for communication with the CLI server. - Attributes: - options: The configuration options for the client. - Example: >>> # Create a client with default options (spawns CLI server) >>> client = CopilotClient() @@ -111,100 +110,72 @@ class CopilotClient: >>> await client.stop() >>> # Or connect to an existing server - >>> client = CopilotClient({"cli_url": "localhost:3000"}) + >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) """ - def __init__(self, options: CopilotClientOptions | None = None): + def __init__( + self, + config: SubprocessConfig | ExternalServerConfig | None = None, + *, + auto_start: bool = True, + on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, + ): """ Initialize a new CopilotClient. Args: - options: Optional configuration options for the client. If not provided, - default options are used (spawns CLI server using stdio). - - Raises: - ValueError: If mutually exclusive options are provided (e.g., cli_url - with use_stdio or cli_path). + config: Connection configuration. Pass a :class:`SubprocessConfig` to + spawn a local CLI process, or an :class:`ExternalServerConfig` to + connect to an existing server. Defaults to ``SubprocessConfig()``. + auto_start: Automatically start the connection on first use + (default: ``True``). + on_list_models: Custom handler for :meth:`list_models`. When provided, + the handler is called instead of querying the CLI server. Example: - >>> # Default options - spawns CLI server using stdio + >>> # Default — spawns CLI server using stdio >>> client = CopilotClient() >>> >>> # Connect to an existing server - >>> client = CopilotClient({"cli_url": "localhost:3000"}) + >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) >>> >>> # Custom CLI path with specific log level - >>> client = CopilotClient({ - ... "cli_path": "/usr/local/bin/copilot", - ... "log_level": "debug" - ... }) + >>> client = CopilotClient(SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... log_level="debug", + ... )) """ - opts = options or {} - - # Validate mutually exclusive options - if opts.get("cli_url") and (opts.get("use_stdio") or opts.get("cli_path")): - raise ValueError("cli_url is mutually exclusive with use_stdio and cli_path") + if config is None: + config = SubprocessConfig() - # Validate auth options with external server - if opts.get("cli_url") and ( - opts.get("github_token") or opts.get("use_logged_in_user") is not None - ): - raise ValueError( - "github_token and use_logged_in_user cannot be used with cli_url " - "(external server manages its own auth)" - ) + self._config: SubprocessConfig | ExternalServerConfig = config + self._auto_start = auto_start + self._on_list_models = on_list_models - # Parse cli_url if provided + # Resolve connection-mode-specific state self._actual_host: str = "localhost" - self._is_external_server: bool = False - if opts.get("cli_url"): - self._actual_host, actual_port = self._parse_cli_url(opts["cli_url"]) + self._is_external_server: bool = isinstance(config, ExternalServerConfig) + + if isinstance(config, ExternalServerConfig): + self._actual_host, actual_port = self._parse_cli_url(config.url) self._actual_port: int | None = actual_port - self._is_external_server = True else: self._actual_port = None - # Determine CLI path: explicit option > bundled binary - # Not needed when connecting to external server via cli_url - if opts.get("cli_url"): - default_cli_path = "" # Not used for external server - elif opts.get("cli_path"): - default_cli_path = opts["cli_path"] - else: - bundled_path = _get_bundled_cli_path() - if bundled_path: - default_cli_path = bundled_path - else: - raise RuntimeError( - "Copilot CLI not found. The bundled CLI binary is not available. " - "Ensure you installed a platform-specific wheel, or provide cli_path." - ) + # Resolve CLI path: explicit > bundled binary + if config.cli_path is None: + bundled_path = _get_bundled_cli_path() + if bundled_path: + config.cli_path = bundled_path + else: + raise RuntimeError( + "Copilot CLI not found. The bundled CLI binary is not available. " + "Ensure you installed a platform-specific wheel, or provide cli_path." + ) - # Default use_logged_in_user to False when github_token is provided - github_token = opts.get("github_token") - use_logged_in_user = opts.get("use_logged_in_user") - if use_logged_in_user is None: - use_logged_in_user = False if github_token else True - - self.options: CopilotClientOptions = { - "cli_path": default_cli_path, - "cwd": opts.get("cwd", os.getcwd()), - "port": opts.get("port", 0), - "use_stdio": False if opts.get("cli_url") else opts.get("use_stdio", True), - "log_level": opts.get("log_level", "info"), - "auto_start": opts.get("auto_start", True), - "use_logged_in_user": use_logged_in_user, - } - if opts.get("cli_args"): - self.options["cli_args"] = opts["cli_args"] - if opts.get("cli_url"): - self.options["cli_url"] = opts["cli_url"] - if opts.get("env"): - self.options["env"] = opts["env"] - if github_token: - self.options["github_token"] = github_token - - self._on_list_models = opts.get("on_list_models") + # Resolve use_logged_in_user default + if config.use_logged_in_user is None: + config.use_logged_in_user = not bool(config.github_token) self._process: subprocess.Popen | None = None self._client: JsonRpcClient | None = None @@ -286,8 +257,9 @@ async def start(self) -> None: """ Start the CLI server and establish a connection. - If connecting to an external server (via cli_url), only establishes the - connection. Otherwise, spawns the CLI server process and then connects. + If connecting to an external server (via :class:`ExternalServerConfig`), + only establishes the connection. Otherwise, spawns the CLI server process + and then connects. This method is called automatically when creating a session if ``auto_start`` is True (default). @@ -296,7 +268,7 @@ async def start(self) -> None: RuntimeError: If the server fails to start or the connection fails. Example: - >>> client = CopilotClient({"auto_start": False}) + >>> client = CopilotClient(auto_start=False) >>> await client.start() >>> # Now ready to create sessions """ @@ -480,7 +452,7 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: ... }) """ if not self._client: - if self.options["auto_start"]: + if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") @@ -672,7 +644,7 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> ... }) """ if not self._client: - if self.options["auto_start"]: + if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") @@ -1289,25 +1261,30 @@ async def _start_cli_server(self) -> None: Raises: RuntimeError: If the server fails to start or times out. """ - cli_path = self.options["cli_path"] + assert isinstance(self._config, SubprocessConfig) + cfg = self._config + + cli_path = cfg.cli_path + assert cli_path is not None # resolved in __init__ # Verify CLI exists if not os.path.exists(cli_path): - raise RuntimeError(f"Copilot CLI not found at {cli_path}") + original_path = cli_path + if (cli_path := shutil.which(cli_path)) is None: + raise RuntimeError(f"Copilot CLI not found at {original_path}") # Start with user-provided cli_args, then add SDK-managed args - cli_args = self.options.get("cli_args") or [] - args = list(cli_args) + [ + args = list(cfg.cli_args) + [ "--headless", "--no-auto-update", "--log-level", - self.options["log_level"], + cfg.log_level, ] # Add auth-related flags - if self.options.get("github_token"): + if cfg.github_token: args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]) - if not self.options.get("use_logged_in_user", True): + if not cfg.use_logged_in_user: args.append("--no-auto-login") # If cli_path is a .js file, run it with node @@ -1318,21 +1295,22 @@ async def _start_cli_server(self) -> None: args = [cli_path] + args # Get environment variables - env = self.options.get("env") - if env is None: + if cfg.env is None: env = dict(os.environ) else: - env = dict(env) + env = dict(cfg.env) # Set auth token in environment if provided - if self.options.get("github_token"): - env["COPILOT_SDK_AUTH_TOKEN"] = self.options["github_token"] + if cfg.github_token: + env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token # On Windows, hide the console window to avoid distracting users in GUI apps creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 + cwd = cfg.cwd or os.getcwd() + # Choose transport mode - if self.options["use_stdio"]: + if cfg.use_stdio: args.append("--stdio") # Use regular Popen with pipes (buffering=0 for unbuffered) self._process = subprocess.Popen( @@ -1341,25 +1319,25 @@ async def _start_cli_server(self) -> None: stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, - cwd=self.options["cwd"], + cwd=cwd, env=env, creationflags=creationflags, ) else: - if self.options["port"] > 0: - args.extend(["--port", str(self.options["port"])]) + if cfg.port > 0: + args.extend(["--port", str(cfg.port)]) self._process = subprocess.Popen( args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=self.options["cwd"], + cwd=cwd, env=env, creationflags=creationflags, ) # For stdio mode, we're ready immediately - if self.options["use_stdio"]: + if cfg.use_stdio: return # For TCP mode, wait for port announcement @@ -1394,7 +1372,8 @@ async def _connect_to_server(self) -> None: Raises: RuntimeError: If the connection fails. """ - if self.options["use_stdio"]: + use_stdio = isinstance(self._config, SubprocessConfig) and self._config.use_stdio + if use_stdio: await self._connect_via_stdio() else: await self._connect_via_tcp() diff --git a/python/copilot/types.py b/python/copilot/types.py index 6ac66f76cb..4198918989 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass, field from typing import Any, Literal, NotRequired, TypedDict # Import generated SessionEvent types @@ -69,38 +69,69 @@ class SelectionAttachment(TypedDict): Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment -# Options for creating a CopilotClient -class CopilotClientOptions(TypedDict, total=False): - """Options for creating a CopilotClient""" +# Configuration for CopilotClient connection modes - cli_path: str # Path to the Copilot CLI executable (default: "copilot") - # Extra arguments to pass to the CLI executable (inserted before SDK-managed args) - cli_args: list[str] - # Working directory for the CLI process (default: current process's cwd) - cwd: str - port: int # Port for the CLI server (TCP mode only, default: 0) - use_stdio: bool # Use stdio transport instead of TCP (default: True) - cli_url: str # URL of an existing Copilot CLI server to connect to over TCP - # Format: "host:port" or "http://host:port" or just "port" (defaults to localhost) - # Examples: "localhost:8080", "http://127.0.0.1:9000", "8080" - # Mutually exclusive with cli_path, use_stdio - log_level: LogLevel # Log level - auto_start: bool # Auto-start the CLI server on first use (default: True) - env: dict[str, str] # Environment variables for the CLI process - # GitHub token to use for authentication. - # When provided, the token is passed to the CLI server via environment variable. - # This takes priority over other authentication methods. - github_token: str - # Whether to use the logged-in user for authentication. - # When True, the CLI server will attempt to use stored OAuth tokens or gh CLI auth. - # When False, only explicit tokens (github_token or environment variables) are used. - # Default: True (but defaults to False when github_token is provided) - use_logged_in_user: bool - # Custom handler for listing available models. - # When provided, client.list_models() calls this handler instead of - # querying the CLI server. Useful in BYOK mode to return models - # available from your custom provider. - on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] + +@dataclass +class SubprocessConfig: + """Config for spawning a local Copilot CLI subprocess. + + Example: + >>> config = SubprocessConfig(github_token="ghp_...") + >>> client = CopilotClient(config) + + >>> # Custom CLI path with TCP transport + >>> config = SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... use_stdio=False, + ... log_level="debug", + ... ) + """ + + cli_path: str | None = None + """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" + + cli_args: list[str] = field(default_factory=list) + """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" + + _: KW_ONLY + + cwd: str | None = None + """Working directory for the CLI process. ``None`` uses the current directory.""" + + use_stdio: bool = True + """Use stdio transport (``True``, default) or TCP (``False``).""" + + port: int = 0 + """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random.""" + + log_level: LogLevel = "info" + """Log level for the CLI process.""" + + env: dict[str, str] | None = None + """Environment variables for the CLI process. ``None`` inherits the current env.""" + + github_token: str | None = None + """GitHub token for authentication. Takes priority over other auth methods.""" + + use_logged_in_user: bool | None = None + """Use the logged-in user for authentication. + + ``None`` (default) resolves to ``True`` unless ``github_token`` is set. + """ + + +@dataclass +class ExternalServerConfig: + """Config for connecting to an existing Copilot CLI server over TCP. + + Example: + >>> config = ExternalServerConfig(url="localhost:3000") + >>> client = CopilotClient(config) + """ + + url: str + """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" ToolResultType = Literal["success", "failure", "rejected", "denied"] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index cee6814f13..6eb07f64cd 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.generated.rpc import SessionAgentSelectParams from .testharness import CLI_PATH, E2ETestContext @@ -14,7 +14,7 @@ class TestAgentSelectionRpc: @pytest.mark.asyncio async def test_should_list_available_custom_agents(self): """Test listing available custom agents via RPC.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -54,7 +54,7 @@ async def test_should_list_available_custom_agents(self): @pytest.mark.asyncio async def test_should_return_null_when_no_agent_is_selected(self): """Test getCurrent returns null when no agent is selected.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -83,7 +83,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): @pytest.mark.asyncio async def test_should_select_and_get_current_agent(self): """Test selecting an agent and verifying getCurrent returns it.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -122,7 +122,7 @@ async def test_should_select_and_get_current_agent(self): @pytest.mark.asyncio async def test_should_deselect_current_agent(self): """Test deselecting the current agent.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -156,7 +156,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): """Test listing agents returns empty when none configured.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index 1395a3888a..d7ec39dcdf 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler, StopError +from copilot import CopilotClient, PermissionHandler, StopError, SubprocessConfig from .testharness import CLI_PATH @@ -10,7 +10,7 @@ class TestClient: @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_stdio(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -27,7 +27,7 @@ async def test_should_start_and_connect_to_server_using_stdio(self): @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_tcp(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": False}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=False)) try: await client.start() @@ -46,7 +46,7 @@ async def test_should_start_and_connect_to_server_using_tcp(self): async def test_should_raise_exception_group_on_failed_cleanup(self): import asyncio - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) try: await client.create_session({"on_permission_request": PermissionHandler.approve_all}) @@ -70,7 +70,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): @pytest.mark.asyncio async def test_should_force_stop_without_cleanup(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.create_session({"on_permission_request": PermissionHandler.approve_all}) await client.force_stop() @@ -78,7 +78,7 @@ async def test_should_force_stop_without_cleanup(self): @pytest.mark.asyncio async def test_should_get_status_with_version_and_protocol_info(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -96,7 +96,7 @@ async def test_should_get_status_with_version_and_protocol_info(self): @pytest.mark.asyncio async def test_should_get_auth_status(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -114,7 +114,7 @@ async def test_should_get_auth_status(self): @pytest.mark.asyncio async def test_should_list_models_when_authenticated(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -142,7 +142,7 @@ async def test_should_list_models_when_authenticated(self): @pytest.mark.asyncio async def test_should_cache_models_list(self): """Test that list_models caches results to avoid rate limiting""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -187,11 +187,11 @@ async def test_should_cache_models_list(self): async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): """Test that CLI startup errors include stderr output in the error message.""" client = CopilotClient( - { - "cli_path": CLI_PATH, - "cli_args": ["--nonexistent-flag-for-testing"], - "use_stdio": True, - } + SubprocessConfig( + cli_path=CLI_PATH, + cli_args=["--nonexistent-flag-for-testing"], + use_stdio=True, + ) ) try: diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index caf58cd554..5131ad2bdf 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -15,8 +15,10 @@ from copilot import ( CopilotClient, + ExternalServerConfig, PermissionHandler, PermissionRequestResult, + SubprocessConfig, ToolInvocation, define_tool, ) @@ -54,15 +56,15 @@ async def setup(self): ) # Client 1 uses TCP mode so a second client can connect to the same server - opts: dict = { - "cli_path": self.cli_path, - "cwd": self.work_dir, - "env": self.get_env(), - "use_stdio": False, - } - if github_token: - opts["github_token"] = github_token - self._client1 = CopilotClient(opts) + self._client1 = CopilotClient( + SubprocessConfig( + cli_path=self.cli_path, + cwd=self.work_dir, + env=self.get_env(), + use_stdio=False, + github_token=github_token, + ) + ) # Trigger connection by creating and disconnecting an init session init_session = await self._client1.create_session( @@ -74,7 +76,7 @@ async def setup(self): actual_port = self._client1.actual_port assert actual_port is not None, "Client 1 should have an actual port after connecting" - self._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"}) + self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}")) async def teardown(self, test_failed: bool = False): if self._client2: @@ -443,7 +445,7 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Recreate client2 for future tests (but don't rejoin the session) actual_port = mctx.client1.actual_port - mctx._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"}) + mctx._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}")) # Now only stable_tool should be available await session1.send( diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 1b455d632f..0db2b4fe0a 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.generated.rpc import PingParams from .testharness import CLI_PATH, E2ETestContext @@ -14,7 +14,7 @@ class TestRpc: @pytest.mark.asyncio async def test_should_call_rpc_ping_with_typed_params(self): """Test calling rpc.ping with typed params and result""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -30,7 +30,7 @@ async def test_should_call_rpc_ping_with_typed_params(self): @pytest.mark.asyncio async def test_should_call_rpc_models_list(self): """Test calling rpc.models.list with typed result""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -53,7 +53,7 @@ async def test_should_call_rpc_models_list(self): @pytest.mark.asyncio async def test_should_call_rpc_account_get_quota(self): """Test calling rpc.account.getQuota when authenticated""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -112,7 +112,7 @@ async def test_get_and_set_session_mode(self): """Test getting and setting session mode""" from copilot.generated.rpc import Mode, SessionModeSetParams - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -148,7 +148,7 @@ async def test_read_update_and_delete_plan(self): """Test reading, updating, and deleting plan""" from copilot.generated.rpc import SessionPlanUpdateParams - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -191,7 +191,7 @@ async def test_create_list_and_read_workspace_files(self): SessionWorkspaceReadFileParams, ) - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 79fb661df1..a779fd079a 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -4,7 +4,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.types import Tool, ToolResult from .testharness import E2ETestContext, get_final_assistant_message, get_next_event_of_type @@ -194,12 +194,12 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - { - "cli_path": ctx.cli_path, - "cwd": ctx.work_dir, - "env": ctx.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=ctx.cli_path, + cwd=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) ) try: diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index d347015a03..f05b3b355e 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -4,7 +4,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from .testharness import E2ETestContext @@ -77,12 +77,12 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - { - "cli_path": ctx.cli_path, - "cwd": ctx.work_dir, - "env": ctx.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=ctx.cli_path, + cwd=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) ) try: diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index c030889129..27dce38a11 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -10,7 +10,7 @@ import tempfile from pathlib import Path -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig from .proxy import CapiProxy @@ -64,12 +64,12 @@ async def setup(self): "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) self._client = CopilotClient( - { - "cli_path": self.cli_path, - "cwd": self.work_dir, - "env": self.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=self.cli_path, + cwd=self.work_dir, + env=self.get_env(), + github_token=github_token, + ) ) async def teardown(self, test_failed: bool = False): diff --git a/python/test_client.py b/python/test_client.py index 62ae7b1886..9b7e8eb0f5 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,7 +6,14 @@ import pytest -from copilot import CopilotClient, PermissionHandler, PermissionRequestResult, define_tool +from copilot import ( + CopilotClient, + ExternalServerConfig, + PermissionHandler, + PermissionRequestResult, + SubprocessConfig, + define_tool, +) from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports from e2e.testharness import CLI_PATH @@ -14,7 +21,7 @@ class TestPermissionHandlerRequired: @pytest.mark.asyncio async def test_create_session_raises_without_permission_handler(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: with pytest.raises(ValueError, match="on_permission_request.*is required"): @@ -24,7 +31,7 @@ async def test_create_session_raises_without_permission_handler(self): @pytest.mark.asyncio async def test_v2_permission_adapter_rejects_no_result(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(CLI_PATH)) await client.start() try: session = await client.create_session( @@ -46,7 +53,7 @@ async def test_v2_permission_adapter_rejects_no_result(self): @pytest.mark.asyncio async def test_resume_session_raises_without_permission_handler(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: session = await client.create_session( @@ -60,123 +67,106 @@ async def test_resume_session_raises_without_permission_handler(self): class TestURLParsing: def test_parse_port_only_url(self): - client = CopilotClient({"cli_url": "8080", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="8080")) assert client._actual_port == 8080 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_host_port_url(self): - client = CopilotClient({"cli_url": "127.0.0.1:9000", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="127.0.0.1:9000")) assert client._actual_port == 9000 assert client._actual_host == "127.0.0.1" assert client._is_external_server def test_parse_http_url(self): - client = CopilotClient({"cli_url": "http://localhost:7000", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="http://localhost:7000")) assert client._actual_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_https_url(self): - client = CopilotClient({"cli_url": "https://example.com:443", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="https://example.com:443")) assert client._actual_port == 443 assert client._actual_host == "example.com" assert client._is_external_server def test_invalid_url_format(self): with pytest.raises(ValueError, match="Invalid cli_url format"): - CopilotClient({"cli_url": "invalid-url", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="invalid-url")) def test_invalid_port_too_high(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:99999", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="localhost:99999")) def test_invalid_port_zero(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:0", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="localhost:0")) def test_invalid_port_negative(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:-1", "log_level": "error"}) - - def test_cli_url_with_use_stdio(self): - with pytest.raises(ValueError, match="cli_url is mutually exclusive"): - CopilotClient({"cli_url": "localhost:8080", "use_stdio": True, "log_level": "error"}) - - def test_cli_url_with_cli_path(self): - with pytest.raises(ValueError, match="cli_url is mutually exclusive"): - CopilotClient( - {"cli_url": "localhost:8080", "cli_path": "/path/to/cli", "log_level": "error"} - ) - - def test_use_stdio_false_when_cli_url(self): - client = CopilotClient({"cli_url": "8080", "log_level": "error"}) - assert not client.options["use_stdio"] + CopilotClient(ExternalServerConfig(url="localhost:-1")) def test_is_external_server_true(self): - client = CopilotClient({"cli_url": "localhost:8080", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="localhost:8080")) assert client._is_external_server class TestAuthOptions: def test_accepts_github_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + log_level="error", + ) ) - assert client.options.get("github_token") == "gho_test_token" + assert isinstance(client._config, SubprocessConfig) + assert client._config.github_token == "gho_test_token" def test_default_use_logged_in_user_true_without_token(self): - client = CopilotClient({"cli_path": CLI_PATH, "log_level": "error"}) - assert client.options.get("use_logged_in_user") is True + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is True def test_default_use_logged_in_user_false_with_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + log_level="error", + ) ) - assert client.options.get("use_logged_in_user") is False + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is False def test_explicit_use_logged_in_user_true_with_token(self): client = CopilotClient( - { - "cli_path": CLI_PATH, - "github_token": "gho_test_token", - "use_logged_in_user": True, - "log_level": "error", - } + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + use_logged_in_user=True, + log_level="error", + ) ) - assert client.options.get("use_logged_in_user") is True + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is True def test_explicit_use_logged_in_user_false_without_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "use_logged_in_user": False, "log_level": "error"} - ) - assert client.options.get("use_logged_in_user") is False - - def test_github_token_with_cli_url_raises(self): - with pytest.raises( - ValueError, match="github_token and use_logged_in_user cannot be used with cli_url" - ): - CopilotClient( - { - "cli_url": "localhost:8080", - "github_token": "gho_test_token", - "log_level": "error", - } - ) - - def test_use_logged_in_user_with_cli_url_raises(self): - with pytest.raises( - ValueError, match="github_token and use_logged_in_user cannot be used with cli_url" - ): - CopilotClient( - {"cli_url": "localhost:8080", "use_logged_in_user": False, "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + use_logged_in_user=False, + log_level="error", ) + ) + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is False class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -205,7 +195,7 @@ def grep(params) -> str: @pytest.mark.asyncio async def test_resume_session_sends_overrides_built_in_tool(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -258,7 +248,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: models = await client.list_models() @@ -287,7 +280,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: await client.list_models() @@ -313,7 +309,10 @@ async def test_list_models_async_handler(self): async def handler(): return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: models = await client.list_models() @@ -341,7 +340,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) models = await client.list_models() assert len(handler_calls) == 1 assert models == custom_models @@ -350,7 +352,7 @@ def handler(): class TestSessionConfigForwarding: @pytest.mark.asyncio async def test_create_session_forwards_client_name(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -371,7 +373,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_client_name(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -400,7 +402,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_agent(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -425,7 +427,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_agent(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -457,7 +459,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index e50a33c162..5b82d5922c 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -13,10 +13,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 89f371789b..b6dcc869ca 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -14,10 +14,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index b86c76ba3d..3854626835 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -12,10 +12,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index b501bb10e9..455288f634 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") @@ -13,10 +13,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4886fe07ad..8295c73d51 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -4,7 +4,7 @@ import time import urllib.request -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig DEVICE_CODE_URL = "https://github.com/login/device/code" @@ -78,10 +78,10 @@ async def main(): display_name = f" ({user.get('name')})" if user.get("name") else "" print(f"Authenticated as: {user.get('login')}{display_name}") - opts = {"github_token": token} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=token, + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 29563149a0..e4c45deacf 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -5,7 +5,7 @@ import urllib.request from flask import Flask, request, jsonify -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig app = Flask(__name__) @@ -13,7 +13,7 @@ async def ask_copilot(prompt: str) -> str: - client = CopilotClient({"cli_url": CLI_URL}) + client = CopilotClient(ExternalServerConfig(url=CLI_URL)) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index c407d4fea0..bbf6cf209c 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index c407d4fea0..bbf6cf209c 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index d1441361ff..26a2cd176b 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 8df61b9d35..5f7bc91636 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig hook_log: list[str] = [] @@ -40,10 +40,10 @@ async def on_error_occurred(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 9674da9170..2ff2538049 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig # Track which tools requested permission permission_log: list[str] = [] @@ -16,10 +16,10 @@ async def auto_approve_tool(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index dc8d9fa9b0..683f11d870 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig input_log: list[str] = [] @@ -20,10 +20,10 @@ async def handle_user_input(request, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index dadc0e7be3..45063b29ed 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 0b243cafaa..a8cf1edcf7 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index c7e21e8b9b..31df91c88b 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index b38452a899..38675f1455 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 5e396c8cdf..b4f5caff13 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index ebca899012..07babc2180 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,16 +1,16 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session1, session2 = await asyncio.gather( diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 23749d06f6..0bd69d8118 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 7eb5e0caee..df5eb33ea8 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: # 1. Create a session diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 94569de11a..aff9d24d92 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 0b5f073d53..5d83380d74 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index f092fb9a84..daf7c72606 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: # MCP server config — demonstrates the configuration pattern. diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index a3824bab75..b4fc620a9c 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -9,10 +9,10 @@ async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 3e06650b5e..396e336504 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -2,14 +2,14 @@ import os from pathlib import Path -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 1fdfacc76c..9a6e1054e3 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 1f1099f0d5..89bd41e465 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from copilot import CopilotClient, PermissionHandler, define_tool +from copilot import CopilotClient, PermissionHandler, SubprocessConfig, define_tool class GrepParams(BaseModel): @@ -16,10 +16,10 @@ def custom_grep(params: GrepParams) -> str: async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 9a51e7efa4..e8317c716b 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, define_tool +from copilot import CopilotClient, SubprocessConfig, define_tool from pydantic import BaseModel, Field # In-memory virtual filesystem @@ -46,10 +46,10 @@ async def auto_approve_tool(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 1b82b1096b..bb60aabf88 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,13 +1,13 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: # First session diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index d1441361ff..26a2cd176b 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index c407d4fea0..bbf6cf209c 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) From f2d21a0b4aaf04745f347d8e194600bb5bc115c5 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 13 Mar 2026 11:52:55 -0400 Subject: [PATCH 35/61] feat: add OpenTelemetry support across all SDKs (#785) * docs: add OpenTelemetry TelemetryConfig and trace context propagation documentation Add telemetry documentation across all SDK docs: - getting-started.md: New 'Telemetry & Observability' section with per-language examples, TelemetryConfig options table, file export example, and trace context propagation explanation - Per-SDK READMEs (Node.js, Python, Go, .NET): Add telemetry option to constructor/options lists and new Telemetry sections with language-specific examples and dependency notes - observability/opentelemetry.md: Add 'Built-in Telemetry Support' section at top with multi-language examples, options table, propagation details, and dependency matrix - docs/index.md: Update Observability description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add OpenTelemetry support across all SDKs Add TelemetryConfig to all four SDKs (Node, Python, Go, .NET) to configure OpenTelemetry instrumentation on the Copilot CLI process. This includes: - TelemetryConfig type with OTLP endpoint, file exporter, source name, and capture-content options, mapped to CLI environment variables - W3C Trace Context propagation (traceparent/tracestate) on session.create, session.resume, and session.send RPC calls - Trace context restoration in tool call handlers (v2 RPC and v3 broadcast) so user tool code executes within the correct distributed trace - Telemetry helper modules (telemetry.ts, telemetry.py, telemetry.go, Telemetry.cs) with unit tests - Updated generated types from latest schema Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(nodejs): remove @opentelemetry/api dependency Replace the optional @opentelemetry/api peer dependency with a user-provided callback approach: - Add TraceContext interface and TraceContextProvider type - Add onGetTraceContext callback to CopilotClientOptions - Pass traceparent/tracestate directly on ToolInvocation for inbound context - Remove @opentelemetry/api from peerDependencies and devDependencies - Rewrite telemetry.ts to a simple callback-based helper (~27 lines) - Update tests, README, and OpenTelemetry docs with wire-up examples Users who want distributed trace propagation provide a callback: const client = new CopilotClient({ onGetTraceContext: () => { const carrier = {}; propagation.inject(context.active(), carrier); return carrier; }, }); TelemetryConfig (CLI env vars) is unchanged and requires no dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson --- docs/getting-started.md | 114 +++++++++++++ docs/index.md | 2 +- docs/observability/opentelemetry.md | 154 +++++++++++++++++- dotnet/README.md | 27 +++ dotnet/src/Client.cs | 37 ++++- dotnet/src/Session.cs | 11 +- dotnet/src/Telemetry.cs | 51 ++++++ dotnet/src/Types.cs | 53 ++++++ dotnet/test/TelemetryTests.cs | 65 ++++++++ go/README.md | 27 +++ go/client.go | 52 +++++- go/go.mod | 13 +- go/go.sum | 21 +++ go/session.go | 30 +++- go/telemetry.go | 31 ++++ go/telemetry_test.go | 86 ++++++++++ go/types.go | 39 +++++ nodejs/README.md | 47 ++++++ nodejs/src/client.ts | 53 +++++- nodejs/src/index.ts | 3 + nodejs/src/session.ts | 30 +++- nodejs/src/telemetry.ts | 27 +++ nodejs/src/types.ts | 72 ++++++++ nodejs/test/client.test.ts | 90 ++++++++++ nodejs/test/telemetry.test.ts | 133 +++++++++++++++ python/README.md | 27 +++ python/copilot/__init__.py | 2 + python/copilot/client.py | 36 +++- python/copilot/session.py | 15 +- python/copilot/telemetry.py | 48 ++++++ python/copilot/types.py | 19 +++ python/pyproject.toml | 3 + python/test_telemetry.py | 128 +++++++++++++++ test/scenarios/auth/byok-anthropic/go/go.mod | 6 + test/scenarios/auth/byok-anthropic/go/go.sum | 21 +++ test/scenarios/auth/byok-azure/go/go.mod | 6 + test/scenarios/auth/byok-azure/go/go.sum | 21 +++ test/scenarios/auth/byok-ollama/go/go.mod | 6 + test/scenarios/auth/byok-ollama/go/go.sum | 21 +++ test/scenarios/auth/byok-openai/go/go.mod | 6 + test/scenarios/auth/byok-openai/go/go.sum | 21 +++ test/scenarios/auth/gh-app/go/go.mod | 6 + test/scenarios/auth/gh-app/go/go.sum | 21 +++ .../bundling/app-backend-to-server/go/go.mod | 6 + .../bundling/app-backend-to-server/go/go.sum | 21 +++ .../bundling/app-direct-server/go/go.mod | 6 + .../bundling/app-direct-server/go/go.sum | 21 +++ .../bundling/container-proxy/go/go.mod | 6 + .../bundling/container-proxy/go/go.sum | 21 +++ .../bundling/fully-bundled/go/go.mod | 6 + .../bundling/fully-bundled/go/go.sum | 21 +++ test/scenarios/callbacks/hooks/go/go.mod | 6 + test/scenarios/callbacks/hooks/go/go.sum | 21 +++ .../scenarios/callbacks/permissions/go/go.mod | 6 + .../scenarios/callbacks/permissions/go/go.sum | 21 +++ test/scenarios/callbacks/user-input/go/go.mod | 6 + test/scenarios/callbacks/user-input/go/go.sum | 21 +++ test/scenarios/modes/default/go/go.mod | 6 + test/scenarios/modes/default/go/go.sum | 21 +++ test/scenarios/modes/minimal/go/go.mod | 6 + test/scenarios/modes/minimal/go/go.sum | 21 +++ test/scenarios/prompts/attachments/go/go.mod | 6 + test/scenarios/prompts/attachments/go/go.sum | 21 +++ .../prompts/reasoning-effort/go/go.mod | 6 + .../prompts/reasoning-effort/go/go.sum | 21 +++ .../prompts/system-message/go/go.mod | 6 + .../prompts/system-message/go/go.sum | 21 +++ .../sessions/concurrent-sessions/go/go.mod | 6 + .../sessions/concurrent-sessions/go/go.sum | 21 +++ .../sessions/infinite-sessions/go/go.mod | 6 + .../sessions/infinite-sessions/go/go.sum | 21 +++ .../sessions/session-resume/go/go.mod | 6 + .../sessions/session-resume/go/go.sum | 21 +++ test/scenarios/sessions/streaming/go/go.mod | 6 + test/scenarios/sessions/streaming/go/go.sum | 21 +++ test/scenarios/tools/custom-agents/go/go.mod | 6 + test/scenarios/tools/custom-agents/go/go.sum | 21 +++ test/scenarios/tools/mcp-servers/go/go.mod | 6 + test/scenarios/tools/mcp-servers/go/go.sum | 21 +++ test/scenarios/tools/no-tools/go/go.mod | 6 + test/scenarios/tools/no-tools/go/go.sum | 21 +++ test/scenarios/tools/skills/go/go.mod | 6 + test/scenarios/tools/skills/go/go.sum | 21 +++ test/scenarios/tools/tool-filtering/go/go.mod | 6 + test/scenarios/tools/tool-filtering/go/go.sum | 21 +++ test/scenarios/tools/tool-overrides/go/go.mod | 6 + test/scenarios/tools/tool-overrides/go/go.sum | 21 +++ .../tools/virtual-filesystem/go/go.mod | 6 + .../tools/virtual-filesystem/go/go.sum | 21 +++ test/scenarios/transport/reconnect/go/go.mod | 6 + test/scenarios/transport/reconnect/go/go.sum | 21 +++ test/scenarios/transport/stdio/go/go.mod | 6 + test/scenarios/transport/stdio/go/go.sum | 21 +++ test/scenarios/transport/tcp/go/go.mod | 6 + test/scenarios/transport/tcp/go/go.sum | 21 +++ 95 files changed, 2345 insertions(+), 38 deletions(-) create mode 100644 dotnet/src/Telemetry.cs create mode 100644 dotnet/test/TelemetryTests.cs create mode 100644 go/telemetry.go create mode 100644 go/telemetry_test.go create mode 100644 nodejs/src/telemetry.ts create mode 100644 nodejs/test/telemetry.test.ts create mode 100644 python/copilot/telemetry.py create mode 100644 python/test_telemetry.py diff --git a/docs/getting-started.md b/docs/getting-started.md index fe952182c1..1785928056 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1395,6 +1395,119 @@ await using var session = await client.CreateSessionAsync(new() --- +## Telemetry & Observability + +The Copilot SDK supports [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. Provide a `telemetry` configuration to the client to enable trace export from the CLI process and automatic [W3C Trace Context](https://www.w3.org/TR/trace-context/) propagation between the SDK and CLI. + +### Enabling Telemetry + +Pass a `telemetry` (or `Telemetry`) config when creating the client. This is the opt-in — no separate "enabled" flag is needed. + +
+Node.js / TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + telemetry: { + otlpEndpoint: "http://localhost:4318", + }, +}); +``` + +Optional peer dependency: `@opentelemetry/api` + +
+ +
+Python + + +```python +from copilot import CopilotClient, SubprocessConfig + +client = CopilotClient(SubprocessConfig( + telemetry={ + "otlp_endpoint": "http://localhost:4318", + }, +)) +``` + +Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`) + +
+ +
+Go + + +```go +client, err := copilot.NewClient(copilot.ClientOptions{ + Telemetry: &copilot.TelemetryConfig{ + OTLPEndpoint: "http://localhost:4318", + }, +}) +``` + +Dependency: `go.opentelemetry.io/otel` + +
+ +
+.NET + + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + Telemetry = new TelemetryConfig + { + OtlpEndpoint = "http://localhost:4318", + }, +}); +``` + +No extra dependencies — uses built-in `System.Diagnostics.Activity`. + +
+ +### TelemetryConfig Options + +| Option | Node.js | Python | Go | .NET | Description | +|---|---|---|---|---|---| +| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `OtlpEndpoint` | OTLP HTTP endpoint URL | +| File path | `filePath` | `file_path` | `FilePath` | `FilePath` | File path for JSON-lines trace output | +| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `ExporterType` | `"otlp-http"` or `"file"` | +| Source name | `sourceName` | `source_name` | `SourceName` | `SourceName` | Instrumentation scope name | +| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `CaptureContent` | Whether to capture message content | + +### File Export + +To write traces to a local file instead of an OTLP endpoint: + + +```typescript +const client = new CopilotClient({ + telemetry: { + filePath: "./traces.jsonl", + exporterType: "file", + }, +}); +``` + +### Trace Context Propagation + +Trace context is propagated automatically — no manual instrumentation is needed: + +- **SDK → CLI**: `traceparent` and `tracestate` headers from the current span/activity are included in `session.create`, `session.resume`, and `session.send` RPC calls. +- **CLI → SDK**: When the CLI invokes tool handlers, the trace context from the CLI's span is propagated so your tool code runs under the correct parent span. + +📖 **[OpenTelemetry Instrumentation Guide →](./observability/opentelemetry.md)** — detailed GenAI semantic conventions, event-to-attribute mapping, and complete examples. + +--- + ## Learn More - [Authentication Guide](./auth/index.md) - GitHub OAuth, environment variables, and BYOK @@ -1406,6 +1519,7 @@ await using var session = await client.CreateSessionAsync(new() - [Using MCP Servers](./features/mcp.md) - Integrate external tools via Model Context Protocol - [GitHub MCP Server Documentation](https://github.com/github/github-mcp-server) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Explore more MCP servers +- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) - Add tracing to your SDK usage --- diff --git a/docs/index.md b/docs/index.md index 9459a7b808..2c5dd202dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,7 +67,7 @@ Detailed API reference for each session hook. ### [Observability](./observability/opentelemetry.md) -- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — add tracing to your SDK usage +- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — built-in TelemetryConfig, trace context propagation, and application-level tracing ### [Integrations](./integrations/microsoft-agent-framework.md) diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index 0ba9802019..26637fc6d9 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -1,6 +1,158 @@ # OpenTelemetry Instrumentation for Copilot SDK -This guide shows how to add OpenTelemetry tracing to your Copilot SDK applications using GenAI semantic conventions. +This guide shows how to add OpenTelemetry tracing to your Copilot SDK applications. + +## Built-in Telemetry Support + +The SDK has built-in support for configuring OpenTelemetry on the CLI process and propagating W3C Trace Context between the SDK and CLI. Provide a `TelemetryConfig` when creating the client to opt in: + +
+Node.js / TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + telemetry: { + otlpEndpoint: "http://localhost:4318", + }, +}); +``` + +
+ +
+Python + + +```python +from copilot import CopilotClient, SubprocessConfig + +client = CopilotClient(SubprocessConfig( + telemetry={ + "otlp_endpoint": "http://localhost:4318", + }, +)) +``` + +
+ +
+Go + + +```go +client, err := copilot.NewClient(copilot.ClientOptions{ + Telemetry: &copilot.TelemetryConfig{ + OTLPEndpoint: "http://localhost:4318", + }, +}) +``` + +
+ +
+.NET + + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + Telemetry = new TelemetryConfig + { + OtlpEndpoint = "http://localhost:4318", + }, +}); +``` + +
+ +### TelemetryConfig Options + +| Option | Node.js | Python | Go | .NET | Description | +|---|---|---|---|---|---| +| OTLP endpoint | `otlpEndpoint` | `otlp_endpoint` | `OTLPEndpoint` | `OtlpEndpoint` | OTLP HTTP endpoint URL | +| File path | `filePath` | `file_path` | `FilePath` | `FilePath` | File path for JSON-lines trace output | +| Exporter type | `exporterType` | `exporter_type` | `ExporterType` | `ExporterType` | `"otlp-http"` or `"file"` | +| Source name | `sourceName` | `source_name` | `SourceName` | `SourceName` | Instrumentation scope name | +| Capture content | `captureContent` | `capture_content` | `CaptureContent` | `CaptureContent` | Whether to capture message content | + +### Trace Context Propagation + +> **Most users don't need this.** The `TelemetryConfig` above is all you need to collect traces from the CLI. The trace context propagation described in this section is an **advanced feature** for applications that create their own OpenTelemetry spans and want them to appear in the **same distributed trace** as the CLI's spans. + +The SDK can propagate W3C Trace Context (`traceparent`/`tracestate`) on JSON-RPC payloads so that your application's spans and the CLI's spans are linked in one distributed trace. This is useful when, for example, you want to see a "handle tool call" span in your app nested inside the CLI's "execute tool" span, or show the SDK call as a child of your request-handling span. + +#### SDK → CLI (outbound) + +For **Node.js**, provide an `onGetTraceContext` callback on the client options. This is only needed if your application already uses `@opentelemetry/api` and you want to link your spans with the CLI's spans. The SDK calls this callback before `session.create`, `session.resume`, and `session.send` RPCs: + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; +import { propagation, context } from "@opentelemetry/api"; + +const client = new CopilotClient({ + telemetry: { otlpEndpoint: "http://localhost:4318" }, + onGetTraceContext: () => { + const carrier: Record = {}; + propagation.inject(context.active(), carrier); + return carrier; // { traceparent: "00-...", tracestate: "..." } + }, +}); +``` + +For **Python**, **Go**, and **.NET**, trace context injection is automatic when the respective OpenTelemetry/Activity API is configured — no callback is needed. + +#### CLI → SDK (inbound) + +When the CLI invokes a tool handler, the `traceparent` and `tracestate` from the CLI's span are available in all languages: + +- **Go**: The `ToolInvocation.TraceContext` field is a `context.Context` with the trace already restored — use it directly as the parent for your spans. +- **Python**: Trace context is automatically restored around the handler via `trace_context()` — child spans are parented to the CLI's span automatically. +- **.NET**: Trace context is automatically restored via `RestoreTraceContext()` — child `Activity` instances are parented to the CLI's span automatically. +- **Node.js**: Since the SDK has no OpenTelemetry dependency, `traceparent` and `tracestate` are passed as raw strings on the `ToolInvocation` object. Restore the context manually if needed: + + +```typescript +import { propagation, context, trace } from "@opentelemetry/api"; + +session.registerTool(myTool, async (args, invocation) => { + // Restore the CLI's trace context as the active context + const carrier = { + traceparent: invocation.traceparent, + tracestate: invocation.tracestate, + }; + const parentCtx = propagation.extract(context.active(), carrier); + + // Create a child span under the CLI's span + const tracer = trace.getTracer("my-app"); + return context.with(parentCtx, () => + tracer.startActiveSpan("my-tool", async (span) => { + try { + const result = await doWork(args); + return result; + } finally { + span.end(); + } + }) + ); +}); +``` + +### Per-Language Dependencies + +| Language | Dependency | Notes | +|---|---|---| +| Node.js | — | No dependency; provide `onGetTraceContext` callback for outbound propagation | +| Python | `opentelemetry-api` | Install with `pip install copilot-sdk[telemetry]` | +| Go | `go.opentelemetry.io/otel` | Required dependency | +| .NET | — | Uses built-in `System.Diagnostics.Activity` | + +## Application-Level Instrumentation + +The rest of this guide shows how to add your own OpenTelemetry spans around SDK operations using GenAI semantic conventions. This is complementary to the built-in `TelemetryConfig` above — you can use both together. ## Overview diff --git a/dotnet/README.md b/dotnet/README.md index 441904de7a..712323c0c6 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -78,6 +78,7 @@ new CopilotClient(CopilotClientOptions? options = null) - `Logger` - `ILogger` instance for SDK logging - `GitHubToken` - GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` - Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CliUrl`. +- `Telemetry` - OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. #### Methods @@ -546,6 +547,32 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +## Telemetry + +The SDK supports OpenTelemetry for distributed tracing. Provide a `Telemetry` config to enable trace export and automatic W3C Trace Context propagation. + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + Telemetry = new TelemetryConfig + { + OtlpEndpoint = "http://localhost:4318", + }, +}); +``` + +**TelemetryConfig properties:** + +- `OtlpEndpoint` - OTLP HTTP endpoint URL +- `FilePath` - File path for JSON-lines trace output +- `ExporterType` - `"otlp-http"` or `"file"` +- `SourceName` - Instrumentation scope name +- `CaptureContent` - Whether to capture message content + +Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `CreateSessionAsync`, `ResumeSessionAsync`, and `SendAsync` calls, and inbound when the CLI invokes tool handlers. + +No extra dependencies — uses built-in `System.Diagnostics.Activity`. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index fd56674b20..40b96580f5 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -431,6 +431,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance try { + var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext(); + var request = new CreateSessionRequest( config.Model, sessionId, @@ -453,7 +455,9 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.ConfigDir, config.SkillDirectories, config.DisabledSkills, - config.InfiniteSessions); + config.InfiniteSessions, + traceparent, + tracestate); var response = await InvokeRpcAsync( connection.Rpc, "session.create", [request], cancellationToken); @@ -535,6 +539,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes try { + var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext(); + var request = new ResumeSessionRequest( sessionId, config.ClientName, @@ -558,7 +564,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Agent, config.SkillDirectories, config.DisabledSkills, - config.InfiniteSessions); + config.InfiniteSessions, + traceparent, + tracestate); var response = await InvokeRpcAsync( connection.Rpc, "session.resume", [request], cancellationToken); @@ -1070,6 +1078,17 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio startInfo.Environment["COPILOT_SDK_AUTH_TOKEN"] = options.GitHubToken; } + // Set telemetry environment variables if configured + if (options.Telemetry is { } telemetry) + { + startInfo.Environment["COPILOT_OTEL_ENABLED"] = "true"; + if (telemetry.OtlpEndpoint is not null) startInfo.Environment["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry.OtlpEndpoint; + if (telemetry.FilePath is not null) startInfo.Environment["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry.FilePath; + if (telemetry.ExporterType is not null) startInfo.Environment["COPILOT_OTEL_EXPORTER_TYPE"] = telemetry.ExporterType; + if (telemetry.SourceName is not null) startInfo.Environment["COPILOT_OTEL_SOURCE_NAME"] = telemetry.SourceName; + if (telemetry.CaptureContent is { } capture) startInfo.Environment["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = capture ? "true" : "false"; + } + var cliProcess = new Process { StartInfo = startInfo }; cliProcess.Start(); @@ -1329,8 +1348,12 @@ public async Task OnHooksInvoke(string sessionId, string ho public async Task OnToolCallV2(string sessionId, string toolCallId, string toolName, - object? arguments) + object? arguments, + string? traceparent = null, + string? tracestate = null) { + using var _ = TelemetryHelpers.RestoreTraceContext(traceparent, tracestate); + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); if (session.GetTool(toolName) is not { } tool) { @@ -1470,7 +1493,9 @@ internal record CreateSessionRequest( string? ConfigDir, List? SkillDirectories, List? DisabledSkills, - InfiniteSessionConfig? InfiniteSessions); + InfiniteSessionConfig? InfiniteSessions, + string? Traceparent = null, + string? Tracestate = null); internal record ToolDefinition( string Name, @@ -1516,7 +1541,9 @@ internal record ResumeSessionRequest( string? Agent, List? SkillDirectories, List? DisabledSkills, - InfiniteSessionConfig? InfiniteSessions); + InfiniteSessionConfig? InfiniteSessions, + string? Traceparent = null, + string? Tracestate = null); internal record ResumeSessionResponse( string SessionId, diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 5f83ef6a02..07a818c211 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -152,12 +152,16 @@ private Task InvokeRpcAsync(string method, object?[]? args, CancellationTo /// public async Task SendAsync(MessageOptions options, CancellationToken cancellationToken = default) { + var (traceparent, tracestate) = TelemetryHelpers.GetTraceContext(); + var request = new SendMessageRequest { SessionId = SessionId, Prompt = options.Prompt, Attachments = options.Attachments, - Mode = options.Mode + Mode = options.Mode, + Traceparent = traceparent, + Tracestate = tracestate }; var response = await InvokeRpcAsync( @@ -412,7 +416,8 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) if (tool is null) return; // This client doesn't handle this tool; another client will. - await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool); + using (TelemetryHelpers.RestoreTraceContext(data.Traceparent, data.Tracestate)) + await ExecuteToolAndRespondAsync(data.RequestId, data.ToolName, data.ToolCallId, data.Arguments, tool); break; } @@ -822,6 +827,8 @@ internal record SendMessageRequest public string Prompt { get; init; } = string.Empty; public List? Attachments { get; init; } public string? Mode { get; init; } + public string? Traceparent { get; init; } + public string? Tracestate { get; init; } } internal record SendMessageResponse diff --git a/dotnet/src/Telemetry.cs b/dotnet/src/Telemetry.cs new file mode 100644 index 0000000000..6bae267a90 --- /dev/null +++ b/dotnet/src/Telemetry.cs @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Diagnostics; + +namespace GitHub.Copilot.SDK; + +internal static class TelemetryHelpers +{ + internal static (string? Traceparent, string? Tracestate) GetTraceContext() + { + return Activity.Current is { } activity + ? (activity.Id, activity.TraceStateString) + : (null, null); + } + + /// + /// Sets to reflect the trace context from the given + /// W3C / headers. + /// The runtime already owns the execute_tool span; this just ensures + /// user code runs under the correct parent so any child activities are properly parented. + /// Dispose the returned to restore the previous . + /// + /// + /// Because this Activity is not created via an , it will not + /// be sampled or exported by any standard OpenTelemetry exporter — it is invisible in + /// trace backends. It exists only to carry the remote parent context through + /// so that child activities created by user tool + /// handlers are parented to the CLI's span. + /// + internal static Activity? RestoreTraceContext(string? traceparent, string? tracestate) + { + if (traceparent is not null && + ActivityContext.TryParse(traceparent, tracestate, out ActivityContext parent)) + { + Activity activity = new("copilot.tool_handler"); + activity.SetParentId(parent.TraceId, parent.SpanId, parent.TraceFlags); + if (tracestate is not null) + { + activity.TraceStateString = tracestate; + } + + activity.Start(); + + return activity; + } + + return null; + } +} diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index cdc0818054..84e7feaede 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -63,6 +63,7 @@ protected CopilotClientOptions(CopilotClientOptions? other) Logger = other.Logger; LogLevel = other.LogLevel; Port = other.Port; + Telemetry = other.Telemetry; UseLoggedInUser = other.UseLoggedInUser; UseStdio = other.UseStdio; OnListModels = other.OnListModels; @@ -148,6 +149,12 @@ public string? GithubToken /// public Func>>? OnListModels { get; set; } + /// + /// OpenTelemetry configuration for the CLI server. + /// When set to a non- instance, the CLI server is started with OpenTelemetry instrumentation enabled. + /// + public TelemetryConfig? Telemetry { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -163,6 +170,52 @@ public virtual CopilotClientOptions Clone() } } +/// +/// OpenTelemetry configuration for the Copilot CLI server. +/// +public sealed class TelemetryConfig +{ + /// + /// OTLP exporter endpoint URL. + /// + /// + /// Maps to the OTEL_EXPORTER_OTLP_ENDPOINT environment variable. + /// + public string? OtlpEndpoint { get; set; } + + /// + /// File path for the file exporter. + /// + /// + /// Maps to the COPILOT_OTEL_FILE_EXPORTER_PATH environment variable. + /// + public string? FilePath { get; set; } + + /// + /// Exporter type ("otlp-http" or "file"). + /// + /// + /// Maps to the COPILOT_OTEL_EXPORTER_TYPE environment variable. + /// + public string? ExporterType { get; set; } + + /// + /// Source name for telemetry spans. + /// + /// + /// Maps to the COPILOT_OTEL_SOURCE_NAME environment variable. + /// + public string? SourceName { get; set; } + + /// + /// Whether to capture message content as part of telemetry. + /// + /// + /// Maps to the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT environment variable. + /// + public bool? CaptureContent { get; set; } +} + /// /// Represents a binary result returned by a tool invocation. /// diff --git a/dotnet/test/TelemetryTests.cs b/dotnet/test/TelemetryTests.cs new file mode 100644 index 0000000000..2d23d584f0 --- /dev/null +++ b/dotnet/test/TelemetryTests.cs @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Diagnostics; +using Xunit; + +namespace GitHub.Copilot.SDK.Test; + +public class TelemetryTests +{ + [Fact] + public void TelemetryConfig_DefaultValues_AreNull() + { + var config = new TelemetryConfig(); + + Assert.Null(config.OtlpEndpoint); + Assert.Null(config.FilePath); + Assert.Null(config.ExporterType); + Assert.Null(config.SourceName); + Assert.Null(config.CaptureContent); + } + + [Fact] + public void TelemetryConfig_CanSetAllProperties() + { + var config = new TelemetryConfig + { + OtlpEndpoint = "http://localhost:4318", + FilePath = "/tmp/traces.json", + ExporterType = "otlp-http", + SourceName = "my-app", + CaptureContent = true + }; + + Assert.Equal("http://localhost:4318", config.OtlpEndpoint); + Assert.Equal("/tmp/traces.json", config.FilePath); + Assert.Equal("otlp-http", config.ExporterType); + Assert.Equal("my-app", config.SourceName); + Assert.True(config.CaptureContent); + } + + [Fact] + public void CopilotClientOptions_Telemetry_DefaultsToNull() + { + var options = new CopilotClientOptions(); + + Assert.Null(options.Telemetry); + } + + [Fact] + public void CopilotClientOptions_Clone_CopiesTelemetry() + { + var telemetry = new TelemetryConfig + { + OtlpEndpoint = "http://localhost:4318", + ExporterType = "otlp-http" + }; + + var options = new CopilotClientOptions { Telemetry = telemetry }; + var clone = options.Clone(); + + Assert.Same(telemetry, clone.Telemetry); + } +} diff --git a/go/README.md b/go/README.md index 060acc61b5..f87c3d1b84 100644 --- a/go/README.md +++ b/go/README.md @@ -141,6 +141,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Env` ([]string): Environment variables for CLI process (default: inherits from current process) - `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods. - `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CLIUrl`. +- `Telemetry` (\*TelemetryConfig): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. **SessionConfig:** @@ -472,6 +473,32 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `Type: "azure"`, not `Type: "openai"`. > - The `BaseURL` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. +## Telemetry + +The SDK supports OpenTelemetry for distributed tracing. Provide a `Telemetry` config to enable trace export and automatic W3C Trace Context propagation. + +```go +client, err := copilot.NewClient(copilot.ClientOptions{ + Telemetry: &copilot.TelemetryConfig{ + OTLPEndpoint: "http://localhost:4318", + }, +}) +``` + +**TelemetryConfig fields:** + +- `OTLPEndpoint` (string): OTLP HTTP endpoint URL +- `FilePath` (string): File path for JSON-lines trace output +- `ExporterType` (string): `"otlp-http"` or `"file"` +- `SourceName` (string): Instrumentation scope name +- `CaptureContent` (bool): Whether to capture message content + +Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `CreateSession`, `ResumeSession`, and `Send` calls, and inbound when the CLI invokes tool handlers. + +> **Note:** The current `ToolHandler` signature does not accept a `context.Context`, so the inbound trace context cannot be passed to handler code. Spans created inside a tool handler will not be automatically parented to the CLI's `execute_tool` span. A future version may add a context parameter. + +Dependency: `go.opentelemetry.io/otel` + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: diff --git a/go/client.go b/go/client.go index afd0e70c72..751ce63479 100644 --- a/go/client.go +++ b/go/client.go @@ -526,6 +526,10 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } req.RequestPermission = Bool(true) + traceparent, tracestate := getTraceContext(ctx) + req.Traceparent = traceparent + req.Tracestate = tracestate + sessionID := config.SessionID if sessionID == "" { sessionID = uuid.New().String() @@ -645,6 +649,10 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.InfiniteSessions = config.InfiniteSessions req.RequestPermission = Bool(true) + traceparent, tracestate := getTraceContext(ctx) + req.Traceparent = traceparent + req.Tracestate = tracestate + // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. session := newSession(sessionID, c.client, "") @@ -1208,6 +1216,30 @@ func (c *Client) startCLIServer(ctx context.Context) error { c.process.Env = append(c.process.Env, "COPILOT_SDK_AUTH_TOKEN="+c.options.GitHubToken) } + if c.options.Telemetry != nil { + t := c.options.Telemetry + c.process.Env = append(c.process.Env, "COPILOT_OTEL_ENABLED=true") + if t.OTLPEndpoint != "" { + c.process.Env = append(c.process.Env, "OTEL_EXPORTER_OTLP_ENDPOINT="+t.OTLPEndpoint) + } + if t.FilePath != "" { + c.process.Env = append(c.process.Env, "COPILOT_OTEL_FILE_EXPORTER_PATH="+t.FilePath) + } + if t.ExporterType != "" { + c.process.Env = append(c.process.Env, "COPILOT_OTEL_EXPORTER_TYPE="+t.ExporterType) + } + if t.SourceName != "" { + c.process.Env = append(c.process.Env, "COPILOT_OTEL_SOURCE_NAME="+t.SourceName) + } + if t.CaptureContent != nil { + val := "false" + if *t.CaptureContent { + val = "true" + } + c.process.Env = append(c.process.Env, "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="+val) + } + } + if c.useStdio { // For stdio mode, we need stdin/stdout pipes stdin, err := c.process.StdinPipe() @@ -1451,10 +1483,12 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso // toolCallRequestV2 is the v2 RPC request payload for tool.call. type toolCallRequestV2 struct { - SessionID string `json:"sessionId"` - ToolCallID string `json:"toolCallId"` - ToolName string `json:"toolName"` - Arguments any `json:"arguments"` + SessionID string `json:"sessionId"` + ToolCallID string `json:"toolCallId"` + ToolName string `json:"toolName"` + Arguments any `json:"arguments"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // toolCallResponseV2 is the v2 RPC response payload for tool.call. @@ -1496,7 +1530,15 @@ func (c *Client) handleToolCallRequestV2(req toolCallRequestV2) (*toolCallRespon }}, nil } - invocation := ToolInvocation(req) + ctx := contextWithTraceParent(context.Background(), req.Traceparent, req.Tracestate) + + invocation := ToolInvocation{ + SessionID: req.SessionID, + ToolCallID: req.ToolCallID, + ToolName: req.ToolName, + Arguments: req.Arguments, + TraceContext: ctx, + } result, err := handler(invocation) if err != nil { diff --git a/go/go.mod b/go/go.mod index 4895825450..ed06061a0d 100644 --- a/go/go.mod +++ b/go/go.mod @@ -7,4 +7,15 @@ require ( github.com/klauspost/compress v1.18.3 ) -require github.com/google/uuid v1.6.0 +require ( + github.com/google/uuid v1.6.0 + go.opentelemetry.io/otel v1.35.0 +) + +require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect +) diff --git a/go/go.sum b/go/go.sum index 2ae02ef350..ec2bbcc1e8 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,3 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= @@ -6,3 +13,17 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/session.go b/go/session.go index 2205ecb111..f7a1ba4ce5 100644 --- a/go/session.go +++ b/go/session.go @@ -119,11 +119,14 @@ func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string) // log.Printf("Failed to send message: %v", err) // } func (s *Session) Send(ctx context.Context, options MessageOptions) (string, error) { + traceparent, tracestate := getTraceContext(ctx) req := sessionSendRequest{ SessionID: s.SessionID, Prompt: options.Prompt, Attachments: options.Attachments, Mode: options.Mode, + Traceparent: traceparent, + Tracestate: tracestate, } result, err := s.client.Request("session.send", req) @@ -512,7 +515,14 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { if event.Data.ToolCallID != nil { toolCallID = *event.Data.ToolCallID } - s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler) + var tp, ts string + if event.Data.Traceparent != nil { + tp = *event.Data.Traceparent + } + if event.Data.Tracestate != nil { + ts = *event.Data.Tracestate + } + s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler, tp, ts) case PermissionRequested: requestID := event.Data.RequestID @@ -528,11 +538,12 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } // executeToolAndRespond executes a tool handler and sends the result back via RPC. -func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler) { +func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, traceparent, tracestate string) { + ctx := contextWithTraceParent(context.Background(), traceparent, tracestate) defer func() { if r := recover(); r != nil { errMsg := fmt.Sprintf("tool panic: %v", r) - s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{ + s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{ RequestID: requestID, Error: &errMsg, }) @@ -540,16 +551,17 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, }() invocation := ToolInvocation{ - SessionID: s.SessionID, - ToolCallID: toolCallID, - ToolName: toolName, - Arguments: arguments, + SessionID: s.SessionID, + ToolCallID: toolCallID, + ToolName: toolName, + Arguments: arguments, + TraceContext: ctx, } result, err := handler(invocation) if err != nil { errMsg := err.Error() - s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{ + s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{ RequestID: requestID, Error: &errMsg, }) @@ -560,7 +572,7 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, if resultStr == "" { resultStr = fmt.Sprintf("%v", result) } - s.RPC.Tools.HandlePendingToolCall(context.Background(), &rpc.SessionToolsHandlePendingToolCallParams{ + s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.SessionToolsHandlePendingToolCallParams{ RequestID: requestID, Result: &rpc.ResultUnion{String: &resultStr}, }) diff --git a/go/telemetry.go b/go/telemetry.go new file mode 100644 index 0000000000..b9a480b871 --- /dev/null +++ b/go/telemetry.go @@ -0,0 +1,31 @@ +package copilot + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +// getTraceContext extracts the current W3C Trace Context (traceparent/tracestate) +// from the Go context using the global OTel propagator. +func getTraceContext(ctx context.Context) (traceparent, tracestate string) { + carrier := propagation.MapCarrier{} + otel.GetTextMapPropagator().Inject(ctx, carrier) + return carrier.Get("traceparent"), carrier.Get("tracestate") +} + +// contextWithTraceParent returns a new context with trace context extracted from +// the provided W3C traceparent and tracestate headers. +func contextWithTraceParent(ctx context.Context, traceparent, tracestate string) context.Context { + if traceparent == "" { + return ctx + } + carrier := propagation.MapCarrier{ + "traceparent": traceparent, + } + if tracestate != "" { + carrier["tracestate"] = tracestate + } + return otel.GetTextMapPropagator().Extract(ctx, carrier) +} diff --git a/go/telemetry_test.go b/go/telemetry_test.go new file mode 100644 index 0000000000..827623fce8 --- /dev/null +++ b/go/telemetry_test.go @@ -0,0 +1,86 @@ +package copilot + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +func TestGetTraceContextEmpty(t *testing.T) { + // Without any propagator configured, should return empty strings + tp, ts := getTraceContext(context.Background()) + if tp != "" || ts != "" { + t.Errorf("expected empty trace context, got traceparent=%q tracestate=%q", tp, ts) + } +} + +func TestGetTraceContextWithPropagator(t *testing.T) { + // Set up W3C propagator + otel.SetTextMapPropagator(propagation.TraceContext{}) + defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) + + // Inject known trace context + carrier := propagation.MapCarrier{ + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + } + ctx := otel.GetTextMapPropagator().Extract(context.Background(), carrier) + + tp, ts := getTraceContext(ctx) + if tp == "" { + t.Error("expected non-empty traceparent") + } + _ = ts // tracestate may be empty +} + +func TestContextWithTraceParentEmpty(t *testing.T) { + ctx := contextWithTraceParent(context.Background(), "", "") + if ctx == nil { + t.Error("expected non-nil context") + } +} + +func TestContextWithTraceParentValid(t *testing.T) { + otel.SetTextMapPropagator(propagation.TraceContext{}) + defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) + + ctx := contextWithTraceParent(context.Background(), + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "") + + // Verify the context has trace info by extracting it back + carrier := propagation.MapCarrier{} + otel.GetTextMapPropagator().Inject(ctx, carrier) + if carrier.Get("traceparent") == "" { + t.Error("expected traceparent to be set in context") + } +} + +func TestToolInvocationTraceContext(t *testing.T) { + otel.SetTextMapPropagator(propagation.TraceContext{}) + defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) + + traceparent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ctx := contextWithTraceParent(context.Background(), traceparent, "") + + inv := ToolInvocation{ + SessionID: "sess-1", + ToolCallID: "call-1", + ToolName: "my_tool", + Arguments: nil, + TraceContext: ctx, + } + + // The TraceContext should carry the remote span context + sc := trace.SpanContextFromContext(inv.TraceContext) + if !sc.IsValid() { + t.Fatal("expected valid span context on ToolInvocation.TraceContext") + } + if sc.TraceID().String() != "4bf92f3577b34da6a3ce929d0e0e4736" { + t.Errorf("unexpected trace ID: %s", sc.TraceID()) + } + if sc.SpanID().String() != "00f067aa0ba902b7" { + t.Errorf("unexpected span ID: %s", sc.SpanID()) + } +} diff --git a/go/types.go b/go/types.go index 3ccbd0cc91..fd9968e3e3 100644 --- a/go/types.go +++ b/go/types.go @@ -61,6 +61,33 @@ type ClientOptions struct { // querying the CLI server. Useful in BYOK mode to return models // available from your custom provider. OnListModels func(ctx context.Context) ([]ModelInfo, error) + // Telemetry configures OpenTelemetry integration for the Copilot CLI process. + // When non-nil, COPILOT_OTEL_ENABLED=true is set and any populated fields + // are mapped to the corresponding environment variables. + Telemetry *TelemetryConfig +} + +// TelemetryConfig configures OpenTelemetry integration for the Copilot CLI process. +type TelemetryConfig struct { + // OTLPEndpoint is the OTLP HTTP endpoint URL for trace/metric export. + // Sets OTEL_EXPORTER_OTLP_ENDPOINT. + OTLPEndpoint string + + // FilePath is the file path for JSON-lines trace output. + // Sets COPILOT_OTEL_FILE_EXPORTER_PATH. + FilePath string + + // ExporterType is the exporter backend type: "otlp-http" or "file". + // Sets COPILOT_OTEL_EXPORTER_TYPE. + ExporterType string + + // SourceName is the instrumentation scope name. + // Sets COPILOT_OTEL_SOURCE_NAME. + SourceName string + + // CaptureContent controls whether to capture message content (prompts, responses). + // Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. + CaptureContent *bool } // Bool returns a pointer to the given bool value. @@ -429,6 +456,12 @@ type ToolInvocation struct { ToolCallID string ToolName string Arguments any + + // TraceContext carries the W3C Trace Context propagated from the CLI's + // execute_tool span. Pass this to OpenTelemetry-aware code so that + // child spans created inside the handler are parented to the CLI span. + // When no trace context is available this will be context.Background(). + TraceContext context.Context } // ToolHandler executes a tool invocation. @@ -682,6 +715,8 @@ type createSessionRequest struct { SkillDirectories []string `json:"skillDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // createSessionResponse is the response from session.create @@ -715,6 +750,8 @@ type resumeSessionRequest struct { SkillDirectories []string `json:"skillDirectories,omitempty"` DisabledSkills []string `json:"disabledSkills,omitempty"` InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // resumeSessionResponse is the response from session.resume @@ -843,6 +880,8 @@ type sessionSendRequest struct { Prompt string `json:"prompt"` Attachments []Attachment `json:"attachments,omitempty"` Mode string `json:"mode,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // sessionSendResponse is the response from session.send diff --git a/nodejs/README.md b/nodejs/README.md index dc2acaf3e9..af37b27bfc 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -84,6 +84,8 @@ new CopilotClient(options?: CopilotClientOptions) - `autoStart?: boolean` - Auto-start server (default: true) - `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. - `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`. +- `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the CLI process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. +- `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the CLI's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. #### Methods @@ -601,6 +603,51 @@ const session = await client.createSession({ > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`. > - The `baseUrl` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. +## Telemetry + +The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export from the CLI process — this is all most users need: + +```typescript +const client = new CopilotClient({ + telemetry: { + otlpEndpoint: "http://localhost:4318", + }, +}); +``` + +With just this configuration, the CLI emits spans for every session, message, and tool call to your collector. No additional dependencies or setup required. + +**TelemetryConfig options:** + +- `otlpEndpoint?: string` - OTLP HTTP endpoint URL +- `filePath?: string` - File path for JSON-lines trace output +- `exporterType?: string` - `"otlp-http"` or `"file"` +- `sourceName?: string` - Instrumentation scope name +- `captureContent?: boolean` - Whether to capture message content + +### Advanced: Trace Context Propagation + +> **You don't need this for normal telemetry collection.** The `telemetry` config above is sufficient to get full traces from the CLI. + +`onGetTraceContext` is only needed if your application creates its own OpenTelemetry spans and you want them to appear in the **same distributed trace** as the CLI's spans — for example, to nest a "handle tool call" span inside the CLI's "execute tool" span, or to show the SDK call as a child of your application's request-handling span. + +If you're already using `@opentelemetry/api` in your app and want this linkage, provide a callback: + +```typescript +import { propagation, context } from "@opentelemetry/api"; + +const client = new CopilotClient({ + telemetry: { otlpEndpoint: "http://localhost:4318" }, + onGetTraceContext: () => { + const carrier: Record = {}; + propagation.inject(context.active(), carrier); + return carrier; + }, +}); +``` + +Inbound trace context from the CLI is available on the `ToolInvocation` object passed to tool handlers as `traceparent` and `tracestate` fields. See the [OpenTelemetry guide](../docs/observability/opentelemetry.md) for a full wire-up example. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler: diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 783177c950..b8e7b31dcc 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -26,6 +26,7 @@ import { import { createServerRpc } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; +import { getTraceContext } from "./telemetry.js"; import type { ConnectionState, CopilotClientOptions, @@ -42,10 +43,12 @@ import type { SessionLifecycleHandler, SessionListFilter, SessionMetadata, + TelemetryConfig, Tool, ToolCallRequestPayload, ToolCallResponsePayload, ToolResultObject, + TraceContextProvider, TypedSessionLifecycleHandler, } from "./types.js"; @@ -143,17 +146,25 @@ export class CopilotClient { private options: Required< Omit< CopilotClientOptions, - "cliPath" | "cliUrl" | "githubToken" | "useLoggedInUser" | "onListModels" + | "cliPath" + | "cliUrl" + | "githubToken" + | "useLoggedInUser" + | "onListModels" + | "telemetry" + | "onGetTraceContext" > > & { cliPath?: string; cliUrl?: string; githubToken?: string; useLoggedInUser?: boolean; + telemetry?: TelemetryConfig; }; private isExternalServer: boolean = false; private forceStopping: boolean = false; private onListModels?: () => Promise | ModelInfo[]; + private onGetTraceContext?: TraceContextProvider; private modelsCache: ModelInfo[] | null = null; private modelsCacheLock: Promise = Promise.resolve(); private sessionLifecycleHandlers: Set = new Set(); @@ -232,6 +243,7 @@ export class CopilotClient { } this.onListModels = options.onListModels; + this.onGetTraceContext = options.onGetTraceContext; this.options = { cliPath: options.cliUrl ? undefined : options.cliPath || getBundledCliPath(), @@ -249,6 +261,7 @@ export class CopilotClient { githubToken: options.githubToken, // Default useLoggedInUser to false when githubToken is provided, otherwise true useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true), + telemetry: options.telemetry, }; } @@ -556,7 +569,12 @@ export class CopilotClient { // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. - const session = new CopilotSession(sessionId, this.connection!); + const session = new CopilotSession( + sessionId, + this.connection!, + undefined, + this.onGetTraceContext + ); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -572,6 +590,7 @@ export class CopilotClient { try { const response = await this.connection!.sendRequest("session.create", { + ...(await getTraceContext(this.onGetTraceContext)), model: config.model, sessionId, clientName: config.clientName, @@ -656,7 +675,12 @@ export class CopilotClient { // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. - const session = new CopilotSession(sessionId, this.connection!); + const session = new CopilotSession( + sessionId, + this.connection!, + undefined, + this.onGetTraceContext + ); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -672,6 +696,7 @@ export class CopilotClient { try { const response = await this.connection!.sendRequest("session.resume", { + ...(await getTraceContext(this.onGetTraceContext)), sessionId, clientName: config.clientName, model: config.model, @@ -1148,6 +1173,24 @@ export class CopilotClient { ); } + // Set OpenTelemetry environment variables if telemetry is configured + if (this.options.telemetry) { + const t = this.options.telemetry; + envWithoutNodeDebug.COPILOT_OTEL_ENABLED = "true"; + if (t.otlpEndpoint !== undefined) + envWithoutNodeDebug.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint; + if (t.filePath !== undefined) + envWithoutNodeDebug.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath; + if (t.exporterType !== undefined) + envWithoutNodeDebug.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType; + if (t.sourceName !== undefined) + envWithoutNodeDebug.COPILOT_OTEL_SOURCE_NAME = t.sourceName; + if (t.captureContent !== undefined) + envWithoutNodeDebug.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String( + t.captureContent + ); + } + // Verify CLI exists before attempting to spawn if (!existsSync(this.options.cliPath)) { throw new Error( @@ -1562,11 +1605,15 @@ export class CopilotClient { } try { + const traceparent = (params as { traceparent?: string }).traceparent; + const tracestate = (params as { tracestate?: string }).tracestate; const invocation = { sessionId: params.sessionId, toolCallId: params.toolCallId, toolName: params.toolName, arguments: params.arguments, + traceparent, + tracestate, }; const result = await handler(params.arguments, invocation); return { result: this.normalizeToolResultV2(result) }; diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index f2655f2fcd..214b800508 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -45,6 +45,9 @@ export type { SystemMessageAppendConfig, SystemMessageConfig, SystemMessageReplaceConfig, + TelemetryConfig, + TraceContext, + TraceContextProvider, Tool, ToolHandler, ToolInvocation, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 4eb4b144a4..ed08326b19 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -10,6 +10,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; import { ConnectionError, ResponseError } from "vscode-jsonrpc/node.js"; import { createSessionRpc } from "./generated/rpc.js"; +import { getTraceContext } from "./telemetry.js"; import type { MessageOptions, PermissionHandler, @@ -22,6 +23,7 @@ import type { SessionHooks, Tool, ToolHandler, + TraceContextProvider, TypedSessionEventHandler, UserInputHandler, UserInputRequest, @@ -68,6 +70,7 @@ export class CopilotSession { private userInputHandler?: UserInputHandler; private hooks?: SessionHooks; private _rpc: ReturnType | null = null; + private traceContextProvider?: TraceContextProvider; /** * Creates a new CopilotSession instance. @@ -75,13 +78,17 @@ export class CopilotSession { * @param sessionId - The unique identifier for this session * @param connection - The JSON-RPC message connection to the Copilot CLI * @param workspacePath - Path to the session workspace directory (when infinite sessions enabled) + * @param traceContextProvider - Optional callback to get W3C Trace Context for outbound RPCs * @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions. */ constructor( public readonly sessionId: string, private connection: MessageConnection, - private _workspacePath?: string - ) {} + private _workspacePath?: string, + traceContextProvider?: TraceContextProvider + ) { + this.traceContextProvider = traceContextProvider; + } /** * Typed session-scoped RPC methods. @@ -122,6 +129,7 @@ export class CopilotSession { */ async send(options: MessageOptions): Promise { const response = await this.connection.sendRequest("session.send", { + ...(await getTraceContext(this.traceContextProvider)), sessionId: this.sessionId, prompt: options.prompt, attachments: options.attachments, @@ -336,9 +344,19 @@ export class CopilotSession { }; const args = (event.data as { arguments: unknown }).arguments; const toolCallId = (event.data as { toolCallId: string }).toolCallId; + const traceparent = (event.data as { traceparent?: string }).traceparent; + const tracestate = (event.data as { tracestate?: string }).tracestate; const handler = this.toolHandlers.get(toolName); if (handler) { - void this._executeToolAndRespond(requestId, toolName, toolCallId, args, handler); + void this._executeToolAndRespond( + requestId, + toolName, + toolCallId, + args, + handler, + traceparent, + tracestate + ); } } else if (event.type === "permission.requested") { const { requestId, permissionRequest } = event.data as { @@ -360,7 +378,9 @@ export class CopilotSession { toolName: string, toolCallId: string, args: unknown, - handler: ToolHandler + handler: ToolHandler, + traceparent?: string, + tracestate?: string ): Promise { try { const rawResult = await handler(args, { @@ -368,6 +388,8 @@ export class CopilotSession { toolCallId, toolName, arguments: args, + traceparent, + tracestate, }); let result: string; if (rawResult == null) { diff --git a/nodejs/src/telemetry.ts b/nodejs/src/telemetry.ts new file mode 100644 index 0000000000..f9d3316781 --- /dev/null +++ b/nodejs/src/telemetry.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Trace-context helpers. + * + * The SDK does not depend on any OpenTelemetry packages. Instead, users + * provide an {@link TraceContextProvider} callback via client options. + * + * @module telemetry + */ + +import type { TraceContext, TraceContextProvider } from "./types.js"; + +/** + * Calls the user-provided {@link TraceContextProvider} to obtain the current + * W3C Trace Context. Returns `{}` when no provider is configured. + */ +export async function getTraceContext(provider?: TraceContextProvider): Promise { + if (!provider) return {}; + try { + return (await provider()) ?? {}; + } catch { + return {}; + } +} diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index c7756a21c0..9576b69252 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -13,6 +13,41 @@ export type SessionEvent = GeneratedSessionEvent; /** * Options for creating a CopilotClient */ +/** + * W3C Trace Context headers used for distributed trace propagation. + */ +export interface TraceContext { + traceparent?: string; + tracestate?: string; +} + +/** + * Callback that returns the current W3C Trace Context. + * Wire this up to your OpenTelemetry (or other tracing) SDK to enable + * distributed trace propagation between your app and the Copilot CLI. + */ +export type TraceContextProvider = () => TraceContext | Promise; + +/** + * Configuration for OpenTelemetry instrumentation. + * + * When provided via {@link CopilotClientOptions.telemetry}, the SDK sets + * the corresponding environment variables on the spawned CLI process so + * that the CLI's built-in OTel exporter is configured automatically. + */ +export interface TelemetryConfig { + /** OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT. */ + otlpEndpoint?: string; + /** File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH. */ + filePath?: string; + /** Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE. */ + exporterType?: string; + /** Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME. */ + sourceName?: string; + /** Whether to capture message content (prompts, responses). Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT. */ + captureContent?: boolean; +} + export interface CopilotClientOptions { /** * Path to the CLI executable or JavaScript entry point. @@ -103,6 +138,39 @@ export interface CopilotClientOptions { * available from your custom provider. */ onListModels?: () => Promise | ModelInfo[]; + + /** + * OpenTelemetry configuration for the CLI process. + * When provided, the corresponding OTel environment variables are set + * on the spawned CLI server. + */ + telemetry?: TelemetryConfig; + + /** + * Advanced: callback that returns the current W3C Trace Context for distributed + * trace propagation. Most users do not need this — the {@link telemetry} config + * alone is sufficient to collect traces from the CLI. + * + * This callback is only useful when your application creates its own + * OpenTelemetry spans and you want them to appear in the **same** distributed + * trace as the CLI's spans. The SDK calls this before `session.create`, + * `session.resume`, and `session.send` RPCs to inject `traceparent`/`tracestate` + * into the request. + * + * @example + * ```typescript + * import { propagation, context } from "@opentelemetry/api"; + * + * const client = new CopilotClient({ + * onGetTraceContext: () => { + * const carrier: Record = {}; + * propagation.inject(context.active(), carrier); + * return carrier; + * }, + * }); + * ``` + */ + onGetTraceContext?: TraceContextProvider; } /** @@ -133,6 +201,10 @@ export interface ToolInvocation { toolCallId: string; toolName: string; arguments: unknown; + /** W3C Trace Context traceparent from the CLI's execute_tool span. */ + traceparent?: string; + /** W3C Trace Context tracestate from the CLI's execute_tool span. */ + tracestate?: string; } export type ToolHandler = ( diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c54e0fc2ca..c8ae948893 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -535,4 +535,94 @@ describe("CopilotClient", () => { }); }); }); + + describe("onGetTraceContext", () => { + it("includes trace context from callback in session.create request", async () => { + const traceContext = { + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + tracestate: "vendor=opaque", + }; + const provider = vi.fn().mockReturnValue(traceContext); + const client = new CopilotClient({ onGetTraceContext: provider }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ onPermissionRequest: approveAll }); + + expect(provider).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith( + "session.create", + expect.objectContaining({ + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + tracestate: "vendor=opaque", + }) + ); + }); + + it("includes trace context from callback in session.resume request", async () => { + const traceContext = { + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + }; + const provider = vi.fn().mockReturnValue(traceContext); + const client = new CopilotClient({ onGetTraceContext: provider }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + + expect(spy).toHaveBeenCalledWith( + "session.resume", + expect.objectContaining({ + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + }) + ); + }); + + it("includes trace context from callback in session.send request", async () => { + const traceContext = { + traceparent: "00-fedcba0987654321fedcba0987654321-abcdef1234567890-01", + }; + const provider = vi.fn().mockReturnValue(traceContext); + const client = new CopilotClient({ onGetTraceContext: provider }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string) => { + if (method === "session.send") return { responseId: "r1" }; + throw new Error(`Unexpected method: ${method}`); + }); + await session.send({ prompt: "hello" }); + + expect(spy).toHaveBeenCalledWith( + "session.send", + expect.objectContaining({ + traceparent: "00-fedcba0987654321fedcba0987654321-abcdef1234567890-01", + }) + ); + }); + + it("does not include trace context when no callback is provided", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ onPermissionRequest: approveAll }); + + const [, params] = spy.mock.calls.find(([method]) => method === "session.create")!; + expect(params.traceparent).toBeUndefined(); + expect(params.tracestate).toBeUndefined(); + }); + }); }); diff --git a/nodejs/test/telemetry.test.ts b/nodejs/test/telemetry.test.ts new file mode 100644 index 0000000000..9ad97b63a1 --- /dev/null +++ b/nodejs/test/telemetry.test.ts @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, it } from "vitest"; +import { getTraceContext } from "../src/telemetry.js"; +import type { TraceContextProvider } from "../src/types.js"; + +describe("telemetry", () => { + describe("getTraceContext", () => { + it("returns empty object when no provider is given", async () => { + const ctx = await getTraceContext(); + expect(ctx).toEqual({}); + }); + + it("returns empty object when provider is undefined", async () => { + const ctx = await getTraceContext(undefined); + expect(ctx).toEqual({}); + }); + + it("calls provider and returns trace context", async () => { + const provider: TraceContextProvider = () => ({ + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + tracestate: "congo=t61rcWkgMzE", + }); + const ctx = await getTraceContext(provider); + expect(ctx).toEqual({ + traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + tracestate: "congo=t61rcWkgMzE", + }); + }); + + it("supports async providers", async () => { + const provider: TraceContextProvider = async () => ({ + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + }); + const ctx = await getTraceContext(provider); + expect(ctx).toEqual({ + traceparent: "00-abcdef1234567890abcdef1234567890-1234567890abcdef-01", + }); + }); + + it("returns empty object when provider throws", async () => { + const provider: TraceContextProvider = () => { + throw new Error("boom"); + }; + const ctx = await getTraceContext(provider); + expect(ctx).toEqual({}); + }); + + it("returns empty object when async provider rejects", async () => { + const provider: TraceContextProvider = async () => { + throw new Error("boom"); + }; + const ctx = await getTraceContext(provider); + expect(ctx).toEqual({}); + }); + + it("returns empty object when provider returns null", async () => { + const provider = (() => null) as unknown as TraceContextProvider; + const ctx = await getTraceContext(provider); + expect(ctx).toEqual({}); + }); + }); + + describe("TelemetryConfig env var mapping", () => { + it("sets correct env vars for full telemetry config", async () => { + const telemetry = { + otlpEndpoint: "http://localhost:4318", + filePath: "/tmp/traces.jsonl", + exporterType: "otlp-http", + sourceName: "my-app", + captureContent: true, + }; + + const env: Record = {}; + + if (telemetry) { + const t = telemetry; + env.COPILOT_OTEL_ENABLED = "true"; + if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint; + if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath; + if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType; + if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName; + if (t.captureContent !== undefined) + env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String( + t.captureContent + ); + } + + expect(env).toEqual({ + COPILOT_OTEL_ENABLED: "true", + OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318", + COPILOT_OTEL_FILE_EXPORTER_PATH: "/tmp/traces.jsonl", + COPILOT_OTEL_EXPORTER_TYPE: "otlp-http", + COPILOT_OTEL_SOURCE_NAME: "my-app", + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "true", + }); + }); + + it("only sets COPILOT_OTEL_ENABLED for empty telemetry config", async () => { + const telemetry = {}; + const env: Record = {}; + + if (telemetry) { + const t = telemetry as any; + env.COPILOT_OTEL_ENABLED = "true"; + if (t.otlpEndpoint !== undefined) env.OTEL_EXPORTER_OTLP_ENDPOINT = t.otlpEndpoint; + if (t.filePath !== undefined) env.COPILOT_OTEL_FILE_EXPORTER_PATH = t.filePath; + if (t.exporterType !== undefined) env.COPILOT_OTEL_EXPORTER_TYPE = t.exporterType; + if (t.sourceName !== undefined) env.COPILOT_OTEL_SOURCE_NAME = t.sourceName; + if (t.captureContent !== undefined) + env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String( + t.captureContent + ); + } + + expect(env).toEqual({ + COPILOT_OTEL_ENABLED: "true", + }); + }); + + it("converts captureContent false to string 'false'", async () => { + const telemetry = { captureContent: false }; + const env: Record = {}; + + env.COPILOT_OTEL_ENABLED = "true"; + if (telemetry.captureContent !== undefined) + env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = String( + telemetry.captureContent + ); + + expect(env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT).toBe("false"); + }); + }); +}); diff --git a/python/README.md b/python/README.md index 3f542fe982..9d83ae650d 100644 --- a/python/README.md +++ b/python/README.md @@ -128,6 +128,7 @@ CopilotClient( - `env` (dict | None): Environment variables for the CLI process - `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). +- `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. **ExternalServerConfig** — connect to an existing CLI server: @@ -442,6 +443,32 @@ session = await client.create_session({ > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`. > - The `base_url` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. +## Telemetry + +The SDK supports OpenTelemetry for distributed tracing. Provide a `telemetry` config to enable trace export and automatic W3C Trace Context propagation. + +```python +from copilot import CopilotClient, SubprocessConfig + +client = CopilotClient(SubprocessConfig( + telemetry={ + "otlp_endpoint": "http://localhost:4318", + }, +)) +``` + +**TelemetryConfig options:** + +- `otlp_endpoint` (str): OTLP HTTP endpoint URL +- `file_path` (str): File path for JSON-lines trace output +- `exporter_type` (str): `"otlp-http"` or `"file"` +- `source_name` (str): Instrumentation scope name +- `capture_content` (bool): Whether to capture message content + +Trace context (`traceparent`/`tracestate`) is automatically propagated between the SDK and CLI on `create_session`, `resume_session`, and `send` calls, and inbound when the CLI invokes tool handlers. + +Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`) + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 99c14b3314..e0f627c70d 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -35,6 +35,7 @@ SessionMetadata, StopError, SubprocessConfig, + TelemetryConfig, Tool, ToolHandler, ToolInvocation, @@ -73,6 +74,7 @@ "SessionMetadata", "StopError", "SubprocessConfig", + "TelemetryConfig", "Tool", "ToolHandler", "ToolInvocation", diff --git a/python/copilot/client.py b/python/copilot/client.py index fd8b62bd04..29cdf81dca 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -30,6 +30,7 @@ from .jsonrpc import JsonRpcClient, ProcessExitedError from .sdk_protocol_version import get_sdk_protocol_version from .session import CopilotSession +from .telemetry import get_trace_context, trace_context from .types import ( ConnectionState, CustomAgentConfig, @@ -589,6 +590,10 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: session_id = cfg.get("session_id") or str(uuid.uuid4()) payload["sessionId"] = session_id + # Propagate W3C Trace Context to CLI if OpenTelemetry is active + trace_ctx = get_trace_context() + payload.update(trace_ctx) + # Create and register the session before issuing the RPC so that # events emitted by the CLI (e.g. session.start) are not dropped. session = CopilotSession(session_id, self._client, None) @@ -790,6 +795,10 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> if not self._client: raise RuntimeError("Client not connected") + # Propagate W3C Trace Context to CLI if OpenTelemetry is active + trace_ctx = get_trace_context() + payload.update(trace_ctx) + # Create and register the session before issuing the RPC so that # events emitted by the CLI (e.g. session.start) are not dropped. session = CopilotSession(session_id, self._client, None) @@ -1304,6 +1313,23 @@ async def _start_cli_server(self) -> None: if cfg.github_token: env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token + # Set OpenTelemetry environment variables if telemetry config is provided + telemetry = cfg.telemetry + if telemetry is not None: + env["COPILOT_OTEL_ENABLED"] = "true" + if "otlp_endpoint" in telemetry: + env["OTEL_EXPORTER_OTLP_ENDPOINT"] = telemetry["otlp_endpoint"] + if "file_path" in telemetry: + env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = telemetry["file_path"] + if "exporter_type" in telemetry: + env["COPILOT_OTEL_EXPORTER_TYPE"] = telemetry["exporter_type"] + if "source_name" in telemetry: + env["COPILOT_OTEL_SOURCE_NAME"] = telemetry["source_name"] + if "capture_content" in telemetry: + env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str( + telemetry["capture_content"] + ).lower() + # On Windows, hide the console window to avoid distracting users in GUI apps creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 @@ -1605,10 +1631,14 @@ async def _handle_tool_call_request_v2(self, params: dict) -> dict: arguments=arguments, ) + tp = params.get("traceparent") + ts = params.get("tracestate") + try: - result = handler(invocation) - if inspect.isawaitable(result): - result = await result + with trace_context(tp, ts): + result = handler(invocation) + if inspect.isawaitable(result): + result = await result tool_result: ToolResult = result # type: ignore[assignment] return { diff --git a/python/copilot/session.py b/python/copilot/session.py index b4ae210df5..ad049811c8 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -24,6 +24,7 @@ ) from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict from .jsonrpc import JsonRpcError, ProcessExitedError +from .telemetry import get_trace_context, trace_context from .types import ( MessageOptions, PermissionRequest, @@ -147,6 +148,7 @@ async def send(self, options: MessageOptions) -> str: params["attachments"] = options["attachments"] if "mode" in options: params["mode"] = options["mode"] + params.update(get_trace_context()) response = await self._client.request("session.send", params) return response["messageId"] @@ -290,9 +292,11 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: tool_call_id = event.data.tool_call_id or "" arguments = event.data.arguments + tp = getattr(event.data, "traceparent", None) + ts = getattr(event.data, "tracestate", None) asyncio.ensure_future( self._execute_tool_and_respond( - request_id, tool_name, tool_call_id, arguments, handler + request_id, tool_name, tool_call_id, arguments, handler, tp, ts ) ) @@ -318,6 +322,8 @@ async def _execute_tool_and_respond( tool_call_id: str, arguments: Any, handler: ToolHandler, + traceparent: str | None = None, + tracestate: str | None = None, ) -> None: """Execute a tool handler and send the result back via HandlePendingToolCall RPC.""" try: @@ -328,9 +334,10 @@ async def _execute_tool_and_respond( arguments=arguments, ) - result = handler(invocation) - if inspect.isawaitable(result): - result = await result + with trace_context(traceparent, tracestate): + result = handler(invocation) + if inspect.isawaitable(result): + result = await result tool_result: ToolResult if result is None: diff --git a/python/copilot/telemetry.py b/python/copilot/telemetry.py new file mode 100644 index 0000000000..caa27a4e7c --- /dev/null +++ b/python/copilot/telemetry.py @@ -0,0 +1,48 @@ +"""OpenTelemetry trace context helpers for Copilot SDK.""" + +from __future__ import annotations + +from collections.abc import Generator +from contextlib import contextmanager + + +def get_trace_context() -> dict[str, str]: + """Get the current W3C Trace Context (traceparent/tracestate) if OpenTelemetry is available.""" + try: + from opentelemetry import context, propagate + except ImportError: + return {} + + carrier: dict[str, str] = {} + propagate.inject(carrier, context=context.get_current()) + result: dict[str, str] = {} + if "traceparent" in carrier: + result["traceparent"] = carrier["traceparent"] + if "tracestate" in carrier: + result["tracestate"] = carrier["tracestate"] + return result + + +@contextmanager +def trace_context(traceparent: str | None, tracestate: str | None) -> Generator[None, None, None]: + """Context manager that sets the trace context from W3C headers for the block's duration.""" + try: + from opentelemetry import context, propagate + except ImportError: + yield + return + + if not traceparent: + yield + return + + carrier: dict[str, str] = {"traceparent": traceparent} + if tracestate: + carrier["tracestate"] = tracestate + + ctx = propagate.extract(carrier, context=context.get_current()) + token = context.attach(ctx) + try: + yield + finally: + context.detach(token) diff --git a/python/copilot/types.py b/python/copilot/types.py index 4198918989..e572e751b6 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -69,6 +69,22 @@ class SelectionAttachment(TypedDict): Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment +# Configuration for OpenTelemetry integration with the Copilot CLI. +class TelemetryConfig(TypedDict, total=False): + """Configuration for OpenTelemetry integration with the Copilot CLI.""" + + otlp_endpoint: str + """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT.""" + file_path: str + """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH.""" + exporter_type: str + """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE.""" + source_name: str + """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME.""" + capture_content: bool + """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 + + # Configuration for CopilotClient connection modes @@ -120,6 +136,9 @@ class SubprocessConfig: ``None`` (default) resolves to ``True`` unless ``github_token`` is set. """ + telemetry: TelemetryConfig | None = None + """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed.""" + @dataclass class ExternalServerConfig: diff --git a/python/pyproject.toml b/python/pyproject.toml index 741232e8a6..ec270f97e5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -40,6 +40,9 @@ dev = [ "pytest-timeout>=2.0.0", "httpx>=0.24.0", ] +telemetry = [ + "opentelemetry-api>=1.0.0", +] # Use find with a glob so that the copilot.bin subpackage (created dynamically # by scripts/build-wheels.mjs during publishing) is included in platform wheels. diff --git a/python/test_telemetry.py b/python/test_telemetry.py new file mode 100644 index 0000000000..2b4649011b --- /dev/null +++ b/python/test_telemetry.py @@ -0,0 +1,128 @@ +"""Tests for OpenTelemetry telemetry helpers.""" + +from __future__ import annotations + +from unittest.mock import patch + +from copilot.telemetry import get_trace_context, trace_context +from copilot.types import SubprocessConfig, TelemetryConfig + + +class TestGetTraceContext: + def test_returns_empty_dict_when_otel_not_installed(self): + """get_trace_context() returns {} when opentelemetry is not importable.""" + real_import = __import__ + + def _block_otel(name: str, *args, **kwargs): + if name.startswith("opentelemetry"): + raise ImportError("mocked") + return real_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=_block_otel): + result = get_trace_context() + + assert result == {} + + def test_returns_dict_type(self): + """get_trace_context() always returns a dict.""" + result = get_trace_context() + assert isinstance(result, dict) + + +class TestTraceContext: + def test_yields_without_error_when_no_traceparent(self): + """trace_context() with no traceparent should yield without error.""" + with trace_context(None, None): + pass # should not raise + + def test_yields_without_error_when_otel_not_installed(self): + """trace_context() should gracefully yield even if opentelemetry is missing.""" + real_import = __import__ + + def _block_otel(name: str, *args, **kwargs): + if name.startswith("opentelemetry"): + raise ImportError("mocked") + return real_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=_block_otel): + with trace_context("00-abc-def-01", None): + pass # should not raise + + def test_yields_without_error_with_traceparent(self): + """trace_context() with a traceparent value should yield without error.""" + tp = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" + with trace_context(tp, None): + pass # should not raise + + def test_yields_without_error_with_tracestate(self): + """trace_context() with both traceparent and tracestate should yield without error.""" + tp = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" + with trace_context(tp, "congo=t61rcWkgMzE"): + pass # should not raise + + +class TestTelemetryConfig: + def test_telemetry_config_type(self): + """TelemetryConfig can be constructed as a TypedDict.""" + config: TelemetryConfig = { + "otlp_endpoint": "http://localhost:4318", + "exporter_type": "otlp-http", + "source_name": "my-app", + "capture_content": True, + } + assert config["otlp_endpoint"] == "http://localhost:4318" + assert config["capture_content"] is True + + def test_telemetry_config_in_subprocess_config(self): + """TelemetryConfig can be used in SubprocessConfig.""" + config = SubprocessConfig( + telemetry={ + "otlp_endpoint": "http://localhost:4318", + "exporter_type": "otlp-http", + } + ) + assert config.telemetry is not None + assert config.telemetry["otlp_endpoint"] == "http://localhost:4318" + + def test_telemetry_env_var_mapping(self): + """TelemetryConfig fields map to expected environment variable names.""" + config: TelemetryConfig = { + "otlp_endpoint": "http://localhost:4318", + "file_path": "/tmp/traces.jsonl", + "exporter_type": "file", + "source_name": "test-app", + "capture_content": True, + } + + env: dict[str, str] = {} + env["COPILOT_OTEL_ENABLED"] = "true" + if "otlp_endpoint" in config: + env["OTEL_EXPORTER_OTLP_ENDPOINT"] = config["otlp_endpoint"] + if "file_path" in config: + env["COPILOT_OTEL_FILE_EXPORTER_PATH"] = config["file_path"] + if "exporter_type" in config: + env["COPILOT_OTEL_EXPORTER_TYPE"] = config["exporter_type"] + if "source_name" in config: + env["COPILOT_OTEL_SOURCE_NAME"] = config["source_name"] + if "capture_content" in config: + env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = str( + config["capture_content"] + ).lower() + + assert env["COPILOT_OTEL_ENABLED"] == "true" + assert env["OTEL_EXPORTER_OTLP_ENDPOINT"] == "http://localhost:4318" + assert env["COPILOT_OTEL_FILE_EXPORTER_PATH"] == "/tmp/traces.jsonl" + assert env["COPILOT_OTEL_EXPORTER_TYPE"] == "file" + assert env["COPILOT_OTEL_SOURCE_NAME"] == "test-app" + assert env["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] == "true" + + def test_capture_content_false_maps_to_lowercase(self): + """capture_content=False should map to 'false' string.""" + config: TelemetryConfig = {"capture_content": False} + value = str(config["capture_content"]).lower() + assert value == "false" + + def test_empty_telemetry_config(self): + """An empty TelemetryConfig is valid since total=False.""" + config: TelemetryConfig = {} + assert len(config) == 0 diff --git a/test/scenarios/auth/byok-anthropic/go/go.mod b/test/scenarios/auth/byok-anthropic/go/go.mod index 005601ee35..995f349278 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.mod +++ b/test/scenarios/auth/byok-anthropic/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-anthropic/go/go.sum b/test/scenarios/auth/byok-anthropic/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.sum +++ b/test/scenarios/auth/byok-anthropic/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/auth/byok-azure/go/go.mod b/test/scenarios/auth/byok-azure/go/go.mod index 21997114b9..760cb8f623 100644 --- a/test/scenarios/auth/byok-azure/go/go.mod +++ b/test/scenarios/auth/byok-azure/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-azure/go/go.sum b/test/scenarios/auth/byok-azure/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/auth/byok-azure/go/go.sum +++ b/test/scenarios/auth/byok-azure/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/auth/byok-ollama/go/go.mod b/test/scenarios/auth/byok-ollama/go/go.mod index a6891a811f..dfa1f94bcb 100644 --- a/test/scenarios/auth/byok-ollama/go/go.mod +++ b/test/scenarios/auth/byok-ollama/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-ollama/go/go.sum b/test/scenarios/auth/byok-ollama/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/auth/byok-ollama/go/go.sum +++ b/test/scenarios/auth/byok-ollama/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/auth/byok-openai/go/go.mod b/test/scenarios/auth/byok-openai/go/go.mod index 65b3c90280..7c9eff1e52 100644 --- a/test/scenarios/auth/byok-openai/go/go.mod +++ b/test/scenarios/auth/byok-openai/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-openai/go/go.sum b/test/scenarios/auth/byok-openai/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/auth/byok-openai/go/go.sum +++ b/test/scenarios/auth/byok-openai/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/auth/gh-app/go/go.mod b/test/scenarios/auth/gh-app/go/go.mod index 7012daa682..13caa4a2db 100644 --- a/test/scenarios/auth/gh-app/go/go.mod +++ b/test/scenarios/auth/gh-app/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/gh-app/go/go.sum b/test/scenarios/auth/gh-app/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/auth/gh-app/go/go.sum +++ b/test/scenarios/auth/gh-app/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.mod b/test/scenarios/bundling/app-backend-to-server/go/go.mod index c225d6a2c0..2afb521a3a 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.mod +++ b/test/scenarios/bundling/app-backend-to-server/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.sum b/test/scenarios/bundling/app-backend-to-server/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.sum +++ b/test/scenarios/bundling/app-backend-to-server/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/bundling/app-direct-server/go/go.mod b/test/scenarios/bundling/app-direct-server/go/go.mod index e36e0f50da..950890c460 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.mod +++ b/test/scenarios/bundling/app-direct-server/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-direct-server/go/go.sum b/test/scenarios/bundling/app-direct-server/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.sum +++ b/test/scenarios/bundling/app-direct-server/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/bundling/container-proxy/go/go.mod b/test/scenarios/bundling/container-proxy/go/go.mod index 270a60c618..37c7c04bd3 100644 --- a/test/scenarios/bundling/container-proxy/go/go.mod +++ b/test/scenarios/bundling/container-proxy/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/container-proxy/go/go.sum b/test/scenarios/bundling/container-proxy/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/bundling/container-proxy/go/go.sum +++ b/test/scenarios/bundling/container-proxy/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/bundling/fully-bundled/go/go.mod b/test/scenarios/bundling/fully-bundled/go/go.mod index 5c7d03b112..c3bb7d0eaf 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.mod +++ b/test/scenarios/bundling/fully-bundled/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/fully-bundled/go/go.sum b/test/scenarios/bundling/fully-bundled/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.sum +++ b/test/scenarios/bundling/fully-bundled/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/callbacks/hooks/go/go.mod b/test/scenarios/callbacks/hooks/go/go.mod index 3220cd5060..0454868a07 100644 --- a/test/scenarios/callbacks/hooks/go/go.mod +++ b/test/scenarios/callbacks/hooks/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/hooks/go/go.sum b/test/scenarios/callbacks/hooks/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/callbacks/hooks/go/go.sum +++ b/test/scenarios/callbacks/hooks/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/callbacks/permissions/go/go.mod b/test/scenarios/callbacks/permissions/go/go.mod index bf88ca7ec6..d8157e589a 100644 --- a/test/scenarios/callbacks/permissions/go/go.mod +++ b/test/scenarios/callbacks/permissions/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/permissions/go/go.sum b/test/scenarios/callbacks/permissions/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/callbacks/permissions/go/go.sum +++ b/test/scenarios/callbacks/permissions/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/callbacks/user-input/go/go.mod b/test/scenarios/callbacks/user-input/go/go.mod index b050ef88b5..3dc18ebabc 100644 --- a/test/scenarios/callbacks/user-input/go/go.mod +++ b/test/scenarios/callbacks/user-input/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/user-input/go/go.sum b/test/scenarios/callbacks/user-input/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/callbacks/user-input/go/go.sum +++ b/test/scenarios/callbacks/user-input/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/modes/default/go/go.mod b/test/scenarios/modes/default/go/go.mod index 5ce3524d7f..85ba2d6b82 100644 --- a/test/scenarios/modes/default/go/go.mod +++ b/test/scenarios/modes/default/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/default/go/go.sum b/test/scenarios/modes/default/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/modes/default/go/go.sum +++ b/test/scenarios/modes/default/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/modes/minimal/go/go.mod b/test/scenarios/modes/minimal/go/go.mod index c8eb4bbfda..4ce0a27cec 100644 --- a/test/scenarios/modes/minimal/go/go.mod +++ b/test/scenarios/modes/minimal/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/minimal/go/go.sum b/test/scenarios/modes/minimal/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/modes/minimal/go/go.sum +++ b/test/scenarios/modes/minimal/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/prompts/attachments/go/go.mod b/test/scenarios/prompts/attachments/go/go.mod index 22aa80a14e..663655657f 100644 --- a/test/scenarios/prompts/attachments/go/go.mod +++ b/test/scenarios/prompts/attachments/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/attachments/go/go.sum b/test/scenarios/prompts/attachments/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/prompts/attachments/go/go.sum +++ b/test/scenarios/prompts/attachments/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/prompts/reasoning-effort/go/go.mod b/test/scenarios/prompts/reasoning-effort/go/go.mod index b3fafcc1ce..7275182805 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.mod +++ b/test/scenarios/prompts/reasoning-effort/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/reasoning-effort/go/go.sum b/test/scenarios/prompts/reasoning-effort/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.sum +++ b/test/scenarios/prompts/reasoning-effort/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/prompts/system-message/go/go.mod b/test/scenarios/prompts/system-message/go/go.mod index 8bc1c55ce3..e84b079ca6 100644 --- a/test/scenarios/prompts/system-message/go/go.mod +++ b/test/scenarios/prompts/system-message/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/system-message/go/go.sum b/test/scenarios/prompts/system-message/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/prompts/system-message/go/go.sum +++ b/test/scenarios/prompts/system-message/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.mod b/test/scenarios/sessions/concurrent-sessions/go/go.mod index a69dedd169..da999c3a1e 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.mod +++ b/test/scenarios/sessions/concurrent-sessions/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.sum b/test/scenarios/sessions/concurrent-sessions/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.sum +++ b/test/scenarios/sessions/concurrent-sessions/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/sessions/infinite-sessions/go/go.mod b/test/scenarios/sessions/infinite-sessions/go/go.mod index 15f8e48f7f..abdacf8e70 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.mod +++ b/test/scenarios/sessions/infinite-sessions/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/infinite-sessions/go/go.sum b/test/scenarios/sessions/infinite-sessions/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.sum +++ b/test/scenarios/sessions/infinite-sessions/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/sessions/session-resume/go/go.mod b/test/scenarios/sessions/session-resume/go/go.mod index ab1b82c398..9d87af8082 100644 --- a/test/scenarios/sessions/session-resume/go/go.mod +++ b/test/scenarios/sessions/session-resume/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/session-resume/go/go.sum b/test/scenarios/sessions/session-resume/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/sessions/session-resume/go/go.sum +++ b/test/scenarios/sessions/session-resume/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/sessions/streaming/go/go.mod b/test/scenarios/sessions/streaming/go/go.mod index f6c553680f..7e4c670045 100644 --- a/test/scenarios/sessions/streaming/go/go.mod +++ b/test/scenarios/sessions/streaming/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/streaming/go/go.sum b/test/scenarios/sessions/streaming/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/sessions/streaming/go/go.sum +++ b/test/scenarios/sessions/streaming/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/custom-agents/go/go.mod b/test/scenarios/tools/custom-agents/go/go.mod index f6f670b8cb..5b267a1f8d 100644 --- a/test/scenarios/tools/custom-agents/go/go.mod +++ b/test/scenarios/tools/custom-agents/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/custom-agents/go/go.sum b/test/scenarios/tools/custom-agents/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/custom-agents/go/go.sum +++ b/test/scenarios/tools/custom-agents/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/mcp-servers/go/go.mod b/test/scenarios/tools/mcp-servers/go/go.mod index 65de0a40b2..39050b710f 100644 --- a/test/scenarios/tools/mcp-servers/go/go.mod +++ b/test/scenarios/tools/mcp-servers/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/mcp-servers/go/go.sum b/test/scenarios/tools/mcp-servers/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/mcp-servers/go/go.sum +++ b/test/scenarios/tools/mcp-servers/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/no-tools/go/go.mod b/test/scenarios/tools/no-tools/go/go.mod index 387c1b51d9..678915fdac 100644 --- a/test/scenarios/tools/no-tools/go/go.mod +++ b/test/scenarios/tools/no-tools/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/no-tools/go/go.sum b/test/scenarios/tools/no-tools/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/no-tools/go/go.sum +++ b/test/scenarios/tools/no-tools/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/skills/go/go.mod b/test/scenarios/tools/skills/go/go.mod index ad94ef6b7c..a5e098a145 100644 --- a/test/scenarios/tools/skills/go/go.mod +++ b/test/scenarios/tools/skills/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/skills/go/go.sum b/test/scenarios/tools/skills/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/skills/go/go.sum +++ b/test/scenarios/tools/skills/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/tool-filtering/go/go.mod b/test/scenarios/tools/tool-filtering/go/go.mod index ad36d3f636..1084324fed 100644 --- a/test/scenarios/tools/tool-filtering/go/go.mod +++ b/test/scenarios/tools/tool-filtering/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-filtering/go/go.sum b/test/scenarios/tools/tool-filtering/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/tool-filtering/go/go.sum +++ b/test/scenarios/tools/tool-filtering/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/tool-overrides/go/go.mod b/test/scenarios/tools/tool-overrides/go/go.mod index ba48b0e7b4..49726e94b6 100644 --- a/test/scenarios/tools/tool-overrides/go/go.mod +++ b/test/scenarios/tools/tool-overrides/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-overrides/go/go.sum b/test/scenarios/tools/tool-overrides/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/tool-overrides/go/go.sum +++ b/test/scenarios/tools/tool-overrides/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/tools/virtual-filesystem/go/go.mod b/test/scenarios/tools/virtual-filesystem/go/go.mod index e5f1216118..38696a380e 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.mod +++ b/test/scenarios/tools/virtual-filesystem/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/virtual-filesystem/go/go.sum b/test/scenarios/tools/virtual-filesystem/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.sum +++ b/test/scenarios/tools/virtual-filesystem/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/transport/reconnect/go/go.mod b/test/scenarios/transport/reconnect/go/go.mod index e1267bb72f..a9a9a34ee5 100644 --- a/test/scenarios/transport/reconnect/go/go.mod +++ b/test/scenarios/transport/reconnect/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/reconnect/go/go.sum b/test/scenarios/transport/reconnect/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/transport/reconnect/go/go.sum +++ b/test/scenarios/transport/reconnect/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/transport/stdio/go/go.mod b/test/scenarios/transport/stdio/go/go.mod index 63ad24beef..ea51925118 100644 --- a/test/scenarios/transport/stdio/go/go.mod +++ b/test/scenarios/transport/stdio/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/stdio/go/go.sum b/test/scenarios/transport/stdio/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/transport/stdio/go/go.sum +++ b/test/scenarios/transport/stdio/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/scenarios/transport/tcp/go/go.mod b/test/scenarios/transport/tcp/go/go.mod index 85fac7926e..83ca00bc90 100644 --- a/test/scenarios/transport/tcp/go/go.mod +++ b/test/scenarios/transport/tcp/go/go.mod @@ -5,8 +5,14 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 require ( + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/uuid v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect ) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/tcp/go/go.sum b/test/scenarios/transport/tcp/go/go.sum index 6029a9b710..605b1f5d22 100644 --- a/test/scenarios/transport/tcp/go/go.sum +++ b/test/scenarios/transport/tcp/go/go.sum @@ -1,6 +1,27 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d71ef4c6e3c76218b098005b72f69dabd7fbdf1b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 13 Mar 2026 16:22:57 +0000 Subject: [PATCH 36/61] Update contribution guide (#843) --- CONTRIBUTING.md | 77 +++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4650ee04e3..7dbe1b4923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,60 +1,71 @@ -## Contributing +# Contributing -[fork]: https://github.com/github/copilot-sdk/fork -[pr]: https://github.com/github/copilot-sdk/compare +Thanks for your interest in contributing! -Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. +This repository contains the Copilot SDK, a set of multi-language SDKs (Node/TypeScript, Python, Go, .NET) for building applications with the GitHub Copilot agent, maintained by the GitHub Copilot team. Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE). Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. -## What kinds of contributions we're looking for +## Before You Submit a PR -We'd love your help with: +**Please discuss any feature work with us before writing code.** - * Fixing any bugs in the existing feature set - * Making the SDKs more idiomatic and nice to use for each supported language - * Improving documentation +The team already has a committed product roadmap, and features must be maintained in sync across all supported languages. Pull requests that introduce features not previously aligned with the team are unlikely to be accepted, regardless of their quality or scope. -If you have ideas for entirely new features, please post an issue or start a discussion. We're very open to new features but need to make sure they align with the direction of the underlying Copilot CLI and can be maintained in sync across all our supported languages. +If you submit a PR, **be sure to link to an associated issue describing the bug or agreed feature**. No PRs without context :) -Currently **we are not looking to add SDKs for other languages**. If you want to create a Copilot SDK for another language, we'd love to hear from you, and we may offer to link to your SDK from our repo. However we do not plan to add further language-specific SDKs to this repo in the short term, since we need to retain our maintenance capacity for moving forwards quickly with the existing language set. So, for any other languages, please consider running your own external project. +## What We're Looking For -## Prerequisites for running and testing code +We welcome: + +- Bug fixes with clear reproduction steps +- Improvements to documentation +- Making the SDKs more idiomatic and nice to use for each supported language +- Bug reports and feature suggestions on [our issue tracker](https://github.com/github/copilot-sdk/issues) — especially for bugs with repro steps + +We are generally **not** looking for: + +- New features, capabilities, or UX changes that haven't been discussed and agreed with the team +- Refactors or architectural changes +- Integrations with external tools or services +- Additional documentation +- **SDKs for other languages** — if you want to create a Copilot SDK for another language, we'd love to hear from you and may offer to link to your SDK from our repo. However we do not plan to add further language-specific SDKs to this repo in the short term, since we need to retain our maintenance capacity for moving forwards quickly with the existing language set. For other languages, please consider running your own external project. + +## Prerequisites for Running and Testing Code This is a multi-language SDK repository. Install the tools for the SDK(s) you plan to work on: ### All SDKs -1. (Optional) Install [just](https://github.com/casey/just) command runner for convenience + +1. The end-to-end tests across all languages use a shared test harness written in Node.js. Before running tests in any language, `cd test/harness && npm ci`. ### Node.js/TypeScript SDK + 1. Install [Node.js](https://nodejs.org/) (v18+) 1. Install dependencies: `cd nodejs && npm ci` ### Python SDK + 1. Install [Python 3.8+](https://www.python.org/downloads/) 1. Install [uv](https://github.com/astral-sh/uv) 1. Install dependencies: `cd python && uv pip install -e ".[dev]"` ### Go SDK + 1. Install [Go 1.24+](https://go.dev/doc/install) 1. Install [golangci-lint](https://golangci-lint.run/welcome/install/#local-installation) 1. Install dependencies: `cd go && go mod download` ### .NET SDK + 1. Install [.NET 8.0+](https://dotnet.microsoft.com/download) -1. Install [Node.js](https://nodejs.org/) (v18+) (the .NET tests depend on a TypeScript-based test harness) -1. Install npm dependencies (from the repository root): - ```bash - cd nodejs && npm ci - cd test/harness && npm ci - ``` 1. Install .NET dependencies: `cd dotnet && dotnet restore` -## Submitting a pull request +## Submitting a Pull Request -1. [Fork][fork] and clone the repository +1. Fork and clone the repository 1. Install dependencies for the SDK(s) you're modifying (see above) 1. Make sure the tests pass on your machine (see commands below) 1. Make sure linter passes on your machine (see commands below) @@ -63,29 +74,7 @@ This is a multi-language SDK repository. Install the tools for the SDK(s) you pl 1. Push to your fork and [submit a pull request][pr] 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. -### Running tests and linters - -If you installed `just`, you can use it to run tests and linters across all SDKs or for specific languages: - -```bash -# All SDKs -just test # Run all tests -just lint # Run all linters -just format # Format all code - -# Individual SDKs -just test-nodejs # Node.js tests -just test-python # Python tests -just test-go # Go tests -just test-dotnet # .NET tests - -just lint-nodejs # Node.js linting -just lint-python # Python linting -just lint-go # Go linting -just lint-dotnet # .NET linting -``` - -Or run commands directly in each SDK directory: +### Running Tests and Linters ```bash # Node.js From 38603f28cb6e892857c9f64209360447fcc1ee8b Mon Sep 17 00:00:00 2001 From: Stefan Sedich Date: Fri, 13 Mar 2026 09:33:49 -0700 Subject: [PATCH 37/61] fix: update Python examples to work OOTB (#784) * fix: update Python example to work OOTB * fix: update all examples * Update docs/getting-started.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/getting-started.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/getting-started.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/getting-started.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 1785928056..24e6c5b8af 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -129,15 +129,18 @@ Create `main.py`: ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({"model": "gpt-4.1"}) - response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) + session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": PermissionHandler.approve_all, + }) + response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) print(response.data.content) await client.stop() @@ -274,7 +277,7 @@ Update `main.py`: ```python import asyncio import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.generated.session_events import SessionEventType async def main(): @@ -283,6 +286,7 @@ async def main(): session = await client.create_session({ "model": "gpt-4.1", + "on_permission_request": PermissionHandler.approve_all, "streaming": True, }) @@ -653,7 +657,7 @@ Update `main.py`: import asyncio import random import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -678,6 +682,7 @@ async def main(): session = await client.create_session({ "model": "gpt-4.1", + "on_permission_request": PermissionHandler.approve_all, "streaming": True, "tools": [get_weather], }) @@ -925,7 +930,7 @@ Create `weather_assistant.py`: import asyncio import random import sys -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.tools import define_tool from copilot.generated.session_events import SessionEventType from pydantic import BaseModel, Field @@ -947,6 +952,7 @@ async def main(): session = await client.create_session({ "model": "gpt-4.1", + "on_permission_request": PermissionHandler.approve_all, "streaming": True, "tools": [get_weather], }) From a29dc1877f6947b95f13b127e9d2d5ab60249896 Mon Sep 17 00:00:00 2001 From: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:35:01 -0500 Subject: [PATCH 38/61] fix(dotnet): add fallback TypeInfoResolver for StreamJsonRpc.RequestId (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a CancellationToken fires during a JSON-RPC call, StreamJsonRpc's StandardCancellationStrategy.CancelOutboundRequest() attempts to serialize RequestId. The SDK's SystemTextJsonFormatter is configured with AOT-safe source-generated contexts, but none of them include StreamJsonRpc.RequestId. This causes a NotSupportedException that is unobserved by the JSON-RPC reader, silently killing the connection and leaving sessions permanently stuck at IsProcessing=true. The fix adds a StreamJsonRpcTypeInfoResolver — a reflection-based fallback IJsonTypeInfoResolver — as the last entry in the TypeInfoResolverChain. This ensures RequestId (and any other StreamJsonRpc-internal types) can be serialized when cancellation fires, without disrupting the AOT-safe source-generated path for all SDK-owned types. The reflection fallback is only reached for types not covered by the five source-generated contexts, so the AOT/trimming suppressions (IL2026, IL3050) are targeted and justified. Fixes: https://github.com/PureWeen/PolyPilot/issues/319 Affected versions: 0.1.30+ (introduced by NativeAOT work in PR #81) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 51 ++++++++++++++++++++ dotnet/test/SerializationTests.cs | 80 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 dotnet/test/SerializationTests.cs diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 40b96580f5..a9ad1fccde 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -14,6 +14,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Text.RegularExpressions; using GitHub.Copilot.SDK.Rpc; using System.Globalization; @@ -1254,6 +1255,12 @@ private static JsonSerializerOptions CreateSerializerOptions() options.TypeInfoResolverChain.Add(SessionEventsJsonContext.Default); options.TypeInfoResolverChain.Add(SDK.Rpc.RpcJsonContext.Default); + // StreamJsonRpc's RequestId needs serialization when CancellationToken fires during + // JSON-RPC operations. Its built-in converter (RequestIdSTJsonConverter) is internal, + // and [JsonSerializable] can't source-gen for it (SYSLIB1220), so we provide our own + // AOT-safe resolver + converter. + options.TypeInfoResolverChain.Add(new RequestIdTypeInfoResolver()); + options.MakeReadOnly(); return options; @@ -1687,6 +1694,50 @@ private static LogLevel MapLevel(TraceEventType eventType) [JsonSerializable(typeof(UserInputResponse))] internal partial class ClientJsonContext : JsonSerializerContext; + /// + /// AOT-safe type info resolver for . + /// StreamJsonRpc's own RequestIdSTJsonConverter is internal (SYSLIB1220/CS0122), + /// so we provide our own converter and wire it through + /// to stay fully AOT/trimming-compatible. + /// + private sealed class RequestIdTypeInfoResolver : IJsonTypeInfoResolver + { + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (type == typeof(RequestId)) + return JsonMetadataServices.CreateValueInfo(options, new RequestIdJsonConverter()); + return null; + } + } + + private sealed class RequestIdJsonConverter : JsonConverter + { + public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.TryGetInt64(out long val) + ? new RequestId(val) + : new RequestId(reader.HasValueSequence + ? Encoding.UTF8.GetString(reader.ValueSequence) + : Encoding.UTF8.GetString(reader.ValueSpan)), + JsonTokenType.String => new RequestId(reader.GetString()!), + JsonTokenType.Null => RequestId.Null, + _ => throw new JsonException($"Unexpected token type for RequestId: {reader.TokenType}"), + }; + } + + public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options) + { + if (value.Number.HasValue) + writer.WriteNumberValue(value.Number.Value); + else if (value.String is not null) + writer.WriteStringValue(value.String); + else + writer.WriteNullValue(); + } + } + [GeneratedRegex(@"listening on port ([0-9]+)", RegexOptions.IgnoreCase)] private static partial Regex ListeningOnPortRegex(); } diff --git a/dotnet/test/SerializationTests.cs b/dotnet/test/SerializationTests.cs new file mode 100644 index 0000000000..6fb266be15 --- /dev/null +++ b/dotnet/test/SerializationTests.cs @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using Xunit; +using System.Text.Json; +using System.Text.Json.Serialization; +using StreamJsonRpc; + +namespace GitHub.Copilot.SDK.Test; + +/// +/// Tests for JSON serialization compatibility, particularly for StreamJsonRpc types +/// that are needed when CancellationTokens fire during JSON-RPC operations. +/// This test suite verifies the fix for https://github.com/PureWeen/PolyPilot/issues/319 +/// +public class SerializationTests +{ + /// + /// Verifies that StreamJsonRpc.RequestId can be round-tripped using the SDK's configured + /// JsonSerializerOptions. This is critical for preventing NotSupportedException when + /// StandardCancellationStrategy fires during JSON-RPC operations. + /// + [Fact] + public void RequestId_CanBeSerializedAndDeserialized_WithSdkOptions() + { + var options = GetSerializerOptions(); + + // Long id + var jsonLong = JsonSerializer.Serialize(new RequestId(42L), options); + Assert.Equal("42", jsonLong); + Assert.Equal(new RequestId(42L), JsonSerializer.Deserialize(jsonLong, options)); + + // String id + var jsonStr = JsonSerializer.Serialize(new RequestId("req-1"), options); + Assert.Equal("\"req-1\"", jsonStr); + Assert.Equal(new RequestId("req-1"), JsonSerializer.Deserialize(jsonStr, options)); + + // Null id + var jsonNull = JsonSerializer.Serialize(RequestId.Null, options); + Assert.Equal("null", jsonNull); + Assert.Equal(RequestId.Null, JsonSerializer.Deserialize(jsonNull, options)); + } + + [Theory] + [InlineData(0L)] + [InlineData(-1L)] + [InlineData(long.MaxValue)] + public void RequestId_NumericEdgeCases_RoundTrip(long id) + { + var options = GetSerializerOptions(); + var requestId = new RequestId(id); + var json = JsonSerializer.Serialize(requestId, options); + Assert.Equal(requestId, JsonSerializer.Deserialize(json, options)); + } + + /// + /// Verifies the SDK's options can resolve type info for RequestId, + /// ensuring AOT-safe serialization without falling back to reflection. + /// + [Fact] + public void SerializerOptions_CanResolveRequestIdTypeInfo() + { + var options = GetSerializerOptions(); + var typeInfo = options.GetTypeInfo(typeof(RequestId)); + Assert.NotNull(typeInfo); + Assert.Equal(typeof(RequestId), typeInfo.Type); + } + + private static JsonSerializerOptions GetSerializerOptions() + { + var prop = typeof(CopilotClient) + .GetProperty("SerializerOptionsForMessageFormatter", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + var options = (JsonSerializerOptions?)prop?.GetValue(null); + Assert.NotNull(options); + return options; + } +} From a0bbde970ca8bda9df3b5463a1bfb502d607d4a4 Mon Sep 17 00:00:00 2001 From: Shane Neuville <5375137+PureWeen@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:37:43 -0500 Subject: [PATCH 39/61] fix(devcontainer): bump to v3 Python image to fix Yarn GPG key failure (#781) The v2 base image (mcr.microsoft.com/devcontainers/python:2-3.14-trixie) ships with a Yarn Classic APT repository whose GPG key has expired, causing apt-get update to fail during feature installation and breaking the entire devcontainer build. Bump to the v3 image (python:3-3.14-trixie, published 2026-01-30) which no longer includes the Yarn APT repo, resolving the issue without any additional workarounds. Ref: devcontainers/images#1797 --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index db0fd0fec5..fff64bcdbe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Python 3", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/python:2-3.14-trixie", + "image": "mcr.microsoft.com/devcontainers/python:3-3.14-trixie", "features": { "ghcr.io/devcontainers/features/copilot-cli:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, From ea90f076091371810c66d05590f65e2863f79bdf Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Fri, 13 Mar 2026 10:28:46 -0700 Subject: [PATCH 40/61] Add reasoningEffort to setModel/session.model.switchTo across all SDKs (#712) --- dotnet/src/Session.cs | 14 +++++++++-- dotnet/test/RpcTests.cs | 4 +-- dotnet/test/SessionTests.cs | 14 +++++++++++ go/internal/e2e/rpc_test.go | 8 +++--- go/internal/e2e/session_test.go | 43 +++++++++++++++++++++++++++++++++ go/session.go | 18 ++++++++++++-- nodejs/src/session.ts | 7 ++++-- nodejs/test/client.test.ts | 25 +++++++++++++++++++ nodejs/test/e2e/rpc.test.ts | 7 ++++-- nodejs/test/e2e/session.test.ts | 12 +++++++++ python/copilot/session.py | 12 +++++++-- python/e2e/test_rpc.py | 6 +++-- python/e2e/test_session.py | 22 +++++++++++++++++ 13 files changed, 175 insertions(+), 17 deletions(-) diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 07a818c211..606c0b052d 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -720,15 +720,25 @@ await InvokeRpcAsync( /// The new model takes effect for the next message. Conversation history is preserved. /// /// Model ID to switch to (e.g., "gpt-4.1"). + /// Reasoning effort level (e.g., "low", "medium", "high", "xhigh"). /// Optional cancellation token. /// /// /// await session.SetModelAsync("gpt-4.1"); + /// await session.SetModelAsync("claude-sonnet-4.6", "high"); /// /// - public async Task SetModelAsync(string model, CancellationToken cancellationToken = default) + public async Task SetModelAsync(string model, string? reasoningEffort, CancellationToken cancellationToken = default) { - await Rpc.Model.SwitchToAsync(model, cancellationToken: cancellationToken); + await Rpc.Model.SwitchToAsync(model, reasoningEffort, cancellationToken); + } + + /// + /// Changes the model for this session. + /// + public Task SetModelAsync(string model, CancellationToken cancellationToken = default) + { + return SetModelAsync(model, reasoningEffort: null, cancellationToken); } /// diff --git a/dotnet/test/RpcTests.cs b/dotnet/test/RpcTests.cs index a13695589d..e041033bd5 100644 --- a/dotnet/test/RpcTests.cs +++ b/dotnet/test/RpcTests.cs @@ -72,8 +72,8 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() var before = await session.Rpc.Model.GetCurrentAsync(); Assert.NotNull(before.ModelId); - // Switch to a different model - var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1"); + // Switch to a different model with reasoning effort + var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1", reasoningEffort: "high"); Assert.Equal("gpt-4.1", result.ModelId); // Verify the switch persisted diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index ea9d0da802..8cd4c84e55 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -440,6 +440,20 @@ public async Task Should_Set_Model_On_Existing_Session() Assert.Equal("gpt-4.1", modelChanged.Data.NewModel); } + [Fact] + public async Task Should_Set_Model_With_ReasoningEffort() + { + var session = await CreateSessionAsync(); + + var modelChangedTask = TestHelper.GetNextEventOfTypeAsync(session); + + await session.SetModelAsync("gpt-4.1", "high"); + + var modelChanged = await modelChangedTask; + Assert.Equal("gpt-4.1", modelChanged.Data.NewModel); + Assert.Equal("high", modelChanged.Data.ReasoningEffort); + } + [Fact] public async Task Should_Log_Messages_At_Various_Levels() { diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index 61a5e338d8..ebcbe1130d 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -168,9 +168,11 @@ func TestSessionRpc(t *testing.T) { t.Error("Expected initial modelId to be defined") } - // Switch to a different model + // Switch to a different model with reasoning effort + re := "high" result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.SessionModelSwitchToParams{ - ModelID: "gpt-4.1", + ModelID: "gpt-4.1", + ReasoningEffort: &re, }) if err != nil { t.Fatalf("Failed to switch model: %v", err) @@ -201,7 +203,7 @@ func TestSessionRpc(t *testing.T) { t.Fatalf("Failed to create session: %v", err) } - if err := session.SetModel(t.Context(), "gpt-4.1"); err != nil { + if err := session.SetModel(t.Context(), "gpt-4.1", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil { t.Fatalf("SetModel returned error: %v", err) } }) diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 4590301d04..c3c9cc0094 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -895,6 +895,49 @@ func getSystemMessage(exchange testharness.ParsedHttpExchange) string { return "" } +func TestSetModelWithReasoningEffort(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + modelChanged := make(chan copilot.SessionEvent, 1) + session.On(func(event copilot.SessionEvent) { + if event.Type == copilot.SessionModelChange { + select { + case modelChanged <- event: + default: + } + } + }) + + if err := session.SetModel(t.Context(), "gpt-4.1", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil { + t.Fatalf("SetModel returned error: %v", err) + } + + select { + case evt := <-modelChanged: + if evt.Data.NewModel == nil || *evt.Data.NewModel != "gpt-4.1" { + t.Errorf("Expected newModel 'gpt-4.1', got %v", evt.Data.NewModel) + } + if evt.Data.ReasoningEffort == nil || *evt.Data.ReasoningEffort != "high" { + t.Errorf("Expected reasoningEffort 'high', got %v", evt.Data.ReasoningEffort) + } + case <-time.After(30 * time.Second): + t.Fatal("Timed out waiting for session.model_change event") + } +} + func getToolNames(exchange testharness.ParsedHttpExchange) []string { var names []string for _, tool := range exchange.Request.Tools { diff --git a/go/session.go b/go/session.go index f7a1ba4ce5..d2a5785be1 100644 --- a/go/session.go +++ b/go/session.go @@ -737,6 +737,12 @@ func (s *Session) Abort(ctx context.Context) error { return nil } +// SetModelOptions configures optional parameters for SetModel. +type SetModelOptions struct { + // ReasoningEffort sets the reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh"). + ReasoningEffort string +} + // SetModel changes the model for this session. // The new model takes effect for the next message. Conversation history is preserved. // @@ -745,8 +751,16 @@ func (s *Session) Abort(ctx context.Context) error { // if err := session.SetModel(context.Background(), "gpt-4.1"); err != nil { // log.Printf("Failed to set model: %v", err) // } -func (s *Session) SetModel(ctx context.Context, model string) error { - _, err := s.RPC.Model.SwitchTo(ctx, &rpc.SessionModelSwitchToParams{ModelID: model}) +// if err := session.SetModel(context.Background(), "claude-sonnet-4.6", SetModelOptions{ReasoningEffort: "high"}); err != nil { +// log.Printf("Failed to set model: %v", err) +// } +func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOptions) error { + params := &rpc.SessionModelSwitchToParams{ModelID: model} + if len(opts) > 0 && opts[0].ReasoningEffort != "" { + re := opts[0].ReasoningEffort + params.ReasoningEffort = &re + } + _, err := s.RPC.Model.SwitchTo(ctx, params) if err != nil { return fmt.Errorf("failed to set model: %w", err) } diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index ed08326b19..674526764c 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -16,6 +16,7 @@ import type { PermissionHandler, PermissionRequest, PermissionRequestResult, + ReasoningEffort, SessionEvent, SessionEventHandler, SessionEventPayload, @@ -718,14 +719,16 @@ export class CopilotSession { * The new model takes effect for the next message. Conversation history is preserved. * * @param model - Model ID to switch to + * @param options - Optional settings for the new model * * @example * ```typescript * await session.setModel("gpt-4.1"); + * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); * ``` */ - async setModel(model: string): Promise { - await this.rpc.model.switchTo({ modelId: model }); + async setModel(model: string, options?: { reasoningEffort?: ReasoningEffort }): Promise { + await this.rpc.model.switchTo({ modelId: model, ...options }); } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index c8ae948893..3d13d27ffc 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -123,6 +123,31 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("sends reasoningEffort with session.model.switchTo when provided", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchTo") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "claude-sonnet-4.6", + reasoningEffort: "high", + }); + + spy.mockRestore(); + }); + describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ diff --git a/nodejs/test/e2e/rpc.test.ts b/nodejs/test/e2e/rpc.test.ts index 62a885d050..d4d732efd1 100644 --- a/nodejs/test/e2e/rpc.test.ts +++ b/nodejs/test/e2e/rpc.test.ts @@ -92,8 +92,11 @@ describe("Session RPC", async () => { const before = await session.rpc.model.getCurrent(); expect(before.modelId).toBeDefined(); - // Switch to a different model - const result = await session.rpc.model.switchTo({ modelId: "gpt-4.1" }); + // Switch to a different model with reasoning effort + const result = await session.rpc.model.switchTo({ + modelId: "gpt-4.1", + reasoningEffort: "high", + }); expect(result.modelId).toBe("gpt-4.1"); // Verify the switch persisted diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 0ad60edca9..1eb8a175d5 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -461,4 +461,16 @@ describe("Send Blocking Behavior", async () => { session.sendAndWait({ prompt: "Run 'sleep 2 && echo done'" }, 100) ).rejects.toThrow(/Timeout after 100ms/); }); + + it("should set model with reasoningEffort", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const modelChangePromise = getNextEventOfType(session, "session.model_change"); + + await session.setModel("gpt-4.1", { reasoningEffort: "high" }); + + const event = await modelChangePromise; + expect(event.data.newModel).toBe("gpt-4.1"); + expect(event.data.reasoningEffort).toBe("high"); + }); }); diff --git a/python/copilot/session.py b/python/copilot/session.py index ad049811c8..cf09cb287a 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -728,7 +728,7 @@ async def abort(self) -> None: """ await self._client.request("session.abort", {"sessionId": self.session_id}) - async def set_model(self, model: str) -> None: + async def set_model(self, model: str, *, reasoning_effort: str | None = None) -> None: """ Change the model for this session. @@ -737,14 +737,22 @@ async def set_model(self, model: str) -> None: Args: model: Model ID to switch to (e.g., "gpt-4.1", "claude-sonnet-4"). + reasoning_effort: Optional reasoning effort level for the new model + (e.g., "low", "medium", "high", "xhigh"). Raises: Exception: If the session has been destroyed or the connection fails. Example: >>> await session.set_model("gpt-4.1") + >>> await session.set_model("claude-sonnet-4.6", reasoning_effort="high") """ - await self.rpc.model.switch_to(SessionModelSwitchToParams(model_id=model)) + await self.rpc.model.switch_to( + SessionModelSwitchToParams( + model_id=model, + reasoning_effort=reasoning_effort, + ) + ) async def log( self, diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 0db2b4fe0a..ddf843ba45 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -99,8 +99,10 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext before = await session.rpc.model.get_current() assert before.model_id is not None - # Switch to a different model - result = await session.rpc.model.switch_to(SessionModelSwitchToParams(model_id="gpt-4.1")) + # Switch to a different model with reasoning effort + result = await session.rpc.model.switch_to( + SessionModelSwitchToParams(model_id="gpt-4.1", reasoning_effort="high") + ) assert result.model_id == "gpt-4.1" # Verify the switch persisted diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index a779fd079a..9e663fcc57 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -558,6 +558,28 @@ def on_event(event): assert by_message["Ephemeral message"].type.value == "session.info" assert by_message["Ephemeral message"].data.info_type == "notification" + async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext): + """Test that setModel passes reasoningEffort and it appears in the model_change event.""" + import asyncio + + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + model_change_event = asyncio.get_event_loop().create_future() + + def on_event(event): + if not model_change_event.done() and event.type.value == "session.model_change": + model_change_event.set_result(event) + + session.on(on_event) + + await session.set_model("gpt-4.1", reasoning_effort="high") + + event = await asyncio.wait_for(model_change_event, timeout=30) + assert event.data.new_model == "gpt-4.1" + assert event.data.reasoning_effort == "high" + def _get_system_message(exchange: dict) -> str: messages = exchange.get("request", {}).get("messages", []) From cb1ea2d1fd782dd62400fdfd34cae9315f37de24 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 02:12:06 -0700 Subject: [PATCH 41/61] Update installation instructions for telemetry support (#853) --- python/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/README.md b/python/README.md index 9d83ae650d..f609db8d6c 100644 --- a/python/README.md +++ b/python/README.md @@ -7,9 +7,9 @@ Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC. ## Installation ```bash -pip install -e ".[dev]" +pip install -e ".[telemetry,dev]" # or -uv pip install -e ".[dev]" +uv pip install -e ".[telemetry,dev]" ``` ## Run the Sample From c9160baa6c6d2ab19f59422003ddd27e896ea1e0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 02:20:25 -0700 Subject: [PATCH 42/61] [Python] Update the signatures of `CopilotSession.send()` and `send_and_wait()` (#814) * Update the signatures of `CopilotSession.send()` and `send_and_wait()` * Fix some code that didn't get migrated from the last merge with main * Add better typing --- python/README.md | 22 +++--- python/copilot/__init__.py | 2 - python/copilot/client.py | 4 +- python/copilot/session.py | 68 +++++++++++-------- python/copilot/types.py | 11 --- python/e2e/test_agent_and_compact_rpc.py | 2 +- python/e2e/test_ask_user.py | 24 ++----- python/e2e/test_compaction.py | 12 ++-- python/e2e/test_hooks.py | 12 ++-- python/e2e/test_mcp_and_agents.py | 20 +++--- python/e2e/test_multi_client.py | 40 +++-------- python/e2e/test_permissions.py | 28 +++----- python/e2e/test_session.py | 49 ++++++------- python/e2e/test_skills.py | 8 +-- python/e2e/test_streaming_fidelity.py | 10 ++- python/e2e/test_tools.py | 22 +++--- python/samples/chat.py | 2 +- .../auth/byok-anthropic/python/main.py | 2 +- test/scenarios/auth/byok-azure/python/main.py | 2 +- .../scenarios/auth/byok-ollama/python/main.py | 2 +- .../scenarios/auth/byok-openai/python/main.py | 2 +- test/scenarios/auth/gh-app/python/main.py | 2 +- .../app-backend-to-server/python/main.py | 2 +- .../bundling/app-direct-server/python/main.py | 2 +- .../bundling/container-proxy/python/main.py | 2 +- .../bundling/fully-bundled/python/main.py | 2 +- test/scenarios/callbacks/hooks/python/main.py | 4 +- .../callbacks/permissions/python/main.py | 4 +- .../callbacks/user-input/python/main.py | 8 +-- test/scenarios/modes/default/python/main.py | 2 +- test/scenarios/modes/minimal/python/main.py | 2 +- .../prompts/attachments/python/main.py | 6 +- .../prompts/reasoning-effort/python/main.py | 2 +- .../prompts/system-message/python/main.py | 2 +- .../concurrent-sessions/python/main.py | 4 +- .../sessions/infinite-sessions/python/main.py | 2 +- .../sessions/session-resume/python/main.py | 4 +- .../sessions/streaming/python/main.py | 2 +- .../tools/custom-agents/python/main.py | 2 +- .../tools/mcp-servers/python/main.py | 2 +- test/scenarios/tools/no-tools/python/main.py | 2 +- test/scenarios/tools/skills/python/main.py | 2 +- .../tools/tool-filtering/python/main.py | 2 +- .../tools/tool-overrides/python/main.py | 2 +- .../tools/virtual-filesystem/python/main.py | 8 +-- .../transport/reconnect/python/main.py | 4 +- test/scenarios/transport/stdio/python/main.py | 2 +- test/scenarios/transport/tcp/python/main.py | 2 +- 48 files changed, 173 insertions(+), 251 deletions(-) diff --git a/python/README.md b/python/README.md index f609db8d6c..6d1c81281c 100644 --- a/python/README.md +++ b/python/README.md @@ -47,7 +47,7 @@ async def main(): session.on(on_event) # Send a message and wait for completion - await session.send({"prompt": "What is 2+2?"}) + await session.send("What is 2+2?") await done.wait() # Clean up @@ -61,7 +61,7 @@ Sessions also support the `async with` context manager pattern for automatic cle ```python async with await client.create_session({"model": "gpt-5"}) as session: - await session.send({"prompt": "What is 2+2?"}) + await session.send("What is 2+2?") # session is automatically disconnected when leaving the block ``` @@ -91,7 +91,7 @@ def on_event(event): print(f"Event: {event['type']}") session.on(on_event) -await session.send({"prompt": "Hello!"}) +await session.send("Hello!") # ... wait for events ... @@ -266,21 +266,21 @@ async def safe_lookup(params: LookupParams) -> str: The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: ```python -await session.send({ - "prompt": "What's in this image?", - "attachments": [ +await session.send( + "What's in this image?", + attachments=[ { "type": "file", "path": "/path/to/image.jpg", } - ] -}) + ], +) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: ```python -await session.send({"prompt": "What does the most recent jpg in this directory portray?"}) +await session.send("What does the most recent jpg in this directory portray?") ``` ## Streaming @@ -325,7 +325,7 @@ async def main(): done.set() session.on(on_event) - await session.send({"prompt": "Tell me a short story"}) + await session.send("Tell me a short story") await done.wait() # Wait for streaming to complete await session.disconnect() @@ -402,7 +402,7 @@ session = await client.create_session({ }, }) -await session.send({"prompt": "Hello!"}) +await session.send("Hello!") ``` **Example with custom OpenAI-compatible API:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index e0f627c70d..c25ea4021e 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -17,7 +17,6 @@ MCPLocalServerConfig, MCPRemoteServerConfig, MCPServerConfig, - MessageOptions, ModelBilling, ModelCapabilities, ModelInfo, @@ -56,7 +55,6 @@ "MCPLocalServerConfig", "MCPRemoteServerConfig", "MCPServerConfig", - "MessageOptions", "ModelBilling", "ModelCapabilities", "ModelInfo", diff --git a/python/copilot/client.py b/python/copilot/client.py index 29cdf81dca..c1fe3c9df1 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -9,7 +9,7 @@ >>> >>> async with CopilotClient() as client: ... session = await client.create_session() - ... await session.send({"prompt": "Hello!"}) + ... await session.send("Hello!") """ import asyncio @@ -104,7 +104,7 @@ class CopilotClient: ... "model": "gpt-4", ... }) >>> session.on(lambda event: print(event.type)) - >>> await session.send({"prompt": "Hello!"}) + >>> await session.send("Hello!") >>> >>> # Clean up >>> await session.disconnect() diff --git a/python/copilot/session.py b/python/copilot/session.py index cf09cb287a..e4a17f2f93 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -9,7 +9,7 @@ import inspect import threading from collections.abc import Callable -from typing import Any, cast +from typing import Any, Literal, cast from .generated.rpc import ( Kind, @@ -26,7 +26,7 @@ from .jsonrpc import JsonRpcError, ProcessExitedError from .telemetry import get_trace_context, trace_context from .types import ( - MessageOptions, + Attachment, PermissionRequest, PermissionRequestResult, SessionHooks, @@ -64,7 +64,7 @@ class CopilotSession: ... unsubscribe = session.on(lambda event: print(event.type)) ... ... # Send a message - ... await session.send({"prompt": "Hello, world!"}) + ... await session.send("Hello, world!") ... ... # Clean up ... unsubscribe() @@ -116,45 +116,57 @@ def workspace_path(self) -> str | None: """ return self._workspace_path - async def send(self, options: MessageOptions) -> str: + async def send( + self, + prompt: str, + *, + attachments: list[Attachment] | None = None, + mode: Literal["enqueue", "immediate"] | None = None, + ) -> str: """ - Send a message to this session and wait for the response. + Send a message to this session. The message is processed asynchronously. Subscribe to events via :meth:`on` - to receive streaming responses and other session events. + to receive streaming responses and other session events. Use + :meth:`send_and_wait` to block until the assistant finishes processing. Args: - options: Message options including the prompt and optional attachments. - Must contain a "prompt" key with the message text. Can optionally - include "attachments" and "mode" keys. + prompt: The message text to send. + attachments: Optional file, directory, or selection attachments. + mode: Message delivery mode (``"enqueue"`` or ``"immediate"``). Returns: - The message ID of the response, which can be used to correlate events. + The message ID assigned by the server, which can be used to correlate events. Raises: Exception: If the session has been disconnected or the connection fails. Example: - >>> message_id = await session.send({ - ... "prompt": "Explain this code", - ... "attachments": [{"type": "file", "path": "./src/main.py"}] - ... }) + >>> message_id = await session.send( + ... "Explain this code", + ... attachments=[{"type": "file", "path": "./src/main.py"}], + ... ) """ params: dict[str, Any] = { "sessionId": self.session_id, - "prompt": options["prompt"], + "prompt": prompt, } - if "attachments" in options: - params["attachments"] = options["attachments"] - if "mode" in options: - params["mode"] = options["mode"] + if attachments is not None: + params["attachments"] = attachments + if mode is not None: + params["mode"] = mode params.update(get_trace_context()) response = await self._client.request("session.send", params) return response["messageId"] async def send_and_wait( - self, options: MessageOptions, timeout: float | None = None + self, + prompt: str, + *, + attachments: list[Attachment] | None = None, + mode: Literal["enqueue", "immediate"] | None = None, + timeout: float = 60.0, ) -> SessionEvent | None: """ Send a message to this session and wait until the session becomes idle. @@ -166,7 +178,9 @@ async def send_and_wait( Events are still delivered to handlers registered via :meth:`on` while waiting. Args: - options: Message options including the prompt and optional attachments. + prompt: The message text to send. + attachments: Optional file, directory, or selection attachments. + mode: Message delivery mode (``"enqueue"`` or ``"immediate"``). timeout: Timeout in seconds (default: 60). Controls how long to wait; does not abort in-flight agent work. @@ -178,12 +192,10 @@ async def send_and_wait( Exception: If the session has been disconnected or the connection fails. Example: - >>> response = await session.send_and_wait({"prompt": "What is 2+2?"}) + >>> response = await session.send_and_wait("What is 2+2?") >>> if response: ... print(response.data.content) """ - effective_timeout = timeout if timeout is not None else 60.0 - idle_event = asyncio.Event() error_event: Exception | None = None last_assistant_message: SessionEvent | None = None @@ -202,13 +214,13 @@ def handler(event: SessionEventTypeAlias) -> None: unsubscribe = self.on(handler) try: - await self.send(options) - await asyncio.wait_for(idle_event.wait(), timeout=effective_timeout) + await self.send(prompt, attachments=attachments, mode=mode) + await asyncio.wait_for(idle_event.wait(), timeout=timeout) if error_event: raise error_event return last_assistant_message except TimeoutError: - raise TimeoutError(f"Timeout after {effective_timeout}s waiting for session.idle") + raise TimeoutError(f"Timeout after {timeout}s waiting for session.idle") finally: unsubscribe() @@ -719,7 +731,7 @@ async def abort(self) -> None: >>> >>> # Start a long-running request >>> task = asyncio.create_task( - ... session.send({"prompt": "Write a very long story..."}) + ... session.send("Write a very long story...") ... ) >>> >>> # Abort after 5 seconds diff --git a/python/copilot/types.py b/python/copilot/types.py index e572e751b6..af124bb0a9 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -654,17 +654,6 @@ class ResumeSessionConfig(TypedDict, total=False): on_event: Callable[[SessionEvent], None] -# Options for sending a message to a session -class MessageOptions(TypedDict): - """Options for sending a message to a session""" - - prompt: str # The prompt/message to send - # Optional file/directory attachments - attachments: NotRequired[list[Attachment]] - # Message processing mode - mode: NotRequired[Literal["enqueue", "immediate"]] - - # Event handler type SessionEventHandler = Callable[[SessionEvent], None] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 6eb07f64cd..ec59586767 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -182,7 +182,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC ) # Send a message to create some history - await session.send_and_wait({"prompt": "What is 2+2?"}) + await session.send_and_wait("What is 2+2?") # Compact the session result = await session.rpc.compaction.compact() diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index bddc062df1..b9800156be 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -37,12 +37,8 @@ async def on_user_input_request(request, invocation): ) await session.send_and_wait( - { - "prompt": ( - "Ask me to choose between 'Option A' and 'Option B' using the ask_user " - "tool. Wait for my response before continuing." - ) - } + "Ask me to choose between 'Option A' and 'Option B' using the ask_user " + "tool. Wait for my response before continuing." ) # Should have received at least one user input request @@ -76,12 +72,8 @@ async def on_user_input_request(request, invocation): ) await session.send_and_wait( - { - "prompt": ( - "Use the ask_user tool to ask me to pick between exactly two options: " - "'Red' and 'Blue'. These should be provided as choices. Wait for my answer." - ) - } + "Use the ask_user tool to ask me to pick between exactly two options: " + "'Red' and 'Blue'. These should be provided as choices. Wait for my answer." ) # Should have received a request @@ -117,12 +109,8 @@ async def on_user_input_request(request, invocation): ) response = await session.send_and_wait( - { - "prompt": ( - "Ask me a question using ask_user and then include my answer in your " - "response. The question should be 'What is your favorite color?'" - ) - } + "Ask me a question using ask_user and then include my answer in your " + "response. The question should be 'What is your favorite color?'" ) # Should have received a request diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index 5447b4badf..1310407056 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -41,13 +41,11 @@ def on_event(event): session.on(on_event) # Send multiple messages to fill up the context window - await session.send_and_wait({"prompt": "Tell me a story about a dragon. Be detailed."}) + await session.send_and_wait("Tell me a story about a dragon. Be detailed.") await session.send_and_wait( - {"prompt": "Continue the story with more details about the dragon's castle."} - ) - await session.send_and_wait( - {"prompt": "Now describe the dragon's treasure in great detail."} + "Continue the story with more details about the dragon's castle." ) + await session.send_and_wait("Now describe the dragon's treasure in great detail.") # Should have triggered compaction at least once assert len(compaction_start_events) >= 1, "Expected at least 1 compaction_start event" @@ -62,7 +60,7 @@ def on_event(event): assert last_complete.data.tokens_removed > 0, "Expected tokensRemoved > 0" # Verify the session still works after compaction - answer = await session.send_and_wait({"prompt": "What was the story about?"}) + answer = await session.send_and_wait("What was the story about?") assert answer is not None assert answer.data.content is not None # Should remember it was about a dragon (context preserved via summary) @@ -89,7 +87,7 @@ def on_event(event): session.on(on_event) - await session.send_and_wait({"prompt": "What is 2+2?"}) + await session.send_and_wait("What is 2+2?") # Should not have any compaction events when disabled assert len(compaction_events) == 0, "Expected no compaction events when disabled" diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index c886c6e279..a4956482c6 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -33,9 +33,7 @@ async def on_pre_tool_use(input_data, invocation): # Create a file for the model to read write_file(ctx.work_dir, "hello.txt", "Hello from the test!") - await session.send_and_wait( - {"prompt": "Read the contents of hello.txt and tell me what it says"} - ) + await session.send_and_wait("Read the contents of hello.txt and tell me what it says") # Should have received at least one preToolUse hook call assert len(pre_tool_use_inputs) > 0 @@ -66,9 +64,7 @@ async def on_post_tool_use(input_data, invocation): # Create a file for the model to read write_file(ctx.work_dir, "world.txt", "World from the test!") - await session.send_and_wait( - {"prompt": "Read the contents of world.txt and tell me what it says"} - ) + await session.send_and_wait("Read the contents of world.txt and tell me what it says") # Should have received at least one postToolUse hook call assert len(post_tool_use_inputs) > 0 @@ -106,7 +102,7 @@ async def on_post_tool_use(input_data, invocation): write_file(ctx.work_dir, "both.txt", "Testing both hooks!") - await session.send_and_wait({"prompt": "Read the contents of both.txt"}) + await session.send_and_wait("Read the contents of both.txt") # Both hooks should have been called assert len(pre_tool_use_inputs) > 0 @@ -143,7 +139,7 @@ async def on_pre_tool_use(input_data, invocation): write_file(ctx.work_dir, "protected.txt", original_content) response = await session.send_and_wait( - {"prompt": "Edit protected.txt and replace 'Original' with 'Modified'"} + "Edit protected.txt and replace 'Original' with 'Modified'" ) # The hook should have been called diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index fd99cc2c3b..8fffbe889a 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -39,7 +39,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( assert session.session_id is not None # Simple interaction to verify session works - message = await session.send_and_wait({"prompt": "What is 2+2?"}) + message = await session.send_and_wait("What is 2+2?") assert message is not None assert "4" in message.data.content @@ -54,7 +54,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with MCP servers mcp_servers: dict[str, MCPServerConfig] = { @@ -73,7 +73,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( assert session2.session_id == session_id - message = await session2.send_and_wait({"prompt": "What is 3+3?"}) + message = await session2.send_and_wait("What is 3+3?") assert message is not None assert "6" in message.data.content @@ -104,10 +104,8 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( assert session.session_id is not None message = await session.send_and_wait( - { - "prompt": "Use the env-echo/get_env tool to read the TEST_SECRET " - "environment variable. Reply with just the value, nothing else." - } + "Use the env-echo/get_env tool to read the TEST_SECRET " + "environment variable. Reply with just the value, nothing else." ) assert message is not None assert "hunter2" in message.data.content @@ -137,7 +135,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( assert session.session_id is not None # Simple interaction to verify session works - message = await session.send_and_wait({"prompt": "What is 5+5?"}) + message = await session.send_and_wait("What is 5+5?") assert message is not None assert "10" in message.data.content @@ -152,7 +150,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with custom agents custom_agents: list[CustomAgentConfig] = [ @@ -174,7 +172,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( assert session2.session_id == session_id - message = await session2.send_and_wait({"prompt": "What is 6+6?"}) + message = await session2.send_and_wait("What is 6+6?") assert message is not None assert "12" in message.data.content @@ -212,7 +210,7 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe assert session.session_id is not None - await session.send({"prompt": "What is 7+7?"}) + await session.send("What is 7+7?") message = await get_final_assistant_message(session) assert "14" in message.data.content diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index 5131ad2bdf..cb5d90cd2f 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -214,9 +214,7 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: session2.on(lambda event: client2_events.append(event)) # Send a prompt that triggers the custom tool - await session1.send( - {"prompt": "Use the magic_number tool with seed 'hello' and tell me the result"} - ) + await session1.send("Use the magic_number tool with seed 'hello' and tell me the result") response = await get_final_assistant_message(session1) assert "MAGIC_hello_42" in (response.data.content or "") @@ -261,9 +259,7 @@ async def test_one_client_approves_permission_and_both_see_the_result( session2.on(lambda event: client2_events.append(event)) # Send a prompt that triggers a write operation (requires permission) - await session1.send( - {"prompt": "Create a file called hello.txt containing the text 'hello world'"} - ) + await session1.send("Create a file called hello.txt containing the text 'hello world'") response = await get_final_assistant_message(session1) assert response.data.content @@ -315,7 +311,7 @@ async def test_one_client_rejects_permission_and_both_see_the_result( with open(test_file, "w") as f: f.write("protected content") - await session1.send({"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."}) + await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.") await get_final_assistant_message(session1) # Verify the file was NOT modified (permission was denied) @@ -370,17 +366,13 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st # Send prompts sequentially to avoid nondeterministic tool_call ordering await session1.send( - {"prompt": "Use the city_lookup tool with countryCode 'US' and tell me the result."} + "Use the city_lookup tool with countryCode 'US' and tell me the result." ) response1 = await get_final_assistant_message(session1) assert "CITY_FOR_US" in (response1.data.content or "") await session1.send( - { - "prompt": ( - "Now use the currency_lookup tool with countryCode 'US' and tell me the result." - ) - } + "Now use the currency_lookup tool with countryCode 'US' and tell me the result." ) response2 = await get_final_assistant_message(session1) assert "CURRENCY_FOR_US" in (response2.data.content or "") @@ -421,19 +413,11 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Verify both tools work before disconnect. # Sequential prompts avoid nondeterministic tool_call ordering. - await session1.send( - { - "prompt": "Use the stable_tool with input 'test1' and tell me the result.", - } - ) + await session1.send("Use the stable_tool with input 'test1' and tell me the result.") stable_response = await get_final_assistant_message(session1) assert "STABLE_test1" in (stable_response.data.content or "") - await session1.send( - { - "prompt": "Use the ephemeral_tool with input 'test2' and tell me the result.", - } - ) + await session1.send("Use the ephemeral_tool with input 'test2' and tell me the result.") ephemeral_response = await get_final_assistant_message(session1) assert "EPHEMERAL_test2" in (ephemeral_response.data.content or "") @@ -449,13 +433,9 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Now only stable_tool should be available await session1.send( - { - "prompt": ( - "Use the stable_tool with input 'still_here'." - " Also try using ephemeral_tool" - " if it is available." - ) - } + "Use the stable_tool with input 'still_here'." + " Also try using ephemeral_tool" + " if it is available." ) after_response = await get_final_assistant_message(session1) assert "STABLE_still_here" in (after_response.data.content or "") diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index 609003e876..d18b15b2da 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -30,9 +30,7 @@ def on_permission_request( write_file(ctx.work_dir, "test.txt", "original content") - await session.send_and_wait( - {"prompt": "Edit test.txt and replace 'original' with 'modified'"} - ) + await session.send_and_wait("Edit test.txt and replace 'original' with 'modified'") # Should have received at least one permission request assert len(permission_requests) > 0 @@ -56,9 +54,7 @@ def on_permission_request( original_content = "protected content" write_file(ctx.work_dir, "protected.txt", original_content) - await session.send_and_wait( - {"prompt": "Edit protected.txt and replace 'protected' with 'hacked'."} - ) + await session.send_and_wait("Edit protected.txt and replace 'protected' with 'hacked'.") # Verify the file was NOT modified content = read_file(ctx.work_dir, "protected.txt") @@ -94,7 +90,7 @@ def on_event(event): session.on(on_event) - await session.send({"prompt": "Run 'node --version'"}) + await session.send("Run 'node --version'") await asyncio.wait_for(done_event.wait(), timeout=60) assert len(denied_events) > 0 @@ -109,7 +105,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") def deny_all(request, invocation): return PermissionRequestResult() @@ -134,7 +130,7 @@ def on_event(event): session2.on(on_event) - await session2.send({"prompt": "Run 'node --version'"}) + await session2.send("Run 'node --version'") await asyncio.wait_for(done_event.wait(), timeout=60) assert len(denied_events) > 0 @@ -147,7 +143,7 @@ async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETes {"on_permission_request": PermissionHandler.approve_all} ) - message = await session.send_and_wait({"prompt": "What is 2+2?"}) + message = await session.send_and_wait("What is 2+2?") assert message is not None assert "4" in message.data.content @@ -168,7 +164,7 @@ async def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - await session.send_and_wait({"prompt": "Run 'echo test' and tell me what happens"}) + await session.send_and_wait("Run 'echo test' and tell me what happens") assert len(permission_requests) > 0 @@ -183,7 +179,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - await session1.send_and_wait({"prompt": "What is 1+1?"}) + await session1.send_and_wait("What is 1+1?") # Resume with permission handler def on_permission_request( @@ -196,7 +192,7 @@ def on_permission_request( session_id, {"on_permission_request": on_permission_request} ) - await session2.send_and_wait({"prompt": "Run 'echo resumed' for me"}) + await session2.send_and_wait("Run 'echo resumed' for me") # Should have permission requests from resumed session assert len(permission_requests) > 0 @@ -213,9 +209,7 @@ def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - message = await session.send_and_wait( - {"prompt": "Run 'echo test'. If you can't, say 'failed'."} - ) + message = await session.send_and_wait("Run 'echo test'. If you can't, say 'failed'.") # Should handle the error and deny permission assert message is not None @@ -240,7 +234,7 @@ def on_permission_request( session = await ctx.client.create_session({"on_permission_request": on_permission_request}) - await session.send_and_wait({"prompt": "Run 'echo test'"}) + await session.send_and_wait("Run 'echo test'") assert received_tool_call_id diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 9e663fcc57..a2bc33bdb0 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -35,13 +35,11 @@ async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): {"on_permission_request": PermissionHandler.approve_all} ) - assistant_message = await session.send_and_wait({"prompt": "What is 1+1?"}) + assistant_message = await session.send_and_wait("What is 1+1?") assert assistant_message is not None assert "2" in assistant_message.data.content - second_message = await session.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + second_message = await session.send_and_wait("Now if you double that, what do you get?") assert second_message is not None assert "4" in second_message.data.content @@ -56,7 +54,7 @@ async def test_should_create_a_session_with_appended_systemMessage_config( } ) - await session.send({"prompt": "What is your full name?"}) + await session.send("What is your full name?") assistant_message = await get_final_assistant_message(session) assert "GitHub" in assistant_message.data.content assert "Have a nice day!" in assistant_message.data.content @@ -78,7 +76,7 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( } ) - await session.send({"prompt": "What is your full name?"}) + await session.send("What is your full name?") assistant_message = await get_final_assistant_message(session) assert "GitHub" not in assistant_message.data.content assert "Testy" in assistant_message.data.content @@ -96,7 +94,7 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon } ) - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") await get_final_assistant_message(session) # It only tells the model about the specified tools and no others @@ -112,7 +110,7 @@ async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestCont {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") await get_final_assistant_message(session) # It has other tools, but not the one we excluded @@ -160,7 +158,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) + answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content @@ -173,9 +171,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC assert "2" in answer2.data.content # Can continue the conversation statefully - answer3 = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer3 = await session2.send_and_wait("Now if you double that, what do you get?") assert answer3 is not None assert "4" in answer3.data.content @@ -185,7 +181,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont {"on_permission_request": PermissionHandler.approve_all} ) session_id = session1.session_id - answer = await session1.send_and_wait({"prompt": "What is 1+1?"}) + answer = await session1.send_and_wait("What is 1+1?") assert answer is not None assert "2" in answer.data.content @@ -214,9 +210,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont assert "session.resume" in message_types # Can continue the conversation statefully - answer2 = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer2 = await session2.send_and_wait("Now if you double that, what do you get?") assert answer2 is not None assert "4" in answer2.data.content finally: @@ -235,11 +229,11 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): session1 = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session1.send_and_wait({"prompt": "Say hello"}) + await session1.send_and_wait("Say hello") session2 = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session2.send_and_wait({"prompt": "Say goodbye"}) + await session2.send_and_wait("Say goodbye") # Small delay to ensure session files are written to disk await asyncio.sleep(0.2) @@ -278,7 +272,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): session = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "Hello"}) + await session.send_and_wait("Hello") session_id = session.session_id # Small delay to ensure session file is written to disk @@ -310,7 +304,7 @@ async def test_should_get_last_session_id(self, ctx: E2ETestContext): session = await ctx.client.create_session( {"on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "Say hello"}) + await session.send_and_wait("Say hello") # Small delay to ensure session data is flushed to disk await asyncio.sleep(0.5) @@ -347,7 +341,7 @@ def get_secret_number_handler(invocation): } ) - answer = await session.send_and_wait({"prompt": "What is the secret number for key ALPHA?"}) + answer = await session.send_and_wait("What is the secret number for key ALPHA?") assert answer is not None assert "54321" in answer.data.content @@ -418,12 +412,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): # Send a message that will trigger a long-running shell command await session.send( - { - "prompt": ( - "run the shell command 'sleep 100' " - "(note this works on both bash and PowerShell)" - ) - } + "run the shell command 'sleep 100' (note this works on both bash and PowerShell)" ) # Wait for the tool to start executing @@ -444,7 +433,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): assert len(abort_events) > 0, "Expected an abort event in messages" # We should be able to send another message - answer = await session.send_and_wait({"prompt": "What is 2+2?"}) + answer = await session.send_and_wait("What is 2+2?") assert "4" in answer.data.content async def test_should_receive_session_events(self, ctx: E2ETestContext): @@ -478,7 +467,7 @@ def on_event(event): session.on(on_event) # Send a message to trigger events - await session.send({"prompt": "What is 100+200?"}) + await session.send("What is 100+200?") # Wait for session to become idle try: @@ -511,7 +500,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo assert session.session_id # Session should work normally with custom config dir - await session.send({"prompt": "What is 1+1?"}) + await session.send("What is 1+1?") assistant_message = await get_final_assistant_message(session) assert "2" in assistant_message.data.content diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 166840e578..066669f297 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -65,7 +65,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET assert session.session_id is not None # The skill instructs the model to include a marker - verify it appears - message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."}) + message = await session.send_and_wait("Say hello briefly using the test skill.") assert message is not None assert SKILL_MARKER in message.data.content @@ -87,7 +87,7 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( assert session.session_id is not None # The skill is disabled, so the marker should NOT appear - message = await session.send_and_wait({"prompt": "Say hello briefly using the test skill."}) + message = await session.send_and_wait("Say hello briefly using the test skill.") assert message is not None assert SKILL_MARKER not in message.data.content @@ -110,7 +110,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( session_id = session1.session_id # First message without skill - marker should not appear - message1 = await session1.send_and_wait({"prompt": "Say hi."}) + message1 = await session1.send_and_wait("Say hi.") assert message1 is not None assert SKILL_MARKER not in message1.data.content @@ -126,7 +126,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( assert session2.session_id == session_id # Now the skill should be applied - message2 = await session2.send_and_wait({"prompt": "Say hello again using the test skill."}) + message2 = await session2.send_and_wait("Say hello again using the test skill.") assert message2 is not None assert SKILL_MARKER in message2.data.content diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index f05b3b355e..7f0d47e29e 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -20,7 +20,7 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: events = [] session.on(lambda event: events.append(event)) - await session.send_and_wait({"prompt": "Count from 1 to 5, separated by commas."}) + await session.send_and_wait("Count from 1 to 5, separated by commas.") types = [e.type.value for e in events] @@ -52,7 +52,7 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E events = [] session.on(lambda event: events.append(event)) - await session.send_and_wait({"prompt": "Say 'hello world'."}) + await session.send_and_wait("Say 'hello world'.") delta_events = [e for e in events if e.type.value == "assistant.message_delta"] @@ -69,7 +69,7 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont session = await ctx.client.create_session( {"streaming": False, "on_permission_request": PermissionHandler.approve_all} ) - await session.send_and_wait({"prompt": "What is 3 + 6?"}) + await session.send_and_wait("What is 3 + 6?") await session.disconnect() # Resume using a new client @@ -93,9 +93,7 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont events = [] session2.on(lambda event: events.append(event)) - answer = await session2.send_and_wait( - {"prompt": "Now if you double that, what do you get?"} - ) + answer = await session2.send_and_wait("Now if you double that, what do you get?") assert answer is not None assert "18" in answer.data.content diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 9bd7abbf07..5d5823d985 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -27,7 +27,7 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): {"on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "What's the first line of README.md in this directory?"}) + await session.send("What's the first line of README.md in this directory?") assistant_message = await get_final_assistant_message(session) assert "ELIZA" in assistant_message.data.content @@ -43,7 +43,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) assert "HELLO" in assistant_message.data.content @@ -56,9 +56,7 @@ def get_user_location() -> str: {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all} ) - await session.send( - {"prompt": "What is my location? If you can't find out, just say 'unknown'."} - ) + await session.send("What is my location? If you can't find out, just say 'unknown'.") answer = await get_final_assistant_message(session) # Check the underlying traffic @@ -123,10 +121,8 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: expected_session_id = session.session_id await session.send( - { - "prompt": "Perform a DB query for the 'cities' table using IDs 12 and 19, " - "sorting ascending. Reply only with lines of the form: [cityname] [population]" - } + "Perform a DB query for the 'cities' table using IDs 12 and 19, " + "sorting ascending. Reply only with lines of the form: [cityname] [population]" ) assistant_message = await get_final_assistant_message(session) @@ -161,7 +157,7 @@ def tracking_handler(request, invocation): {"tools": [safe_lookup], "on_permission_request": tracking_handler} ) - await session.send({"prompt": "Use safe_lookup to look up 'test123'"}) + await session.send("Use safe_lookup to look up 'test123'") assistant_message = await get_final_assistant_message(session) assert "RESULT: test123" in assistant_message.data.content assert not did_run_permission_request @@ -182,7 +178,7 @@ def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str: {"tools": [custom_grep], "on_permission_request": PermissionHandler.approve_all} ) - await session.send({"prompt": "Use grep to search for the word 'hello'"}) + await session.send("Use grep to search for the word 'hello'") assistant_message = await get_final_assistant_message(session) assert "CUSTOM_GREP_RESULT" in assistant_message.data.content @@ -207,7 +203,7 @@ def on_permission_request(request, invocation): } ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") assistant_message = await get_final_assistant_message(session) assert "HELLO" in assistant_message.data.content @@ -238,7 +234,7 @@ def on_permission_request(request, invocation): } ) - await session.send({"prompt": "Use encrypt_string to encrypt this string: Hello"}) + await session.send("Use encrypt_string to encrypt this string: Hello") await get_final_assistant_message(session) # The tool handler should NOT have been called since permission was denied diff --git a/python/samples/chat.py b/python/samples/chat.py index eb781e4e22..908a125d77 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -34,7 +34,7 @@ def on_event(event): continue print() - reply = await session.send_and_wait({"prompt": user_input}) + reply = await session.send_and_wait(user_input) print(f"\nAssistant: {reply.data.content if reply else None}\n") diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 5b82d5922c..b76a82e2af 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -33,7 +33,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index b6dcc869ca..f19729ab21 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -37,7 +37,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 3854626835..517c1bee18 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -31,7 +31,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 455288f634..7717982a03 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -28,7 +28,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 8295c73d51..f4ea5a2e8b 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -85,7 +85,7 @@ async def main(): try: session = await client.create_session({"model": "claude-haiku-4.5"}) - response = await session.send_and_wait({"prompt": "What is the capital of France?"}) + response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) await session.disconnect() diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index e4c45deacf..730fba01b4 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -18,7 +18,7 @@ async def ask_copilot(prompt: str) -> str: try: session = await client.create_session({"model": "claude-haiku-4.5"}) - response = await session.send_and_wait({"prompt": prompt}) + response = await session.send_and_wait(prompt) await session.disconnect() diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index bbf6cf209c..ca366d93dc 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index bbf6cf209c..ca366d93dc 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 26a2cd176b..947e698ce1 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -13,7 +13,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 5f7bc91636..4d0463b9d3 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -62,9 +62,7 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": "List the files in the current directory using the glob tool with pattern '*.md'.", - } + "List the files in the current directory using the glob tool with pattern '*.md'." ) if response: diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 2ff2538049..3c4cb66254 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -31,9 +31,7 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": "List the files in the current directory using glob with pattern '*.md'." - } + "List the files in the current directory using glob with pattern '*.md'." ) if response: diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 683f11d870..7a50431d73 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -36,12 +36,8 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": ( - "I want to learn about a city. Use the ask_user tool to ask me " - "which city I'm interested in. Then tell me about that city." - ) - } + "I want to learn about a city. Use the ask_user tool to ask me " + "which city I'm interested in. Then tell me about that city." ) if response: diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 45063b29ed..8480767924 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -14,7 +14,7 @@ async def main(): "model": "claude-haiku-4.5", }) - response = await session.send_and_wait({"prompt": "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines."}) + response = await session.send_and_wait("Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.") if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index a8cf1edcf7..b225e6937b 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -19,7 +19,7 @@ async def main(): }, }) - response = await session.send_and_wait({"prompt": "Use the grep tool to search for 'SDK' in README.md."}) + response = await session.send_and_wait("Use the grep tool to search for 'SDK' in README.md.") if response: print(f"Response: {response.data.content}") diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index 31df91c88b..b51f95f755 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -24,10 +24,8 @@ async def main(): sample_file = os.path.abspath(sample_file) response = await session.send_and_wait( - { - "prompt": "What languages are listed in the attached file?", - "attachments": [{"type": "file", "path": sample_file}], - } + "What languages are listed in the attached file?", + attachments=[{"type": "file", "path": sample_file}], ) if response: diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 38675f1455..0900c7001a 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -21,7 +21,7 @@ async def main(): }) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index b4f5caff13..1fb1337eef 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -21,7 +21,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index 07babc2180..4c053d7306 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -32,10 +32,10 @@ async def main(): response1, response2 = await asyncio.gather( session1.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ), session2.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ), ) diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 0bd69d8118..96135df310 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -31,7 +31,7 @@ async def main(): ] for prompt in prompts: - response = await session.send_and_wait({"prompt": prompt}) + response = await session.send_and_wait(prompt) if response: print(f"Q: {prompt}") print(f"A: {response.data.content}\n") diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index df5eb33ea8..818f5adb85 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -20,7 +20,7 @@ async def main(): # 2. Send the secret word await session.send_and_wait( - {"prompt": "Remember this: the secret word is PINEAPPLE."} + "Remember this: the secret word is PINEAPPLE." ) # 3. Get the session ID (don't disconnect — resume needs the session to persist) @@ -32,7 +32,7 @@ async def main(): # 5. Ask for the secret word response = await resumed.send_and_wait( - {"prompt": "What was the secret word I told you?"} + "What was the secret word I told you?" ) if response: diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index aff9d24d92..610d5f08d6 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -27,7 +27,7 @@ def on_event(event): session.on(on_event) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 5d83380d74..97762bb100 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -26,7 +26,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What custom agents are available? Describe the researcher agent and its capabilities."} + "What custom agents are available? Describe the researcher agent and its capabilities." ) if response: diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index daf7c72606..5d17903dc2 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -36,7 +36,7 @@ async def main(): session = await client.create_session(session_config) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index b4fc620a9c..1cd2e14389 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -24,7 +24,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use the bash tool to run 'echo hello'."} + "Use the bash tool to run 'echo hello'." ) if response: diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 396e336504..00e8506a79 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -26,7 +26,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use the greeting skill to greet someone named Alice."} + "Use the greeting skill to greet someone named Alice." ) if response: diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 9a6e1054e3..95c22dda1f 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -21,7 +21,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "What tools do you have available? List each one by name."} + "What tools do you have available? List each one by name." ) if response: diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 89bd41e465..2170fbe625 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -31,7 +31,7 @@ async def main(): ) response = await session.send_and_wait( - {"prompt": "Use grep to search for the word 'hello'"} + "Use grep to search for the word 'hello'" ) if response: diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index e8317c716b..9aba683ccf 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -63,12 +63,8 @@ async def main(): ) response = await session.send_and_wait( - { - "prompt": ( - "Create a file called plan.md with a brief 3-item project plan " - "for building a CLI tool. Then read it back and tell me what you wrote." - ) - } + "Create a file called plan.md with a brief 3-item project plan " + "for building a CLI tool. Then read it back and tell me what you wrote." ) if response: diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index bb60aabf88..4c5b39b836 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -15,7 +15,7 @@ async def main(): session1 = await client.create_session({"model": "claude-haiku-4.5"}) response1 = await session1.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response1 and response1.data.content: @@ -32,7 +32,7 @@ async def main(): session2 = await client.create_session({"model": "claude-haiku-4.5"}) response2 = await session2.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response2 and response2.data.content: diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 26a2cd176b..947e698ce1 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -13,7 +13,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index bbf6cf209c..ca366d93dc 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -12,7 +12,7 @@ async def main(): session = await client.create_session({"model": "claude-haiku-4.5"}) response = await session.send_and_wait( - {"prompt": "What is the capital of France?"} + "What is the capital of France?" ) if response: From 1dba08d1aca531f0980b0156b4950d74fd26f028 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 16 Mar 2026 02:21:22 -0700 Subject: [PATCH 43/61] feat(python): add overloads for `CopilotClient.on()` (#589) * feat(python): add overloads for `CopilotClient.on()` * Carry forward types in overloads * Rename UnsubscribeHandler to HandlerUnsubcribe in type annotations * Fix type checking * Fix a merge mistake * Try to fix merge error * Fix type casting for model list retrieval in CopilotClient * Remove type warning change --------- Co-authored-by: Patrick Nikoletich --- python/copilot/client.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index c1fe3c9df1..0d8074fe0a 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -23,7 +23,7 @@ import uuid from collections.abc import Awaitable, Callable from pathlib import Path -from typing import Any, cast +from typing import Any, cast, overload from .generated.rpc import ServerRpc from .generated.session_events import PermissionRequest, session_event_from_dict @@ -53,6 +53,8 @@ ToolResult, ) +HandlerUnsubcribe = Callable[[], None] + NO_RESULT_PERMISSION_V2_ERROR = ( "Permission handlers cannot return 'no-result' when connected to a protocol v2 server." ) @@ -1097,11 +1099,20 @@ async def set_foreground_session_id(self, session_id: str) -> None: error = response.get("error", "Unknown error") raise RuntimeError(f"Failed to set foreground session: {error}") + @overload + def on(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: ... + + @overload + def on( + self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler + ) -> HandlerUnsubcribe: ... + def on( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, + /, handler: SessionLifecycleHandler | None = None, - ) -> Callable[[], None]: + ) -> HandlerUnsubcribe: """ Subscribe to session lifecycle events. From 485ea5ed1ce43125075bab2f3d2681f1816a4f9a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 16 Mar 2026 10:06:52 -0400 Subject: [PATCH 44/61] Remove outdated pre-TelemetryConfig OTel documentation (#855) * Remove outdated pre-TelemetryConfig OTel documentation - Remove the large 'Application-Level Instrumentation' section from docs/observability/opentelemetry.md (614 lines) that described manual OTel setup which is now handled by the built-in TelemetryConfig - Replace --disable-telemetry CLI arg example in docs/setup/local-cli.md with --no-auto-update to avoid confusion with TelemetryConfig - Update link descriptions in getting-started.md and index.md to reflect the trimmed OTel doc scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/getting-started.md | 4 +- docs/index.md | 2 +- docs/observability/opentelemetry.md | 614 ---------------------------- docs/setup/local-cli.md | 4 +- 4 files changed, 5 insertions(+), 619 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 24e6c5b8af..15f11e8b77 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1510,7 +1510,7 @@ Trace context is propagated automatically — no manual instrumentation is neede - **SDK → CLI**: `traceparent` and `tracestate` headers from the current span/activity are included in `session.create`, `session.resume`, and `session.send` RPC calls. - **CLI → SDK**: When the CLI invokes tool handlers, the trace context from the CLI's span is propagated so your tool code runs under the correct parent span. -📖 **[OpenTelemetry Instrumentation Guide →](./observability/opentelemetry.md)** — detailed GenAI semantic conventions, event-to-attribute mapping, and complete examples. +📖 **[OpenTelemetry Instrumentation Guide →](./observability/opentelemetry.md)** — TelemetryConfig options, trace context propagation, and per-language dependencies. --- @@ -1525,7 +1525,7 @@ Trace context is propagated automatically — no manual instrumentation is neede - [Using MCP Servers](./features/mcp.md) - Integrate external tools via Model Context Protocol - [GitHub MCP Server Documentation](https://github.com/github/github-mcp-server) - [MCP Servers Directory](https://github.com/modelcontextprotocol/servers) - Explore more MCP servers -- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) - Add tracing to your SDK usage +- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) - TelemetryConfig, trace context propagation, and per-language dependencies --- diff --git a/docs/index.md b/docs/index.md index 2c5dd202dd..04ef99bd88 100644 --- a/docs/index.md +++ b/docs/index.md @@ -67,7 +67,7 @@ Detailed API reference for each session hook. ### [Observability](./observability/opentelemetry.md) -- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — built-in TelemetryConfig, trace context propagation, and application-level tracing +- [OpenTelemetry Instrumentation](./observability/opentelemetry.md) — built-in TelemetryConfig and trace context propagation ### [Integrations](./integrations/microsoft-agent-framework.md) diff --git a/docs/observability/opentelemetry.md b/docs/observability/opentelemetry.md index 26637fc6d9..b59e61a4cd 100644 --- a/docs/observability/opentelemetry.md +++ b/docs/observability/opentelemetry.md @@ -150,623 +150,9 @@ session.registerTool(myTool, async (args, invocation) => { | Go | `go.opentelemetry.io/otel` | Required dependency | | .NET | — | Uses built-in `System.Diagnostics.Activity` | -## Application-Level Instrumentation - -The rest of this guide shows how to add your own OpenTelemetry spans around SDK operations using GenAI semantic conventions. This is complementary to the built-in `TelemetryConfig` above — you can use both together. - -## Overview - -The Copilot SDK emits session events as your agent processes requests. You can instrument your application to convert these events into OpenTelemetry spans and attributes following the [OpenTelemetry GenAI Semantic Conventions v1.34.0](https://opentelemetry.io/docs/specs/semconv/gen-ai/). - -## Installation - -```bash -pip install opentelemetry-sdk opentelemetry-api -``` - -For exporting to observability backends: - -```bash -# Console output -pip install opentelemetry-sdk - -# Azure Monitor -pip install azure-monitor-opentelemetry - -# OTLP (Jaeger, Prometheus, etc.) -pip install opentelemetry-exporter-otlp -``` - -## Basic Setup - -### 1. Initialize OpenTelemetry - -```python -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter - -# Setup tracer provider -tracer_provider = TracerProvider() -trace.set_tracer_provider(tracer_provider) - -# Add exporter (console example) -span_exporter = ConsoleSpanExporter() -tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter)) - -# Get a tracer -tracer = trace.get_tracer(__name__) -``` - -### 2. Create Spans Around Agent Operations - -```python -from copilot import CopilotClient, PermissionHandler -from copilot.generated.session_events import SessionEventType -from opentelemetry import trace, context -from opentelemetry.trace import SpanKind - -# Initialize client and start the CLI server -client = CopilotClient() -await client.start() - -tracer = trace.get_tracer(__name__) - -# Create a span for the agent invocation -span_attrs = { - "gen_ai.operation.name": "invoke_agent", - "gen_ai.provider.name": "github.copilot", - "gen_ai.agent.name": "my-agent", - "gen_ai.request.model": "gpt-5", -} - -span = tracer.start_span( - name="invoke_agent my-agent", - kind=SpanKind.CLIENT, - attributes=span_attrs -) -token = context.attach(trace.set_span_in_context(span)) - -try: - # Create a session (model is set here, not on the client) - session = await client.create_session({ - "model": "gpt-5", - "on_permission_request": PermissionHandler.approve_all, - }) - - # Subscribe to events via callback - def handle_event(event): - if event.type == SessionEventType.ASSISTANT_USAGE: - if event.data.model: - span.set_attribute("gen_ai.response.model", event.data.model) - - unsubscribe = session.on(handle_event) - - # Send a message (returns a message ID) - await session.send({"prompt": "Hello, world!"}) - - # Or send and wait for the session to become idle - response = await session.send_and_wait({"prompt": "Hello, world!"}) -finally: - context.detach(token) - span.end() - await client.stop() -``` - -## Copilot SDK Event to GenAI Attribute Mapping - -The Copilot SDK emits `SessionEventType` events during agent execution. Subscribe to these events using `session.on(handler)`, which returns an unsubscribe function. Here's how to map these events to GenAI semantic convention attributes: - -### Core Session Events - -| SessionEventType | GenAI Attributes | Description | -|------------------|------------------|-------------| -| `SESSION_START` | - | Session initialization (mark span start) | -| `SESSION_IDLE` | - | Session completed (mark span end) | -| `SESSION_ERROR` | `error.type`, `error.message` | Error occurred | - -### Assistant Events - -| SessionEventType | GenAI Attributes | Description | -|------------------|------------------|-------------| -| `ASSISTANT_TURN_START` | - | Assistant begins processing | -| `ASSISTANT_TURN_END` | - | Assistant finished processing | -| `ASSISTANT_MESSAGE` | `gen_ai.output.messages` (event) | Final assistant message with complete content | -| `ASSISTANT_MESSAGE_DELTA` | - | Streaming message chunk (optional to trace) | -| `ASSISTANT_USAGE` | `gen_ai.usage.input_tokens`
`gen_ai.usage.output_tokens`
`gen_ai.response.model` | Token usage and model information | -| `ASSISTANT_REASONING` | - | Reasoning content (optional to trace) | -| `ASSISTANT_INTENT` | - | Assistant's understood intent | - -### Tool Execution Events - -| SessionEventType | GenAI Attributes / Span | Description | -|------------------|-------------------------|-------------| -| `TOOL_EXECUTION_START` | Create child span:
- `gen_ai.tool.name`
- `gen_ai.tool.call.id`
- `gen_ai.operation.name`: `execute_tool`
- `gen_ai.tool.call.arguments` (opt-in) | Tool execution begins | -| `TOOL_EXECUTION_COMPLETE` | On child span:
- `gen_ai.tool.call.result` (opt-in)
- `error.type` (if failed)
End child span | Tool execution finished | -| `TOOL_EXECUTION_PARTIAL_RESULT` | - | Streaming tool result | - -### Model and Context Events - -| SessionEventType | GenAI Attributes | Description | -|------------------|------------------|-------------| -| `SESSION_MODEL_CHANGE` | `gen_ai.request.model` | Model changed during session | -| `SESSION_CONTEXT_CHANGED` | - | Context window modified | -| `SESSION_TRUNCATION` | - | Context truncated | - -## Detailed Event Mapping Examples - -### ASSISTANT_USAGE Event - -When you receive an `ASSISTANT_USAGE` event, extract token usage: - -```python -from copilot.generated.session_events import SessionEventType - -def handle_usage(event): - if event.type == SessionEventType.ASSISTANT_USAGE: - data = event.data - if data.model: - span.set_attribute("gen_ai.response.model", data.model) - if data.input_tokens is not None: - span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens)) - if data.output_tokens is not None: - span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens)) - -unsubscribe = session.on(handle_usage) -await session.send({"prompt": "Hello"}) -``` - -**Event Data Structure:** - -```python -from dataclasses import dataclass - -@dataclass -class Usage: - input_tokens: float - output_tokens: float - cache_read_tokens: float - cache_write_tokens: float -``` - -```python -@dataclass -class Usage: - input_tokens: float - output_tokens: float - cache_read_tokens: float - cache_write_tokens: float -``` - -**Maps to GenAI Attributes:** -- `input_tokens` → `gen_ai.usage.input_tokens` -- `output_tokens` → `gen_ai.usage.output_tokens` -- Response model → `gen_ai.response.model` - -### TOOL_EXECUTION_START / COMPLETE Events - -Create child spans for each tool execution: - -```python -from opentelemetry.trace import SpanKind -import json - -# Dictionary to track active tool spans -tool_spans = {} - -def handle_tool_events(event): - data = event.data - - if event.type == SessionEventType.TOOL_EXECUTION_START and data: - call_id = data.tool_call_id or str(uuid.uuid4()) - tool_name = data.tool_name or "unknown" - - tool_attrs = { - "gen_ai.tool.name": tool_name, - "gen_ai.operation.name": "execute_tool", - } - - if call_id: - tool_attrs["gen_ai.tool.call.id"] = call_id - - # Optional: include tool arguments (may contain sensitive data) - if data.arguments is not None: - try: - tool_attrs["gen_ai.tool.call.arguments"] = json.dumps(data.arguments) - except Exception: - tool_attrs["gen_ai.tool.call.arguments"] = str(data.arguments) - - tool_span = tracer.start_span( - name=f"execute_tool {tool_name}", - kind=SpanKind.CLIENT, - attributes=tool_attrs - ) - tool_token = context.attach(trace.set_span_in_context(tool_span)) - tool_spans[call_id] = (tool_span, tool_token) - - elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data: - call_id = data.tool_call_id - entry = tool_spans.pop(call_id, None) if call_id else None - - if entry: - tool_span, tool_token = entry - - # Optional: include tool result (may contain sensitive data) - if data.result is not None: - try: - result_str = json.dumps(data.result) - except Exception: - result_str = str(data.result) - # Truncate to 512 chars to avoid huge spans - tool_span.set_attribute("gen_ai.tool.call.result", result_str[:512]) - - # Mark as error if tool failed - if hasattr(data, "success") and data.success is False: - tool_span.set_attribute("error.type", "tool_error") - - context.detach(tool_token) - tool_span.end() - -unsubscribe = session.on(handle_tool_events) -await session.send({"prompt": "What's the weather?"}) -``` - -**Tool Event Data:** -- `tool_call_id` → `gen_ai.tool.call.id` -- `tool_name` → `gen_ai.tool.name` -- `arguments` → `gen_ai.tool.call.arguments` (opt-in) -- `result` → `gen_ai.tool.call.result` (opt-in) - -### ASSISTANT_MESSAGE Event - -Capture the final message as a span event: - -```python -def handle_message(event): - if event.type == SessionEventType.ASSISTANT_MESSAGE and event.data: - if event.data.content: - # Add as a span event (opt-in for content recording) - span.add_event( - "gen_ai.output.messages", - attributes={ - "gen_ai.event.content": json.dumps({ - "role": "assistant", - "content": event.data.content - }) - } - ) - -unsubscribe = session.on(handle_message) -await session.send({"prompt": "Tell me a joke"}) -``` - -## Complete Example - -```python -import asyncio -import json -import uuid -from copilot import CopilotClient, PermissionHandler -from copilot.generated.session_events import SessionEventType -from opentelemetry import trace, context -from opentelemetry.trace import SpanKind -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter - -# Setup OpenTelemetry -tracer_provider = TracerProvider() -trace.set_tracer_provider(tracer_provider) -tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) -tracer = trace.get_tracer(__name__) - -async def invoke_agent(prompt: str): - """Invoke agent with full OpenTelemetry instrumentation.""" - - # Create main span - span_attrs = { - "gen_ai.operation.name": "invoke_agent", - "gen_ai.provider.name": "github.copilot", - "gen_ai.agent.name": "example-agent", - "gen_ai.request.model": "gpt-5", - } - - span = tracer.start_span( - name="invoke_agent example-agent", - kind=SpanKind.CLIENT, - attributes=span_attrs - ) - token = context.attach(trace.set_span_in_context(span)) - tool_spans = {} - - try: - client = CopilotClient() - await client.start() - - session = await client.create_session({ - "model": "gpt-5", - "on_permission_request": PermissionHandler.approve_all, - }) - - # Subscribe to events via callback - def handle_event(event): - data = event.data - - # Handle usage events - if event.type == SessionEventType.ASSISTANT_USAGE and data: - if data.model: - span.set_attribute("gen_ai.response.model", data.model) - if data.input_tokens is not None: - span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens)) - if data.output_tokens is not None: - span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens)) - - # Handle tool execution - elif event.type == SessionEventType.TOOL_EXECUTION_START and data: - call_id = data.tool_call_id or str(uuid.uuid4()) - tool_name = data.tool_name or "unknown" - - tool_attrs = { - "gen_ai.tool.name": tool_name, - "gen_ai.operation.name": "execute_tool", - "gen_ai.tool.call.id": call_id, - } - - tool_span = tracer.start_span( - name=f"execute_tool {tool_name}", - kind=SpanKind.CLIENT, - attributes=tool_attrs - ) - tool_token = context.attach(trace.set_span_in_context(tool_span)) - tool_spans[call_id] = (tool_span, tool_token) - - elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data: - call_id = data.tool_call_id - entry = tool_spans.pop(call_id, None) if call_id else None - if entry: - tool_span, tool_token = entry - context.detach(tool_token) - tool_span.end() - - # Capture final message - elif event.type == SessionEventType.ASSISTANT_MESSAGE and data: - if data.content: - print(f"Assistant: {data.content}") - - unsubscribe = session.on(handle_event) - - # Send message and wait for completion - response = await session.send_and_wait({"prompt": prompt}) - - span.set_attribute("gen_ai.response.finish_reasons", ["stop"]) - unsubscribe() - - except Exception as e: - span.set_attribute("error.type", type(e).__name__) - raise - finally: - # Clean up any unclosed tool spans - for call_id, (tool_span, tool_token) in tool_spans.items(): - tool_span.set_attribute("error.type", "stream_aborted") - context.detach(tool_token) - tool_span.end() - - context.detach(token) - span.end() - await client.stop() - -# Run -asyncio.run(invoke_agent("What's 2+2?")) -``` - -## Required Span Attributes - -According to OpenTelemetry GenAI semantic conventions, these attributes are **required** for agent invocation spans: - -| Attribute | Description | Example | -|-----------|-------------|---------| -| `gen_ai.operation.name` | Operation type | `invoke_agent`, `chat`, `execute_tool` | -| `gen_ai.provider.name` | Provider identifier | `github.copilot` | -| `gen_ai.request.model` | Model used for request | `gpt-5`, `gpt-4.1` | - -## Recommended Span Attributes - -These attributes are **recommended** for better observability: - -| Attribute | Description | -|-----------|-------------| -| `gen_ai.agent.id` | Unique agent identifier | -| `gen_ai.agent.name` | Human-readable agent name | -| `gen_ai.response.model` | Actual model used in response | -| `gen_ai.usage.input_tokens` | Input tokens consumed | -| `gen_ai.usage.output_tokens` | Output tokens generated | -| `gen_ai.response.finish_reasons` | Completion reasons (e.g., `["stop"]`) | - -## Content Recording - -Recording message content and tool arguments/results is **optional** and should be opt-in since it may contain sensitive data. - -### Environment Variable Control - -```bash -# Enable content recording -export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true -``` - -### Checking at Runtime - - -```python -import os -from typing import Any - -span: Any = None -event: Any = None - -def should_record_content(): - return os.getenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false").lower() == "true" - -if should_record_content() and event.data.content: - span.add_event("gen_ai.output.messages", ...) -``` - -```python -import os - -def should_record_content(): - return os.getenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "false").lower() == "true" - -# Only add content if enabled -if should_record_content() and event.data.content: - span.add_event("gen_ai.output.messages", ...) -``` - -## MCP (Model Context Protocol) Tool Conventions - -For MCP-based tools, add these additional attributes following the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/): - - -```python -from typing import Any - -data: Any = None -session: Any = None - -tool_attrs = { - "mcp.method.name": "tools/call", - "mcp.server.name": data.mcp_server_name, - "mcp.session.id": session.session_id, - "gen_ai.tool.name": data.mcp_tool_name, - "gen_ai.operation.name": "execute_tool", - "network.transport": "pipe", -} -``` - -```python -tool_attrs = { - # Required - "mcp.method.name": "tools/call", - - # Recommended - "mcp.server.name": data.mcp_server_name, - "mcp.session.id": session.session_id, - - # GenAI attributes - "gen_ai.tool.name": data.mcp_tool_name, - "gen_ai.operation.name": "execute_tool", - "network.transport": "pipe", # Copilot SDK uses stdio -} -``` - -## Span Naming Conventions - -Follow these patterns for span names: - -| Operation | Span Name Pattern | Example | -|-----------|-------------------|---------| -| Agent invocation | `invoke_agent {agent_name}` | `invoke_agent weather-bot` | -| Chat | `chat` | `chat` | -| Tool execution | `execute_tool {tool_name}` | `execute_tool fetch_weather` | -| MCP tool | `tools/call {tool_name}` | `tools/call read_file` | - -## Metrics - -You can also export metrics for token usage and operation duration: - -```python -from opentelemetry import metrics -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader - -# Setup metrics -reader = PeriodicExportingMetricReader(ConsoleMetricExporter()) -provider = MeterProvider(metric_readers=[reader]) -metrics.set_meter_provider(provider) - -meter = metrics.get_meter(__name__) - -# Create metrics -operation_duration = meter.create_histogram( - name="gen_ai.client.operation.duration", - description="Duration of GenAI operations", - unit="ms" -) - -token_usage = meter.create_counter( - name="gen_ai.client.token.usage", - description="Token usage count" -) - -# Record metrics -operation_duration.record(123.45, attributes={ - "gen_ai.operation.name": "invoke_agent", - "gen_ai.request.model": "gpt-5", -}) - -token_usage.add(150, attributes={ - "gen_ai.token.type": "input", - "gen_ai.operation.name": "invoke_agent", -}) -``` - -## Azure Monitor Integration - -For production observability with Azure Monitor: - -```python -from azure.monitor.opentelemetry import configure_azure_monitor - -# Enable Azure Monitor -connection_string = "InstrumentationKey=..." -configure_azure_monitor(connection_string=connection_string) - -# Your instrumented code here -``` - -View traces in the Azure Portal under your Application Insights resource → Tracing. - -## Best Practices - -1. **Always close spans**: Use try/finally blocks to ensure spans are ended even on errors -2. **Set error attributes**: On exceptions, set `error.type` and optionally `error.message` -3. **Use child spans for tools**: Create separate spans for each tool execution -4. **Opt-in for content**: Only record message content and tool arguments when explicitly enabled -5. **Truncate large values**: Limit tool results and arguments to reasonable sizes (e.g., 512 chars) -6. **Set finish reasons**: Always set `gen_ai.response.finish_reasons` when the operation completes successfully -7. **Include model info**: Capture both request and response model names - -## Troubleshooting - -### No spans appearing - -1. Verify tracer provider is set: `trace.set_tracer_provider(provider)` -2. Add a span processor: `provider.add_span_processor(SimpleSpanProcessor(exporter))` -3. Ensure spans are ended: Check for missing `span.end()` calls - -### Tool spans not showing as children - -Make sure to attach the tool span to the parent context: - -```python -from opentelemetry import trace, context -from opentelemetry.trace import SpanKind - -tracer = trace.get_tracer(__name__) -tool_span = tracer.start_span("test", kind=SpanKind.CLIENT) -tool_token = context.attach(trace.set_span_in_context(tool_span)) -``` - -```python -tool_token = context.attach(trace.set_span_in_context(tool_span)) -``` - -### Context warnings in async code - -You may see "Failed to detach context" warnings in async streaming code. These are expected and don't affect tracing correctness. - ## References - [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) - [OpenTelemetry MCP Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) - [OpenTelemetry Python SDK](https://opentelemetry.io/docs/instrumentation/python/) -- [GenAI Semantic Conventions v1.34.0](https://opentelemetry.io/schemas/1.34.0) - [Copilot SDK Documentation](https://github.com/github/copilot-sdk) diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index c9074af675..b78e294f28 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -166,8 +166,8 @@ const client = new CopilotClient({ // Set log level for debugging logLevel: "debug", - // Pass extra CLI arguments - cliArgs: ["--disable-telemetry"], + // Pass extra CLI arguments (example: set a custom log directory) + cliArgs: ["--log-dir=/tmp/copilot-logs"], // Set working directory cwd: "/path/to/project", From 7d8fb517c12ae8755d6417d7cb4a5ef10e36eda2 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Tue, 17 Mar 2026 09:52:41 -0700 Subject: [PATCH 45/61] Add official Java SDK information to README (#876) * Add official Java SDK information to README * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Revise Java SDK installation instructions Updated Java SDK installation instructions to include Maven and Gradle links. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4899588a6..087fa44491 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Agents for every app. -Embed Copilot's agentic workflows in your application—now available in Technical preview as a programmable SDK for Python, TypeScript, Go, and .NET. +Embed Copilot's agentic workflows in your application—now available in Technical preview as a programmable SDK for Python, TypeScript, Go, .NET, and Java. The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production-tested agent runtime you can invoke programmatically. No need to build your own orchestration—you define agent behavior, Copilot handles planning, tool invocation, file edits, and more. @@ -20,6 +20,7 @@ The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production- | **Python** | [`python/`](./python/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/python/README.md) | `pip install github-copilot-sdk` | | **Go** | [`go/`](./go/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/go/README.md) | `go get github.com/github/copilot-sdk/go` | | **.NET** | [`dotnet/`](./dotnet/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/README.md) | `dotnet add package GitHub.Copilot.SDK` | +| **Java** | [`github/copilot-sdk-java`](https://github.com/github/copilot-sdk-java) | WIP | See instructions for [Maven](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#maven) and [Gradle](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#gradle) | See the individual SDK READMEs for installation, usage examples, and API reference. @@ -97,6 +98,8 @@ Yes, check out the custom instructions for each SDK: - **[Python](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-python.instructions.md)** - **[.NET](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-csharp.instructions.md)** - **[Go](https://github.com/github/awesome-copilot/blob/main/instructions/copilot-sdk-go.instructions.md)** +- **[Java](https://github.com/github/copilot-sdk-java/blob/main/instructions/copilot-sdk-java.instructions.md)** + ### What models are supported? @@ -127,12 +130,10 @@ Please use the [GitHub Issues](https://github.com/github/copilot-sdk/issues) pag | SDK | Location | | --------------| ----------------------------------------------------------------- | -| **Java** | [copilot-community-sdk/copilot-sdk-java][sdk-java] | | **Rust** | [copilot-community-sdk/copilot-sdk-rust][sdk-rust] | | **Clojure** | [copilot-community-sdk/copilot-sdk-clojure][sdk-clojure] | | **C++** | [0xeb/copilot-sdk-cpp][sdk-cpp] | -[sdk-java]: https://github.com/copilot-community-sdk/copilot-sdk-java [sdk-rust]: https://github.com/copilot-community-sdk/copilot-sdk-rust [sdk-cpp]: https://github.com/0xeb/copilot-sdk-cpp [sdk-clojure]: https://github.com/copilot-community-sdk/copilot-sdk-clojure From dde88fbad302542395815d73ac6512ccc588fdff Mon Sep 17 00:00:00 2001 From: James Montemagno Date: Wed, 18 Mar 2026 09:51:11 -0700 Subject: [PATCH 46/61] Add Permission Handling documentation to all language READMEs (#879) * Add Permission Handling documentation to all language READMEs Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesmontemagno <1676321+jamesmontemagno@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dotnet/README.md | 88 ++++++++++++++++++++++++++++++++++++++++- go/README.md | 78 +++++++++++++++++++++++++++++++++++- nodejs/README.md | 84 +++++++++++++++++++++++++++++++++++++-- python/README.md | 100 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 339 insertions(+), 11 deletions(-) diff --git a/dotnet/README.md b/dotnet/README.md index 712323c0c6..482de00d81 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -28,10 +28,11 @@ using GitHub.Copilot.SDK; await using var client = new CopilotClient(); await client.StartAsync(); -// Create a session +// Create a session (OnPermissionRequest is required) await using var session = await client.CreateSessionAsync(new SessionConfig { - Model = "gpt-5" + Model = "gpt-5", + OnPermissionRequest = PermissionHandler.ApproveAll, }); // Wait for response using session.idle event @@ -110,6 +111,7 @@ Create a new conversation session. - `Provider` - Custom API provider configuration (BYOK) - `Streaming` - Enable streaming of response chunks (default: false) - `InfiniteSessions` - Configure automatic context compaction (see below) +- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -117,6 +119,10 @@ Create a new conversation session. Resume an existing session. Returns the session with `WorkspacePath` populated if infinite sessions were enabled. +**ResumeSessionConfig:** + +- `OnPermissionRequest` - **Required.** Handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. + ##### `PingAsync(string? message = null): Task` Ping the server to check connectivity. @@ -573,6 +579,84 @@ Trace context (`traceparent`/`tracestate`) is automatically propagated between t No extra dependencies — uses built-in `System.Diagnostics.Activity`. +## Permission Handling + +An `OnPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision. + +### Approve All (simplest) + +Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks: + +```csharp +using GitHub.Copilot.SDK; + +var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-5", + OnPermissionRequest = PermissionHandler.ApproveAll, +}); +``` + +### Custom Permission Handler + +Provide your own `PermissionRequestHandler` delegate to inspect each request and apply custom logic: + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-5", + OnPermissionRequest = async (request, invocation) => + { + // request.Kind — string discriminator for the type of operation being requested: + // "shell" — executing a shell command + // "write" — writing or editing a file + // "read" — reading a file + // "mcp" — calling an MCP tool + // "custom_tool" — calling one of your registered tools + // "url" — fetching a URL + // "memory" — accessing or modifying assistant memory + // "hook" — invoking a registered hook + // request.ToolCallId — the tool call that triggered this request + // request.ToolName — name of the tool (for custom-tool / mcp) + // request.FileName — file being written (for write) + // request.FullCommandText — full shell command text (for shell) + + if (request.Kind == "shell") + { + // Deny shell commands + return new PermissionRequestResult { Kind = PermissionRequestResultKind.DeniedInteractivelyByUser }; + } + + return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + } +}); +``` + +### Permission Result Kinds + +| Value | Meaning | +|-------|---------| +| `PermissionRequestResultKind.Approved` | Allow the tool to run | +| `PermissionRequestResultKind.DeniedInteractivelyByUser` | User explicitly denied the request | +| `PermissionRequestResultKind.DeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked | +| `PermissionRequestResultKind.DeniedByRules` | Denied by a policy rule | +| `PermissionRequestResultKind.NoResult` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). | + +### Resuming Sessions + +Pass `OnPermissionRequest` when resuming a session too — it is required: + +```csharp +var session = await client.ResumeSessionAsync("session-id", new ResumeSessionConfig +{ + OnPermissionRequest = PermissionHandler.ApproveAll, +}); +``` + +### Per-Tool Skip Permission + +To let a specific custom tool bypass the permission prompt entirely, set `skip_permission = true` in the tool's `AdditionalProperties`. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: diff --git a/go/README.md b/go/README.md index f87c3d1b84..f22666f733 100644 --- a/go/README.md +++ b/go/README.md @@ -44,9 +44,10 @@ func main() { } defer client.Stop() - // Create a session + // Create a session (OnPermissionRequest is required) session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ - Model: "gpt-5", + Model: "gpt-5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) @@ -153,11 +154,13 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `Streaming` (bool): Enable streaming delta events - `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration +- `OnPermissionRequest` (PermissionHandlerFunc): **Required.** Handler called before each tool execution to approve or deny it. Use `copilot.PermissionHandler.ApproveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `OnUserInputRequest` (UserInputHandler): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `Hooks` (\*SessionHooks): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. **ResumeSessionConfig:** +- `OnPermissionRequest` (PermissionHandlerFunc): **Required.** Handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section. - `Tools` ([]Tool): Tools to expose when resuming - `ReasoningEffort` (string): Reasoning effort level for models that support it - `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. @@ -499,6 +502,77 @@ Trace context (`traceparent`/`tracestate`) is automatically propagated between t Dependency: `go.opentelemetry.io/otel` +## Permission Handling + +An `OnPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision. + +### Approve All (simplest) + +Use the built-in `PermissionHandler.ApproveAll` helper to allow every tool call without any checks: + +```go +session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ + Model: "gpt-5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +}) +``` + +### Custom Permission Handler + +Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic: + +```go +session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ + Model: "gpt-5", + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + // request.Kind — what type of operation is being requested: + // copilot.KindShell — executing a shell command + // copilot.Write — writing or editing a file + // copilot.Read — reading a file + // copilot.MCP — calling an MCP tool + // copilot.CustomTool — calling one of your registered tools + // copilot.URL — fetching a URL + // copilot.Memory — accessing or updating Copilot-managed memory + // copilot.Hook — invoking a registered hook + // request.ToolCallID — pointer to the tool call that triggered this request + // request.ToolName — pointer to the name of the tool (for custom-tool / mcp) + // request.FileName — pointer to the file being written (for write) + // request.FullCommandText — pointer to the full shell command (for shell) + + if request.Kind == copilot.KindShell { + // Deny shell commands + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindDeniedInteractivelyByUser}, nil + } + + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, +}) +``` + +### Permission Result Kinds + +| Constant | Meaning | +|----------|---------| +| `PermissionRequestResultKindApproved` | Allow the tool to run | +| `PermissionRequestResultKindDeniedInteractivelyByUser` | User explicitly denied the request | +| `PermissionRequestResultKindDeniedCouldNotRequestFromUser` | No approval rule matched and user could not be asked | +| `PermissionRequestResultKindDeniedByRules` | Denied by a policy rule | +| `PermissionRequestResultKindNoResult` | Leave the permission request unanswered (protocol v1 only; not allowed for protocol v2) | + +### Resuming Sessions + +Pass `OnPermissionRequest` when resuming a session too — it is required: + +```go +session, err := client.ResumeSession(context.Background(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, +}) +``` + +### Per-Tool Skip Permission + +To let a specific custom tool bypass the permission prompt entirely, set `SkipPermission = true` on the tool. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `OnUserInputRequest` handler: diff --git a/nodejs/README.md b/nodejs/README.md index af37b27bfc..75cc33d46f 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -26,15 +26,16 @@ npm start ## Quick Start ```typescript -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; // Create and start client const client = new CopilotClient(); await client.start(); -// Create a session +// Create a session (onPermissionRequest is required) const session = await client.createSession({ model: "gpt-5", + onPermissionRequest: approveAll, }); // Wait for response using typed event handlers @@ -59,7 +60,7 @@ await client.stop(); Sessions also support `Symbol.asyncDispose` for use with [`await using`](https://github.com/tc39/proposal-explicit-resource-management) (TypeScript 5.2+/Node.js 18.0+): ```typescript -await using session = await client.createSession({ model: "gpt-5" }); +await using session = await client.createSession({ model: "gpt-5", onPermissionRequest: approveAll }); // session is automatically disconnected when leaving scope ``` @@ -114,6 +115,7 @@ Create a new conversation session. - `systemMessage?: SystemMessageConfig` - System message customization (see below) - `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below) - `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section. +- `onPermissionRequest: PermissionHandler` - **Required.** Handler called before each tool execution to approve or deny it. Use `approveAll` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `onUserInputRequest?: UserInputHandler` - Handler for user input requests from the agent. Enables the `ask_user` tool. See [User Input Requests](#user-input-requests) section. - `hooks?: SessionHooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -648,6 +650,82 @@ const client = new CopilotClient({ Inbound trace context from the CLI is available on the `ToolInvocation` object passed to tool handlers as `traceparent` and `tracestate` fields. See the [OpenTelemetry guide](../docs/observability/opentelemetry.md) for a full wire-up example. +## Permission Handling + +An `onPermissionRequest` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision. + +### Approve All (simplest) + +Use the built-in `approveAll` helper to allow every tool call without any checks: + +```typescript +import { CopilotClient, approveAll } from "@github/copilot-sdk"; + +const session = await client.createSession({ + model: "gpt-5", + onPermissionRequest: approveAll, +}); +``` + +### Custom Permission Handler + +Provide your own function to inspect each request and apply custom logic: + +```typescript +import type { PermissionRequest, PermissionRequestResult } from "@github/copilot-sdk"; + +const session = await client.createSession({ + model: "gpt-5", + onPermissionRequest: (request: PermissionRequest, invocation): PermissionRequestResult => { + // request.kind — what type of operation is being requested: + // "shell" — executing a shell command + // "write" — writing or editing a file + // "read" — reading a file + // "mcp" — calling an MCP tool + // "custom-tool" — calling one of your registered tools + // "url" — fetching a URL + // "memory" — storing or retrieving persistent session memory + // "hook" — invoking a server-side hook or integration + // (additional kinds may be added; include a default case in handlers) + // request.toolCallId — the tool call that triggered this request + // request.toolName — name of the tool (for custom-tool / mcp) + // request.fileName — file being written (for write) + // request.fullCommandText — full shell command (for shell) + + if (request.kind === "shell") { + // Deny shell commands + return { kind: "denied-interactively-by-user" }; + } + + return { kind: "approved" }; + }, +}); +``` + +### Permission Result Kinds + +| Kind | Meaning | +|------|---------| +| `"approved"` | Allow the tool to run | +| `"denied-interactively-by-user"` | User explicitly denied the request | +| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked | +| `"denied-by-rules"` | Denied by a policy rule | +| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy | +| `"no-result"` | Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers) | +### Resuming Sessions + +Pass `onPermissionRequest` when resuming a session too — it is required: + +```typescript +const session = await client.resumeSession("session-id", { + onPermissionRequest: approveAll, +}); +``` + +### Per-Tool Skip Permission + +To let a specific custom tool bypass the permission prompt entirely, set `skipPermission: true` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `onUserInputRequest` handler: diff --git a/python/README.md b/python/README.md index 6d1c81281c..5d08e7fcb6 100644 --- a/python/README.md +++ b/python/README.md @@ -25,15 +25,18 @@ python chat.py ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): # Create and start client client = CopilotClient() await client.start() - # Create a session - session = await client.create_session({"model": "gpt-5"}) + # Create a session (on_permission_request is required) + session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": PermissionHandler.approve_all, + }) # Wait for response using session.idle event done = asyncio.Event() @@ -60,7 +63,10 @@ asyncio.run(main()) Sessions also support the `async with` context manager pattern for automatic cleanup: ```python -async with await client.create_session({"model": "gpt-5"}) as session: +async with await client.create_session({ + "model": "gpt-5", + "on_permission_request": PermissionHandler.approve_all, +}) as session: await session.send("What is 2+2?") # session is automatically disconnected when leaving the block ``` @@ -144,6 +150,7 @@ CopilotClient( - `streaming` (bool): Enable streaming delta events - `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `infinite_sessions` (dict): Automatic context compaction configuration +- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. @@ -469,6 +476,91 @@ Trace context (`traceparent`/`tracestate`) is automatically propagated between t Install with telemetry extras: `pip install copilot-sdk[telemetry]` (provides `opentelemetry-api`) +## Permission Handling + +An `on_permission_request` handler is **required** whenever you create or resume a session. The handler is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and must return a decision. + +### Approve All (simplest) + +Use the built-in `PermissionHandler.approve_all` helper to allow every tool call without any checks: + +```python +from copilot import CopilotClient, PermissionHandler + +session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": PermissionHandler.approve_all, +}) +``` + +### Custom Permission Handler + +Provide your own function to inspect each request and apply custom logic (sync or async): + +```python +from copilot import PermissionRequest, PermissionRequestResult + +def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: + # request.kind — what type of operation is being requested: + # "shell" — executing a shell command + # "write" — writing or editing a file + # "read" — reading a file + # "mcp" — calling an MCP tool + # "custom-tool" — calling one of your registered tools + # "url" — fetching a URL + # "memory" — accessing or updating session/workspace memory + # "hook" — invoking a registered hook + # request.tool_call_id — the tool call that triggered this request + # request.tool_name — name of the tool (for custom-tool / mcp) + # request.file_name — file being written (for write) + # request.full_command_text — full shell command (for shell) + + if request.kind.value == "shell": + # Deny shell commands + return PermissionRequestResult(kind="denied-interactively-by-user") + + return PermissionRequestResult(kind="approved") + +session = await client.create_session({ + "model": "gpt-5", + "on_permission_request": on_permission_request, +}) +``` + +Async handlers are also supported: + +```python +async def on_permission_request(request: PermissionRequest, invocation: dict) -> PermissionRequestResult: + # Simulate an async approval check (e.g., prompting a user over a network) + await asyncio.sleep(0) + return PermissionRequestResult(kind="approved") +``` + +### Permission Result Kinds + +| `kind` value | Meaning | +|---|---------| +| `"approved"` | Allow the tool to run | +| `"denied-interactively-by-user"` | User explicitly denied the request | +| `"denied-no-approval-rule-and-could-not-request-from-user"` | No approval rule matched and user could not be asked (default when no kind is specified) | +| `"denied-by-rules"` | Denied by a policy rule | +| `"denied-by-content-exclusion-policy"` | Denied due to a content exclusion policy | +| `"no-result"` | Leave the request unanswered (not allowed for protocol v2 permission requests) | + +### Resuming Sessions + +Pass `on_permission_request` when resuming a session too — it is required: + +```python +session = await client.resume_session("session-id", { + "on_permission_request": PermissionHandler.approve_all, +}) +``` + +### Per-Tool Skip Permission + +To let a specific custom tool bypass the permission prompt entirely, set `skip_permission=True` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. + ## User Input Requests Enable the agent to ask questions to the user using the `ask_user` tool by providing an `on_user_input_request` handler: From 698b2598e32e0958a5298e6dcd715970e0a94d53 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 18 Mar 2026 16:01:18 -0700 Subject: [PATCH 47/61] feat: add blob attachment type for inline base64 data (#731) * feat: add blob attachment type for inline base64 data Add support for a new 'blob' attachment type that allows sending base64-encoded content (e.g. images) directly without disk I/O. Generated types will be updated automatically when the runtime publishes the new schema to @github/copilot. This commit includes: - Add blob variant to Node.js and Python hand-written types - Export attachment types from Python SDK public API - Update docs: image-input.md, all language READMEs, streaming-events.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update dotnet/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add E2E tests for blob attachments across all 4 SDKs Add blob attachment E2E tests for Node.js, Python, Go, and .NET SDKs. Each test sends a message with an inline base64-encoded PNG blob attachment and verifies the request is accepted by the replay proxy. - nodejs/test/e2e/session_config.test.ts: should accept blob attachments - python/e2e/test_session.py: test_should_accept_blob_attachments - go/internal/e2e/session_test.go: TestSessionBlobAttachment - dotnet/test/SessionTests.cs: Should_Accept_Blob_Attachments - test/snapshots/: request-only YAML snapshots for replay proxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(python): break long base64 string to satisfy ruff E501 line length Split the inline base64-encoded PNG data in the blob attachment E2E test into a local variable with implicit string concatenation so every line stays within the 100-character limit enforced by ruff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/features/image-input.md | 209 +++++++++++++++++- docs/features/streaming-events.md | 2 +- dotnet/README.md | 23 +- dotnet/test/SessionTests.cs | 23 ++ go/README.md | 16 +- go/internal/e2e/session_test.go | 42 ++++ nodejs/README.md | 15 +- nodejs/src/types.ts | 8 +- nodejs/test/e2e/session_config.test.ts | 19 ++ python/README.md | 15 +- python/copilot/__init__.py | 10 + python/copilot/types.py | 13 +- python/e2e/test_session.py | 27 +++ test/scenarios/prompts/attachments/README.md | 21 +- .../should_accept_blob_attachments.yaml | 8 + .../should_accept_blob_attachments.yaml | 8 + 16 files changed, 435 insertions(+), 24 deletions(-) create mode 100644 test/snapshots/session/should_accept_blob_attachments.yaml create mode 100644 test/snapshots/session_config/should_accept_blob_attachments.yaml diff --git a/docs/features/image-input.md b/docs/features/image-input.md index aa3bf2f643..1a3bde0a23 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -1,6 +1,9 @@ # Image Input -Send images to Copilot sessions by attaching them as file attachments. The runtime reads the file from disk, converts it to base64 internally, and sends it to the LLM as an image content block — no manual encoding required. +Send images to Copilot sessions as attachments. There are two ways to attach images: + +- **File attachment** (`type: "file"`) — provide an absolute path; the runtime reads the file from disk, converts it to base64, and sends it to the LLM. +- **Blob attachment** (`type: "blob"`) — provide base64-encoded data directly; useful when the image is already in memory (e.g., screenshots, generated images, or data from an API). ## Overview @@ -25,11 +28,12 @@ sequenceDiagram | Concept | Description | |---------|-------------| | **File attachment** | An attachment with `type: "file"` and an absolute `path` to an image on disk | -| **Automatic encoding** | The runtime reads the image, converts it to base64, and sends it as an `image_url` block | +| **Blob attachment** | An attachment with `type: "blob"`, base64-encoded `data`, and a `mimeType` — no disk I/O needed | +| **Automatic encoding** | For file attachments, the runtime reads the image and converts it to base64 automatically | | **Auto-resize** | The runtime automatically resizes or quality-reduces images that exceed model-specific limits | | **Vision capability** | The model must have `capabilities.supports.vision = true` to process images | -## Quick Start +## Quick Start — File Attachment Attach an image file to any message using the file attachment type. The path must be an absolute path to an image on disk. @@ -75,15 +79,15 @@ session = await client.create_session({ "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), }) -await session.send({ - "prompt": "Describe what you see in this image", - "attachments": [ +await session.send( + "Describe what you see in this image", + attachments=[ { "type": "file", "path": "/absolute/path/to/screenshot.png", }, ], -}) +) ``` @@ -215,9 +219,190 @@ await session.SendAsync(new MessageOptions +## Quick Start — Blob Attachment + +When you already have image data in memory (e.g., a screenshot captured by your app, or an image fetched from an API), use a blob attachment to send it directly without writing to disk. + +
+Node.js / TypeScript + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient(); +await client.start(); + +const session = await client.createSession({ + model: "gpt-4.1", + onPermissionRequest: async () => ({ kind: "approved" }), +}); + +const base64ImageData = "..."; // your base64-encoded image +await session.send({ + prompt: "Describe what you see in this image", + attachments: [ + { + type: "blob", + data: base64ImageData, + mimeType: "image/png", + displayName: "screenshot.png", + }, + ], +}); +``` + +
+ +
+Python + +```python +from copilot import CopilotClient +from copilot.types import PermissionRequestResult + +client = CopilotClient() +await client.start() + +session = await client.create_session({ + "model": "gpt-4.1", + "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), +}) + +base64_image_data = "..." # your base64-encoded image +await session.send( + "Describe what you see in this image", + attachments=[ + { + "type": "blob", + "data": base64_image_data, + "mimeType": "image/png", + "displayName": "screenshot.png", + }, + ], +) +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(nil) + client.Start(ctx) + + session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ + Model: "gpt-4.1", + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + }, + }) + + base64ImageData := "..." + mimeType := "image/png" + displayName := "screenshot.png" + session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, + }) +} +``` + + +```go +mimeType := "image/png" +displayName := "screenshot.png" +session.Send(ctx, copilot.MessageOptions{ + Prompt: "Describe what you see in this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, // base64-encoded string + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, +}) +``` + +
+ +
+.NET + + +```csharp +using GitHub.Copilot.SDK; + +public static class BlobAttachmentExample +{ + public static async Task Main() + { + await using var client = new CopilotClient(); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Model = "gpt-4.1", + OnPermissionRequest = (req, inv) => + Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + }); + + var base64ImageData = "..."; + await session.SendAsync(new MessageOptions + { + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", + DisplayName = "screenshot.png", + }, + }, + }); + } +} +``` + + +```csharp +await session.SendAsync(new MessageOptions +{ + Prompt = "Describe what you see in this image", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", + DisplayName = "screenshot.png", + }, + }, +}); +``` + +
+ ## Supported Formats -Supported image formats include JPG, PNG, GIF, and other common image types. The runtime reads the image from disk and converts it as needed before sending to the LLM. Use PNG or JPEG for best results, as these are the most widely supported formats. +Supported image formats include JPG, PNG, GIF, and other common image types. For file attachments, the runtime reads the image from disk and converts it as needed. For blob attachments, you provide the base64 data and MIME type directly. Use PNG or JPEG for best results, as these are the most widely supported formats. The model's `capabilities.limits.vision.supported_media_types` field lists the exact MIME types it accepts. @@ -283,10 +468,10 @@ These image blocks appear in `tool.execution_complete` event results. See the [S |-----|---------| | **Use PNG or JPEG directly** | Avoids conversion overhead — these are sent to the LLM as-is | | **Keep images reasonably sized** | Large images may be quality-reduced, which can lose important details | -| **Use absolute paths** | The runtime reads files from disk; relative paths may not resolve correctly | -| **Check vision support first** | Sending images to a non-vision model wastes tokens on the file path without visual understanding | -| **Multiple images are supported** | Attach several file attachments in one message, up to the model's `max_prompt_images` limit | -| **Images are not base64 in your code** | You provide a file path — the runtime handles encoding, resizing, and format conversion | +| **Use absolute paths for file attachments** | The runtime reads files from disk; relative paths may not resolve correctly | +| **Use blob attachments for in-memory data** | When you already have base64 data (e.g., screenshots, API responses), blob avoids unnecessary disk I/O | +| **Check vision support first** | Sending images to a non-vision model wastes tokens without visual understanding | +| **Multiple images are supported** | Attach several attachments in one message, up to the model's `max_prompt_images` limit | | **SVG is not supported** | SVG files are text-based and excluded from image processing | ## See Also diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 81b27f80fd..d03ed95fa7 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -639,7 +639,7 @@ The user sent a message. Recorded for the session timeline. |------------|------|----------|-------------| | `content` | `string` | ✅ | The user's message text | | `transformedContent` | `string` | | Transformed version after preprocessing | -| `attachments` | `Attachment[]` | | File, directory, selection, or GitHub reference attachments | +| `attachments` | `Attachment[]` | | File, directory, selection, blob, or GitHub reference attachments | | `source` | `string` | | Message source identifier | | `agentMode` | `string` | | Agent mode: `"interactive"`, `"plan"`, `"autopilot"`, or `"shell"` | | `interactionId` | `string` | | CAPI interaction ID | diff --git a/dotnet/README.md b/dotnet/README.md index 482de00d81..cb7dbba18e 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -271,18 +271,33 @@ session.On(evt => ## Image Support -The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `Attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```csharp +// File attachment — runtime reads from disk await session.SendAsync(new MessageOptions { Prompt = "What's in this image?", Attachments = new List { - new UserMessageDataAttachmentsItem + new UserMessageDataAttachmentsItemFile { - Type = UserMessageDataAttachmentsItemType.File, - Path = "/path/to/image.jpg" + Path = "/path/to/image.jpg", + DisplayName = "image.jpg", + } + } +}); + +// Blob attachment — provide base64 data directly +await session.SendAsync(new MessageOptions +{ + Prompt = "What's in this image?", + Attachments = new List + { + new UserMessageDataAttachmentsItemBlob + { + Data = base64ImageData, + MimeType = "image/png", } } }); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 8cd4c84e55..30a9135a5c 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -538,6 +538,29 @@ public async Task DisposeAsync_From_Handler_Does_Not_Deadlock() await disposed.Task.WaitAsync(TimeSpan.FromSeconds(10)); } + [Fact] + public async Task Should_Accept_Blob_Attachments() + { + var session = await CreateSessionAsync(); + + await session.SendAsync(new MessageOptions + { + Prompt = "Describe this image", + Attachments = + [ + new UserMessageDataAttachmentsItemBlob + { + Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + MimeType = "image/png", + DisplayName = "test-pixel.png", + }, + ], + }); + + // Just verify send doesn't throw — blob attachment support varies by runtime + await session.DisposeAsync(); + } + private static async Task WaitForAsync(Func condition, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; diff --git a/go/README.md b/go/README.md index f22666f733..1d0665130d 100644 --- a/go/README.md +++ b/go/README.md @@ -181,9 +181,10 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec ## Image Support -The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path: +The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```go +// File attachment — runtime reads from disk _, err = session.Send(context.Background(), copilot.MessageOptions{ Prompt: "What's in this image?", Attachments: []copilot.Attachment{ @@ -193,6 +194,19 @@ _, err = session.Send(context.Background(), copilot.MessageOptions{ }, }, }) + +// Blob attachment — provide base64 data directly +mimeType := "image/png" +_, err = session.Send(context.Background(), copilot.MessageOptions{ + Prompt: "What's in this image?", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &base64ImageData, + MIMEType: &mimeType, + }, + }, +}) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index c3c9cc0094..052ae15809 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -938,6 +938,48 @@ func TestSetModelWithReasoningEffort(t *testing.T) { } } +func TestSessionBlobAttachment(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + t.Run("should accept blob attachments", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + mimeType := "image/png" + displayName := "test-pixel.png" + _, err = session.Send(t.Context(), copilot.MessageOptions{ + Prompt: "Describe this image", + Attachments: []copilot.Attachment{ + { + Type: copilot.Blob, + Data: &data, + MIMEType: &mimeType, + DisplayName: &displayName, + }, + }, + }) + if err != nil { + t.Fatalf("Send with blob attachment failed: %v", err) + } + + // Just verify send doesn't error — blob attachment support varies by runtime + session.Disconnect() + }) +} + func getToolNames(exchange testharness.ParsedHttpExchange) []string { var names []string for _, tool := range exchange.Request.Tools { diff --git a/nodejs/README.md b/nodejs/README.md index 75cc33d46f..e9d23c529a 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -300,9 +300,10 @@ See `SessionEvent` type in the source for full details. ## Image Support -The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```typescript +// File attachment — runtime reads from disk await session.send({ prompt: "What's in this image?", attachments: [ @@ -312,6 +313,18 @@ await session.send({ }, ], }); + +// Blob attachment — provide base64 data directly +await session.send({ + prompt: "What's in this image?", + attachments: [ + { + type: "blob", + data: base64ImageData, + mimeType: "image/png", + }, + ], +}); ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9576b69252..9052bde524 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -933,7 +933,7 @@ export interface MessageOptions { prompt: string; /** - * File, directory, or selection attachments + * File, directory, selection, or blob attachments */ attachments?: Array< | { @@ -956,6 +956,12 @@ export interface MessageOptions { }; text?: string; } + | { + type: "blob"; + data: string; + mimeType: string; + displayName?: string; + } >; /** diff --git a/nodejs/test/e2e/session_config.test.ts b/nodejs/test/e2e/session_config.test.ts index 2984c3c04c..e27421ebf8 100644 --- a/nodejs/test/e2e/session_config.test.ts +++ b/nodejs/test/e2e/session_config.test.ts @@ -43,6 +43,25 @@ describe("Session Configuration", async () => { } }); + it("should accept blob attachments", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + await session.send({ + prompt: "Describe this image", + attachments: [ + { + type: "blob", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + mimeType: "image/png", + displayName: "test-pixel.png", + }, + ], + }); + + // Just verify send doesn't throw — blob attachment support varies by runtime + await session.disconnect(); + }); + it("should accept message attachments", async () => { await writeFile(join(workDir, "attached.txt"), "This file is attached"); diff --git a/python/README.md b/python/README.md index 5d08e7fcb6..582031d402 100644 --- a/python/README.md +++ b/python/README.md @@ -270,9 +270,10 @@ async def safe_lookup(params: LookupParams) -> str: ## Image Support -The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path: +The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: ```python +# File attachment — runtime reads from disk await session.send( "What's in this image?", attachments=[ @@ -282,6 +283,18 @@ await session.send( } ], ) + +# Blob attachment — provide base64 data directly +await session.send( + "What's in this image?", + attachments=[ + { + "type": "blob", + "data": base64_image_data, + "mimeType": "image/png", + } + ], +) ``` Supported image formats include JPG, PNG, GIF, and other common image types. The agent's `view` tool can also read images directly from the filesystem, so you can also ask questions like: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index c25ea4021e..03f8e89b74 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -8,10 +8,14 @@ from .session import CopilotSession from .tools import define_tool from .types import ( + Attachment, AzureProviderOptions, + BlobAttachment, ConnectionState, CustomAgentConfig, + DirectoryAttachment, ExternalServerConfig, + FileAttachment, GetAuthStatusResponse, GetStatusResponse, MCPLocalServerConfig, @@ -27,6 +31,7 @@ PingResponse, ProviderConfig, ResumeSessionConfig, + SelectionAttachment, SessionConfig, SessionContext, SessionEvent, @@ -44,12 +49,16 @@ __version__ = "0.1.0" __all__ = [ + "Attachment", "AzureProviderOptions", + "BlobAttachment", "CopilotClient", "CopilotSession", "ConnectionState", "CustomAgentConfig", + "DirectoryAttachment", "ExternalServerConfig", + "FileAttachment", "GetAuthStatusResponse", "GetStatusResponse", "MCPLocalServerConfig", @@ -65,6 +74,7 @@ "PingResponse", "ProviderConfig", "ResumeSessionConfig", + "SelectionAttachment", "SessionConfig", "SessionContext", "SessionEvent", diff --git a/python/copilot/types.py b/python/copilot/types.py index af124bb0a9..0a6e98867d 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -65,8 +65,19 @@ class SelectionAttachment(TypedDict): text: NotRequired[str] +class BlobAttachment(TypedDict): + """Inline base64-encoded content attachment (e.g. images).""" + + type: Literal["blob"] + data: str + """Base64-encoded content""" + mimeType: str + """MIME type of the inline data""" + displayName: NotRequired[str] + + # Attachment type - union of all attachment types -Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment +Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment # Configuration for OpenTelemetry integration with the Copilot CLI. diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index a2bc33bdb0..272fd94a60 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -569,6 +569,33 @@ def on_event(event): assert event.data.new_model == "gpt-4.1" assert event.data.reasoning_effort == "high" + async def test_should_accept_blob_attachments(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + # 1x1 transparent PNG pixel, base64-encoded + pixel_png = ( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAY" + "AAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhg" + "GAWjR9awAAAABJRU5ErkJggg==" + ) + + await session.send( + "Describe this image", + attachments=[ + { + "type": "blob", + "data": pixel_png, + "mimeType": "image/png", + "displayName": "test-pixel.png", + }, + ], + ) + + # Just verify send doesn't throw — blob attachment support varies by runtime + await session.disconnect() + def _get_system_message(exchange: dict) -> str: messages = exchange.get("request", {}).get("messages", []) diff --git a/test/scenarios/prompts/attachments/README.md b/test/scenarios/prompts/attachments/README.md index 8c8239b239..d61a26e577 100644 --- a/test/scenarios/prompts/attachments/README.md +++ b/test/scenarios/prompts/attachments/README.md @@ -11,19 +11,36 @@ Demonstrates sending **file attachments** alongside a prompt using the Copilot S ## Attachment Format +### File Attachment + | Field | Value | Description | |-------|-------|-------------| | `type` | `"file"` | Indicates a local file attachment | | `path` | Absolute path to file | The SDK reads and sends the file content to the model | +### Blob Attachment + +| Field | Value | Description | +|-------|-------|-------------| +| `type` | `"blob"` | Indicates an inline data attachment | +| `data` | Base64-encoded string | The file content encoded as base64 | +| `mimeType` | MIME type string | The MIME type of the data (e.g., `"image/png"`) | +| `displayName` | *(optional)* string | User-facing display name for the attachment | + ### Language-Specific Usage -| Language | Attachment Syntax | -|----------|------------------| +| Language | File Attachment Syntax | +|----------|------------------------| | TypeScript | `attachments: [{ type: "file", path: sampleFile }]` | | Python | `"attachments": [{"type": "file", "path": sample_file}]` | | Go | `Attachments: []copilot.Attachment{{Type: "file", Path: sampleFile}}` | +| Language | Blob Attachment Syntax | +|----------|------------------------| +| TypeScript | `attachments: [{ type: "blob", data: base64Data, mimeType: "image/png" }]` | +| Python | `"attachments": [{"type": "blob", "data": base64_data, "mimeType": "image/png"}]` | +| Go | `Attachments: []copilot.Attachment{{Type: copilot.Blob, Data: &data, MIMEType: &mime}}` | + ## Sample Data The `sample-data.txt` file contains basic project metadata used as the attachment target: diff --git a/test/snapshots/session/should_accept_blob_attachments.yaml b/test/snapshots/session/should_accept_blob_attachments.yaml new file mode 100644 index 0000000000..89e5d47ed4 --- /dev/null +++ b/test/snapshots/session/should_accept_blob_attachments.yaml @@ -0,0 +1,8 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Describe this image diff --git a/test/snapshots/session_config/should_accept_blob_attachments.yaml b/test/snapshots/session_config/should_accept_blob_attachments.yaml new file mode 100644 index 0000000000..89e5d47ed4 --- /dev/null +++ b/test/snapshots/session_config/should_accept_blob_attachments.yaml @@ -0,0 +1,8 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Describe this image From 2bfbb47824d5f25346666db449112dfda6bc153a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 19 Mar 2026 09:33:47 -0400 Subject: [PATCH 48/61] Add experimental API annotations from schema stability markers (#875) * Add experimental API annotations from schema stability markers Read the 'stability' field from api.schema.json and propagate it to generated code in all four SDK languages. APIs marked as experimental in the schema (fleet, agent, compaction) now carry appropriate annotations in the generated output. Changes to codegen scripts (scripts/codegen/): - utils.ts: Add stability field to RpcMethod; add isNodeFullyExperimental helper - csharp.ts: Emit [Experimental(Diagnostics.Experimental)] on types and API classes - typescript.ts: Emit /** @experimental */ JSDoc on types and groups - python.ts: Emit # Experimental comments on types and API classes, docstrings on methods - go.ts: Emit // Experimental: comments on types and API structs Design decisions: - When all methods in a group are experimental, the group/class is annotated and individual methods are not (annotation is inherited) - Data types (request/result) for experimental methods are also annotated - C# uses a Diagnostics.Experimental const ("GHCP001") referenced by all attributes - SDK csproj suppresses GHCP001 internally; consumers still see warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Strip trailing whitespace from Go quicktype output Address review feedback: quicktype generates comments with trailing whitespace which fails gofmt checks. Add a post-processing step to strip trailing whitespace from the quicktype output before writing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use tabs for Go indentation and column-align struct fields gofmt requires tab indentation (not spaces) and column-aligns struct field declarations and composite literal keys. Updated the Go codegen to emit tabs for all indentation and compute proper padding for field alignment in wrapper structs and constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use multi-line if-return blocks for gofmt compatibility gofmt expands single-line 'if err != nil { return nil, err }' into multi-line blocks. Updated Go codegen to emit the multi-line form directly, avoiding the gofmt diff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: expand single-line struct defs to multi-line for gofmt gofmt expands single-line struct definitions with semicolons into multi-line format. Updated the Go codegen to emit API struct definitions in multi-line format directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: collapse quicktype column alignment for gofmt compatibility quicktype emits wide-spaced struct fields for column alignment, but gofmt doesn't column-align when fields have interleaved comments. Added post-processing to collapse excessive spacing in quicktype struct field lines and remove trailing blank lines from quicktype output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: regenerate Go output with gofmt for proper formatting Removed manual quicktype column-alignment workaround and regenerated with Go installed locally so formatGoFile() runs gofmt properly. This ensures the committed output exactly matches what CI produces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 23 +++++ dotnet/src/GitHub.Copilot.SDK.csproj | 4 + dotnet/test/GitHub.Copilot.SDK.Test.csproj | 1 + go/rpc/generated_rpc.go | 23 ++++- nodejs/src/generated/rpc.ts | 15 +++ python/copilot/generated/rpc.py | 11 +++ scripts/codegen/csharp.ts | 55 +++++++++-- scripts/codegen/go.ts | 107 +++++++++++++++------ scripts/codegen/python.ts | 35 ++++++- scripts/codegen/typescript.ts | 18 +++- scripts/codegen/utils.ts | 16 +++ 11 files changed, 262 insertions(+), 46 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index f6ca0382f1..6fc593c126 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5,12 +5,20 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; namespace GitHub.Copilot.SDK.Rpc; +/// Diagnostic IDs for the Copilot SDK. +internal static class Diagnostics +{ + /// Indicates an experimental API that may change or be removed. + internal const string Experimental = "GHCP001"; +} + /// RPC data type for Ping operations. public class PingResult { @@ -427,6 +435,7 @@ internal class SessionWorkspaceCreateFileRequest } /// RPC data type for SessionFleetStart operations. +[Experimental(Diagnostics.Experimental)] public class SessionFleetStartResult { /// Whether fleet mode was successfully activated. @@ -435,6 +444,7 @@ public class SessionFleetStartResult } /// RPC data type for SessionFleetStart operations. +[Experimental(Diagnostics.Experimental)] internal class SessionFleetStartRequest { /// Target session identifier. @@ -463,6 +473,7 @@ public class Agent } /// RPC data type for SessionAgentList operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentListResult { /// Available custom agents. @@ -471,6 +482,7 @@ public class SessionAgentListResult } /// RPC data type for SessionAgentList operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentListRequest { /// Target session identifier. @@ -495,6 +507,7 @@ public class SessionAgentGetCurrentResultAgent } /// RPC data type for SessionAgentGetCurrent operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentGetCurrentResult { /// Currently selected custom agent, or null if using the default agent. @@ -503,6 +516,7 @@ public class SessionAgentGetCurrentResult } /// RPC data type for SessionAgentGetCurrent operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentGetCurrentRequest { /// Target session identifier. @@ -527,6 +541,7 @@ public class SessionAgentSelectResultAgent } /// RPC data type for SessionAgentSelect operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentSelectResult { /// The newly selected custom agent. @@ -535,6 +550,7 @@ public class SessionAgentSelectResult } /// RPC data type for SessionAgentSelect operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentSelectRequest { /// Target session identifier. @@ -547,11 +563,13 @@ internal class SessionAgentSelectRequest } /// RPC data type for SessionAgentDeselect operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentDeselectResult { } /// RPC data type for SessionAgentDeselect operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentDeselectRequest { /// Target session identifier. @@ -560,6 +578,7 @@ internal class SessionAgentDeselectRequest } /// RPC data type for SessionCompactionCompact operations. +[Experimental(Diagnostics.Experimental)] public class SessionCompactionCompactResult { /// Whether compaction completed successfully. @@ -576,6 +595,7 @@ public class SessionCompactionCompactResult } /// RPC data type for SessionCompactionCompact operations. +[Experimental(Diagnostics.Experimental)] internal class SessionCompactionCompactRequest { /// Target session identifier. @@ -1000,6 +1020,7 @@ public async Task CreateFileAsync(string path, } /// Provides session-scoped Fleet APIs. +[Experimental(Diagnostics.Experimental)] public class FleetApi { private readonly JsonRpc _rpc; @@ -1020,6 +1041,7 @@ public async Task StartAsync(string? prompt = null, Can } /// Provides session-scoped Agent APIs. +[Experimental(Diagnostics.Experimental)] public class AgentApi { private readonly JsonRpc _rpc; @@ -1061,6 +1083,7 @@ public async Task DeselectAsync(CancellationToken ca } /// Provides session-scoped Compaction APIs. +[Experimental(Diagnostics.Experimental)] public class CompactionApi { private readonly JsonRpc _rpc; diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 5d2502c87d..38eb0cf3aa 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -20,6 +20,10 @@ true + + $(NoWarn);GHCP001 + + true diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj index fbc9f17c37..8e0dbf6b79 100644 --- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj +++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj @@ -2,6 +2,7 @@ false + $(NoWarn);GHCP001 diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index ffe87455e4..401f38305f 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -208,16 +208,19 @@ type SessionWorkspaceCreateFileParams struct { Path string `json:"path"` } +// Experimental: SessionFleetStartResult is part of an experimental API and may change or be removed. type SessionFleetStartResult struct { // Whether fleet mode was successfully activated Started bool `json:"started"` } +// Experimental: SessionFleetStartParams is part of an experimental API and may change or be removed. type SessionFleetStartParams struct { // Optional user prompt to combine with fleet instructions Prompt *string `json:"prompt,omitempty"` } +// Experimental: SessionAgentListResult is part of an experimental API and may change or be removed. type SessionAgentListResult struct { // Available custom agents Agents []AgentElement `json:"agents"` @@ -232,6 +235,7 @@ type AgentElement struct { Name string `json:"name"` } +// Experimental: SessionAgentGetCurrentResult is part of an experimental API and may change or be removed. type SessionAgentGetCurrentResult struct { // Currently selected custom agent, or null if using the default agent Agent *SessionAgentGetCurrentResultAgent `json:"agent"` @@ -246,6 +250,7 @@ type SessionAgentGetCurrentResultAgent struct { Name string `json:"name"` } +// Experimental: SessionAgentSelectResult is part of an experimental API and may change or be removed. type SessionAgentSelectResult struct { // The newly selected custom agent Agent SessionAgentSelectResultAgent `json:"agent"` @@ -261,14 +266,17 @@ type SessionAgentSelectResultAgent struct { Name string `json:"name"` } +// Experimental: SessionAgentSelectParams is part of an experimental API and may change or be removed. type SessionAgentSelectParams struct { // Name of the custom agent to select Name string `json:"name"` } +// Experimental: SessionAgentDeselectResult is part of an experimental API and may change or be removed. type SessionAgentDeselectResult struct { } +// Experimental: SessionCompactionCompactResult is part of an experimental API and may change or be removed. type SessionCompactionCompactResult struct { // Number of messages removed during compaction MessagesRemoved float64 `json:"messagesRemoved"` @@ -402,7 +410,9 @@ type ResultUnion struct { String *string } -type ServerModelsRpcApi struct{ client *jsonrpc2.Client } +type ServerModelsRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error) { raw, err := a.client.Request("models.list", map[string]interface{}{}) @@ -416,7 +426,9 @@ func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error return &result, nil } -type ServerToolsRpcApi struct{ client *jsonrpc2.Client } +type ServerToolsRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) (*ToolsListResult, error) { raw, err := a.client.Request("tools.list", params) @@ -430,7 +442,9 @@ func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) ( return &result, nil } -type ServerAccountRpcApi struct{ client *jsonrpc2.Client } +type ServerAccountRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerAccountRpcApi) GetQuota(ctx context.Context) (*AccountGetQuotaResult, error) { raw, err := a.client.Request("account.getQuota", map[string]interface{}{}) @@ -641,6 +655,7 @@ func (a *WorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspa return &result, nil } +// Experimental: FleetRpcApi contains experimental APIs that may change or be removed. type FleetRpcApi struct { client *jsonrpc2.Client sessionID string @@ -664,6 +679,7 @@ func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams return &result, nil } +// Experimental: AgentRpcApi contains experimental APIs that may change or be removed. type AgentRpcApi struct { client *jsonrpc2.Client sessionID string @@ -724,6 +740,7 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult return &result, nil } +// Experimental: CompactionRpcApi contains experimental APIs that may change or be removed. type CompactionRpcApi struct { client *jsonrpc2.Client sessionID string diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index e5ba9ad4ca..16907fdba7 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -340,6 +340,7 @@ export interface SessionWorkspaceCreateFileParams { content: string; } +/** @experimental */ export interface SessionFleetStartResult { /** * Whether fleet mode was successfully activated @@ -347,6 +348,7 @@ export interface SessionFleetStartResult { started: boolean; } +/** @experimental */ export interface SessionFleetStartParams { /** * Target session identifier @@ -358,6 +360,7 @@ export interface SessionFleetStartParams { prompt?: string; } +/** @experimental */ export interface SessionAgentListResult { /** * Available custom agents @@ -378,6 +381,7 @@ export interface SessionAgentListResult { }[]; } +/** @experimental */ export interface SessionAgentListParams { /** * Target session identifier @@ -385,6 +389,7 @@ export interface SessionAgentListParams { sessionId: string; } +/** @experimental */ export interface SessionAgentGetCurrentResult { /** * Currently selected custom agent, or null if using the default agent @@ -405,6 +410,7 @@ export interface SessionAgentGetCurrentResult { } | null; } +/** @experimental */ export interface SessionAgentGetCurrentParams { /** * Target session identifier @@ -412,6 +418,7 @@ export interface SessionAgentGetCurrentParams { sessionId: string; } +/** @experimental */ export interface SessionAgentSelectResult { /** * The newly selected custom agent @@ -432,6 +439,7 @@ export interface SessionAgentSelectResult { }; } +/** @experimental */ export interface SessionAgentSelectParams { /** * Target session identifier @@ -443,8 +451,10 @@ export interface SessionAgentSelectParams { name: string; } +/** @experimental */ export interface SessionAgentDeselectResult {} +/** @experimental */ export interface SessionAgentDeselectParams { /** * Target session identifier @@ -452,6 +462,7 @@ export interface SessionAgentDeselectParams { sessionId: string; } +/** @experimental */ export interface SessionCompactionCompactResult { /** * Whether compaction completed successfully @@ -467,6 +478,7 @@ export interface SessionCompactionCompactResult { messagesRemoved: number; } +/** @experimental */ export interface SessionCompactionCompactParams { /** * Target session identifier @@ -660,10 +672,12 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin createFile: async (params: Omit): Promise => connection.sendRequest("session.workspace.createFile", { sessionId, ...params }), }, + /** @experimental */ fleet: { start: async (params: Omit): Promise => connection.sendRequest("session.fleet.start", { sessionId, ...params }), }, + /** @experimental */ agent: { list: async (): Promise => connection.sendRequest("session.agent.list", { sessionId }), @@ -674,6 +688,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin deselect: async (): Promise => connection.sendRequest("session.agent.deselect", { sessionId }), }, + /** @experimental */ compaction: { compact: async (): Promise => connection.sendRequest("session.compaction.compact", { sessionId }), diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 29b7463dfe..564ccf64ec 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -724,6 +724,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFleetStartResult: started: bool @@ -741,6 +742,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFleetStartParams: prompt: str | None = None @@ -786,6 +788,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentListResult: agents: list[AgentElement] @@ -830,6 +833,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentGetCurrentResult: agent: SessionAgentGetCurrentResultAgent | None = None @@ -876,6 +880,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentSelectResult: agent: SessionAgentSelectResultAgent @@ -893,6 +898,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentSelectParams: name: str @@ -910,6 +916,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentDeselectResult: @staticmethod @@ -922,6 +929,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionCompactionCompactResult: messages_removed: float @@ -1666,6 +1674,7 @@ async def create_file(self, params: SessionWorkspaceCreateFileParams, *, timeout return SessionWorkspaceCreateFileResult.from_dict(await self._client.request("session.workspace.createFile", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class FleetApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -1677,6 +1686,7 @@ async def start(self, params: SessionFleetStartParams, *, timeout: float | None return SessionFleetStartResult.from_dict(await self._client.request("session.fleet.start", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class AgentApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -1697,6 +1707,7 @@ async def deselect(self, *, timeout: float | None = None) -> SessionAgentDeselec return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class CompactionApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 3aeb0eef35..57e8fcbcbb 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -16,6 +16,7 @@ import { getApiSchemaPath, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, EXCLUDED_EVENT_TYPES, REPO_ROOT, type ApiSchema, @@ -594,6 +595,7 @@ export async function generateSessionEvents(schemaPath?: string): Promise // ══════════════════════════════════════════════════════════════════════════════ let emittedRpcClasses = new Set(); +let experimentalRpcTypes = new Set(); let rpcKnownTypes = new Map(); let rpcEnumOutput: string[] = []; @@ -651,6 +653,9 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi const requiredSet = new Set(schema.required || []); const lines: string[] = []; lines.push(...xmlDocComment(schema.description || `RPC data type for ${className.replace(/Request$/, "").replace(/Result$/, "")} operations.`, "")); + if (experimentalRpcTypes.has(className)) { + lines.push(`[Experimental(Diagnostics.Experimental)]`); + } lines.push(`${visibility} class ${className}`, `{`); const props = Object.entries(schema.properties || {}); @@ -712,7 +717,7 @@ function emitServerRpcClasses(node: Record, classes: string[]): // Top-level methods (like ping) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitServerInstanceMethod(key, value, srLines, classes, " "); + emitServerInstanceMethod(key, value, srLines, classes, " ", false); } // Group properties @@ -737,6 +742,10 @@ function emitServerApiClass(className: string, node: Record, cl const lines: string[] = []; const displayName = className.replace(/^Server/, "").replace(/Api$/, ""); lines.push(`/// Provides server-scoped ${displayName} APIs.`); + const groupExperimental = isNodeFullyExperimental(node); + if (groupExperimental) { + lines.push(`[Experimental(Diagnostics.Experimental)]`); + } lines.push(`public class ${className}`); lines.push(`{`); lines.push(` private readonly JsonRpc _rpc;`); @@ -748,7 +757,7 @@ function emitServerApiClass(className: string, node: Record, cl for (const [key, value] of Object.entries(node)) { if (!isRpcMethod(value)) continue; - emitServerInstanceMethod(key, value, lines, classes, " "); + emitServerInstanceMethod(key, value, lines, classes, " ", groupExperimental); } lines.push(`}`); @@ -757,13 +766,17 @@ function emitServerApiClass(className: string, node: Record, cl function emitServerInstanceMethod( name: string, - method: { rpcMethod: string; params: JSONSchema7 | null; result: JSONSchema7 }, + method: RpcMethod, lines: string[], classes: string[], - indent: string + indent: string, + groupExperimental: boolean ): void { const methodName = toPascalCase(name); const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(resultClassName); + } const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); if (resultClass) classes.push(resultClass); @@ -773,12 +786,18 @@ function emitServerInstanceMethod( let requestClassName: string | null = null; if (paramEntries.length > 0) { requestClassName = `${typeToClassName(method.rpcMethod)}Request`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(requestClassName); + } const reqClass = emitRpcClass(requestClassName, method.params!, "internal", classes); if (reqClass) classes.push(reqClass); } lines.push(""); lines.push(`${indent}/// Calls "${method.rpcMethod}".`); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + } const sigParams: string[] = []; const bodyAssignments: string[] = []; @@ -817,7 +836,7 @@ function emitSessionRpcClasses(node: Record, classes: string[]) // Emit top-level session RPC methods directly on the SessionRpc class const topLevelLines: string[] = []; for (const [key, value] of topLevelMethods) { - emitSessionMethod(key, value as RpcMethod, topLevelLines, classes, " "); + emitSessionMethod(key, value as RpcMethod, topLevelLines, classes, " ", false); } srLines.push(...topLevelLines); @@ -830,9 +849,12 @@ function emitSessionRpcClasses(node: Record, classes: string[]) return result; } -function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string): void { +function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string, groupExperimental: boolean): void { const methodName = toPascalCase(key); const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(resultClassName); + } const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); if (resultClass) classes.push(resultClass); @@ -847,12 +869,18 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas }); const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(requestClassName); + } if (method.params) { const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); if (reqClass) classes.push(reqClass); } lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + } const sigParams: string[] = []; const bodyAssignments = [`SessionId = _sessionId`]; @@ -872,12 +900,14 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas function emitSessionApiClass(className: string, node: Record, classes: string[]): string { const displayName = className.replace(/Api$/, ""); - const lines = [`/// Provides session-scoped ${displayName} APIs.`, `public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; + const groupExperimental = isNodeFullyExperimental(node); + const experimentalAttr = groupExperimental ? `[Experimental(Diagnostics.Experimental)]\n` : ""; + const lines = [`/// Provides session-scoped ${displayName} APIs.`, `${experimentalAttr}public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; lines.push(` internal ${className}(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`, ` }`); for (const [key, value] of Object.entries(node)) { if (!isRpcMethod(value)) continue; - emitSessionMethod(key, value, lines, classes, " "); + emitSessionMethod(key, value, lines, classes, " ", groupExperimental); } lines.push(`}`); return lines.join("\n"); @@ -885,6 +915,7 @@ function emitSessionApiClass(className: string, node: Record, c function generateRpcCode(schema: ApiSchema): string { emittedRpcClasses.clear(); + experimentalRpcTypes.clear(); rpcKnownTypes.clear(); rpcEnumOutput = []; generatedEnums.clear(); // Clear shared enum deduplication map @@ -902,11 +933,19 @@ function generateRpcCode(schema: ApiSchema): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; namespace GitHub.Copilot.SDK.Rpc; + +/// Diagnostic IDs for the Copilot SDK. +internal static class Diagnostics +{ + /// Indicates an experimental API that may change or be removed. + internal const string Experimental = "GHCP001"; +} `); for (const cls of classes) if (cls) lines.push(cls, ""); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 1ebc50797d..c467761d0d 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -17,6 +17,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -161,16 +162,35 @@ async function generateRpc(schemaPath?: string): Promise { lines.push(`package rpc`); lines.push(``); lines.push(`import (`); - lines.push(` "context"`); - lines.push(` "encoding/json"`); + lines.push(`\t"context"`); + lines.push(`\t"encoding/json"`); lines.push(``); - lines.push(` "github.com/github/copilot-sdk/go/internal/jsonrpc2"`); + lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); lines.push(`)`); lines.push(``); - // Add quicktype-generated types (skip package line) - const qtLines = qtResult.lines.filter((l) => !l.startsWith("package ")); - lines.push(...qtLines); + // Add quicktype-generated types (skip package line), annotating experimental types + const experimentalTypeNames = new Set(); + for (const method of allMethods) { + if (method.stability !== "experimental") continue; + experimentalTypeNames.add(toPascalCase(method.rpcMethod) + "Result"); + const baseName = toPascalCase(method.rpcMethod); + if (combinedSchema.definitions![baseName + "Params"]) { + experimentalTypeNames.add(baseName + "Params"); + } + } + let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n"); + // Strip trailing whitespace from quicktype output (gofmt requirement) + qtCode = qtCode.replace(/[ \t]+$/gm, ""); + for (const typeName of experimentalTypeNames) { + qtCode = qtCode.replace( + new RegExp(`^(type ${typeName} struct)`, "m"), + `// Experimental: ${typeName} is part of an experimental API and may change or be removed.\n$1` + ); + } + // Remove trailing blank lines from quicktype output before appending + qtCode = qtCode.replace(/\n+$/, ""); + lines.push(qtCode); lines.push(``); // Emit ServerRpc @@ -200,23 +220,39 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio for (const [groupName, groupNode] of groups) { const prefix = isSession ? "" : "Server"; const apiName = prefix + toPascalCase(groupName) + apiSuffix; - const fields = isSession ? "client *jsonrpc2.Client; sessionID string" : "client *jsonrpc2.Client"; - lines.push(`type ${apiName} struct { ${fields} }`); + const groupExperimental = isNodeFullyExperimental(groupNode as Record); + if (groupExperimental) { + lines.push(`// Experimental: ${apiName} contains experimental APIs that may change or be removed.`); + } + lines.push(`type ${apiName} struct {`); + if (isSession) { + lines.push(`\tclient *jsonrpc2.Client`); + lines.push(`\tsessionID string`); + } else { + lines.push(`\tclient *jsonrpc2.Client`); + } + lines.push(`}`); lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, apiName, key, value, isSession); + emitMethod(lines, apiName, key, value, isSession, groupExperimental); } } + // Compute field name lengths for gofmt-compatible column alignment + const groupPascalNames = groups.map(([g]) => toPascalCase(g)); + const allFieldNames = isSession ? ["client", "sessionID", ...groupPascalNames] : ["client", ...groupPascalNames]; + const maxFieldLen = Math.max(...allFieldNames.map((n) => n.length)); + const pad = (name: string) => name.padEnd(maxFieldLen); + // Emit wrapper struct lines.push(`// ${wrapperName} provides typed ${isSession ? "session" : "server"}-scoped RPC methods.`); lines.push(`type ${wrapperName} struct {`); - lines.push(` client *jsonrpc2.Client`); - if (isSession) lines.push(` sessionID string`); + lines.push(`\t${pad("client")} *jsonrpc2.Client`); + if (isSession) lines.push(`\t${pad("sessionID")} string`); for (const [groupName] of groups) { const prefix = isSession ? "" : "Server"; - lines.push(` ${toPascalCase(groupName)} *${prefix}${toPascalCase(groupName)}${apiSuffix}`); + lines.push(`\t${pad(toPascalCase(groupName))} *${prefix}${toPascalCase(groupName)}${apiSuffix}`); } lines.push(`}`); lines.push(``); @@ -224,27 +260,31 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods (server only) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, wrapperName, key, value, isSession); + emitMethod(lines, wrapperName, key, value, isSession, false); } + // Compute key alignment for constructor composite literal (gofmt aligns key: value) + const maxKeyLen = Math.max(...groupPascalNames.map((n) => n.length + 1)); // +1 for colon + const padKey = (name: string) => (name + ":").padEnd(maxKeyLen + 1); // +1 for min trailing space + // Constructor const ctorParams = isSession ? "client *jsonrpc2.Client, sessionID string" : "client *jsonrpc2.Client"; const ctorFields = isSession ? "client: client, sessionID: sessionID," : "client: client,"; lines.push(`func New${wrapperName}(${ctorParams}) *${wrapperName} {`); - lines.push(` return &${wrapperName}{${ctorFields}`); + lines.push(`\treturn &${wrapperName}{${ctorFields}`); for (const [groupName] of groups) { const prefix = isSession ? "" : "Server"; const apiInit = isSession ? `&${toPascalCase(groupName)}${apiSuffix}{client: client, sessionID: sessionID}` : `&${prefix}${toPascalCase(groupName)}${apiSuffix}{client: client}`; - lines.push(` ${toPascalCase(groupName)}: ${apiInit},`); + lines.push(`\t\t${padKey(toPascalCase(groupName))}${apiInit},`); } - lines.push(` }`); + lines.push(`\t}`); lines.push(`}`); lines.push(``); } -function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { const methodName = toPascalCase(name); const resultType = toPascalCase(method.rpcMethod) + "Result"; @@ -254,6 +294,9 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; const paramsType = hasParams ? toPascalCase(method.rpcMethod) + "Params" : ""; + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`// Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`); + } const sig = hasParams ? `func (a *${receiver}) ${methodName}(ctx context.Context, params *${paramsType}) (*${resultType}, error)` : `func (a *${receiver}) ${methodName}(ctx context.Context) (*${resultType}, error)`; @@ -261,33 +304,37 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc lines.push(sig + ` {`); if (isSession) { - lines.push(` req := map[string]interface{}{"sessionId": a.sessionID}`); + lines.push(`\treq := map[string]interface{}{"sessionId": a.sessionID}`); if (hasParams) { - lines.push(` if params != nil {`); + lines.push(`\tif params != nil {`); for (const pName of nonSessionParams) { const goField = toGoFieldName(pName); const isOptional = !requiredParams.has(pName); if (isOptional) { // Optional fields are pointers - only add when non-nil and dereference - lines.push(` if params.${goField} != nil {`); - lines.push(` req["${pName}"] = *params.${goField}`); - lines.push(` }`); + lines.push(`\t\tif params.${goField} != nil {`); + lines.push(`\t\t\treq["${pName}"] = *params.${goField}`); + lines.push(`\t\t}`); } else { - lines.push(` req["${pName}"] = params.${goField}`); + lines.push(`\t\treq["${pName}"] = params.${goField}`); } } - lines.push(` }`); + lines.push(`\t}`); } - lines.push(` raw, err := a.client.Request("${method.rpcMethod}", req)`); + lines.push(`\traw, err := a.client.Request("${method.rpcMethod}", req)`); } else { const arg = hasParams ? "params" : "map[string]interface{}{}"; - lines.push(` raw, err := a.client.Request("${method.rpcMethod}", ${arg})`); + lines.push(`\traw, err := a.client.Request("${method.rpcMethod}", ${arg})`); } - lines.push(` if err != nil { return nil, err }`); - lines.push(` var result ${resultType}`); - lines.push(` if err := json.Unmarshal(raw, &result); err != nil { return nil, err }`); - lines.push(` return &result, nil`); + lines.push(`\tif err != nil {`); + lines.push(`\t\treturn nil, err`); + lines.push(`\t}`); + lines.push(`\tvar result ${resultType}`); + lines.push(`\tif err := json.Unmarshal(raw, &result); err != nil {`); + lines.push(`\t\treturn nil, err`); + lines.push(`\t}`); + lines.push(`\treturn &result, nil`); lines.push(`}`); lines.push(``); } diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 65563d7411..3dfa525353 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -15,6 +15,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -215,6 +216,23 @@ async function generateRpc(schemaPath?: string): Promise { // Modernize to Python 3.11+ syntax typesCode = modernizePython(typesCode); + // Annotate experimental data types + const experimentalTypeNames = new Set(); + for (const method of allMethods) { + if (method.stability !== "experimental") continue; + experimentalTypeNames.add(toPascalCase(method.rpcMethod) + "Result"); + const baseName = toPascalCase(method.rpcMethod); + if (combinedSchema.definitions![baseName + "Params"]) { + experimentalTypeNames.add(baseName + "Params"); + } + } + for (const typeName of experimentalTypeNames) { + typesCode = typesCode.replace( + new RegExp(`^(@dataclass\\n)?class ${typeName}[:(]`, "m"), + (match) => `# Experimental: this type is part of an experimental API and may change or be removed.\n${match}` + ); + } + const lines: string[] = []; lines.push(`""" AUTO-GENERATED FILE - DO NOT EDIT @@ -259,12 +277,19 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio for (const [groupName, groupNode] of groups) { const prefix = isSession ? "" : "Server"; const apiName = prefix + toPascalCase(groupName) + "Api"; + const groupExperimental = isNodeFullyExperimental(groupNode as Record); if (isSession) { + if (groupExperimental) { + lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + } lines.push(`class ${apiName}:`); lines.push(` def __init__(self, client: "JsonRpcClient", session_id: str):`); lines.push(` self._client = client`); lines.push(` self._session_id = session_id`); } else { + if (groupExperimental) { + lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + } lines.push(`class ${apiName}:`); lines.push(` def __init__(self, client: "JsonRpcClient"):`); lines.push(` self._client = client`); @@ -272,7 +297,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, groupExperimental); } lines.push(``); } @@ -301,12 +326,12 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, false); } lines.push(``); } -function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { const methodName = toSnakeCase(name); const resultType = toPascalCase(method.rpcMethod) + "Result"; @@ -322,6 +347,10 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: lines.push(sig); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(` """.. warning:: This API is experimental and may change or be removed in future versions."""`); + } + // Build request body with proper serialization/deserialization if (isSession) { if (hasParams) { diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 77c31019a1..8d23b428fd 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -15,6 +15,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -91,6 +92,9 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; bannerComment: "", additionalProperties: false, }); + if (method.stability === "experimental") { + lines.push("/** @experimental */"); + } lines.push(compiled.trim()); lines.push(""); @@ -99,6 +103,9 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; bannerComment: "", additionalProperties: false, }); + if (method.stability === "experimental") { + lines.push("/** @experimental */"); + } lines.push(paramsCompiled.trim()); lines.push(""); } @@ -129,7 +136,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; console.log(` ✓ ${outPath}`); } -function emitGroup(node: Record, indent: string, isSession: boolean): string[] { +function emitGroup(node: Record, indent: string, isSession: boolean, parentExperimental = false): string[] { const lines: string[] = []; for (const [key, value] of Object.entries(node)) { if (isRpcMethod(value)) { @@ -160,11 +167,18 @@ function emitGroup(node: Record, indent: string, isSession: boo } } + if ((value as RpcMethod).stability === "experimental" && !parentExperimental) { + lines.push(`${indent}/** @experimental */`); + } lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); } else if (typeof value === "object" && value !== null) { + const groupExperimental = isNodeFullyExperimental(value as Record); + if (groupExperimental) { + lines.push(`${indent}/** @experimental */`); + } lines.push(`${indent}${key}: {`); - lines.push(...emitGroup(value as Record, indent + " ", isSession)); + lines.push(...emitGroup(value as Record, indent + " ", isSession, groupExperimental)); lines.push(`${indent}},`); } } diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 88ca68de80..2c13b1d965 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -126,6 +126,7 @@ export interface RpcMethod { rpcMethod: string; params: JSONSchema7 | null; result: JSONSchema7; + stability?: string; } export interface ApiSchema { @@ -136,3 +137,18 @@ export interface ApiSchema { export function isRpcMethod(node: unknown): node is RpcMethod { return typeof node === "object" && node !== null && "rpcMethod" in node; } + +/** Returns true when every leaf RPC method inside `node` is marked experimental. */ +export function isNodeFullyExperimental(node: Record): boolean { + const methods: RpcMethod[] = []; + (function collect(n: Record) { + for (const value of Object.values(n)) { + if (isRpcMethod(value)) { + methods.push(value); + } else if (typeof value === "object" && value !== null) { + collect(value as Record); + } + } + })(node); + return methods.length > 0 && methods.every(m => m.stability === "experimental"); +} From d96488c428c1c200709145dae63af7913018a7aa Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 19 Mar 2026 06:46:20 -0700 Subject: [PATCH 49/61] [Python] Declare various modules as private (#884) * Declare various modules private - `telemetry` - `jsonrpc` - `sdk_protocol_version` * Add back in trailing whitespace --------- Co-authored-by: Steve Sanderson --- nodejs/scripts/update-protocol-version.ts | 6 +++--- python/copilot/{jsonrpc.py => _jsonrpc.py} | 0 .../{sdk_protocol_version.py => _sdk_protocol_version.py} | 0 python/copilot/{telemetry.py => _telemetry.py} | 0 python/copilot/client.py | 6 +++--- python/copilot/generated/rpc.py | 2 +- python/copilot/session.py | 4 ++-- python/test_jsonrpc.py | 2 +- python/test_telemetry.py | 2 +- scripts/codegen/python.ts | 5 +++-- 10 files changed, 14 insertions(+), 13 deletions(-) rename python/copilot/{jsonrpc.py => _jsonrpc.py} (100%) rename python/copilot/{sdk_protocol_version.py => _sdk_protocol_version.py} (100%) rename python/copilot/{telemetry.py => _telemetry.py} (100%) diff --git a/nodejs/scripts/update-protocol-version.ts b/nodejs/scripts/update-protocol-version.ts index 46f6189e86..a18a560c75 100644 --- a/nodejs/scripts/update-protocol-version.ts +++ b/nodejs/scripts/update-protocol-version.ts @@ -8,7 +8,7 @@ * Reads from sdk-protocol-version.json and generates: * - nodejs/src/sdkProtocolVersion.ts * - go/sdk_protocol_version.go - * - python/copilot/sdk_protocol_version.py + * - python/copilot/_sdk_protocol_version.py * - dotnet/src/SdkProtocolVersion.cs * * Run this script whenever the protocol version changes. @@ -89,8 +89,8 @@ def get_sdk_protocol_version() -> int: """ return SDK_PROTOCOL_VERSION `; -fs.writeFileSync(path.join(rootDir, "python", "copilot", "sdk_protocol_version.py"), pythonCode); -console.log(" ✓ python/copilot/sdk_protocol_version.py"); +fs.writeFileSync(path.join(rootDir, "python", "copilot", "_sdk_protocol_version.py"), pythonCode); +console.log(" ✓ python/copilot/_sdk_protocol_version.py"); // Generate C# const csharpCode = `// Code generated by update-protocol-version.ts. DO NOT EDIT. diff --git a/python/copilot/jsonrpc.py b/python/copilot/_jsonrpc.py similarity index 100% rename from python/copilot/jsonrpc.py rename to python/copilot/_jsonrpc.py diff --git a/python/copilot/sdk_protocol_version.py b/python/copilot/_sdk_protocol_version.py similarity index 100% rename from python/copilot/sdk_protocol_version.py rename to python/copilot/_sdk_protocol_version.py diff --git a/python/copilot/telemetry.py b/python/copilot/_telemetry.py similarity index 100% rename from python/copilot/telemetry.py rename to python/copilot/_telemetry.py diff --git a/python/copilot/client.py b/python/copilot/client.py index 0d8074fe0a..81c1459f21 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -25,12 +25,12 @@ from pathlib import Path from typing import Any, cast, overload +from ._jsonrpc import JsonRpcClient, ProcessExitedError +from ._sdk_protocol_version import get_sdk_protocol_version +from ._telemetry import get_trace_context, trace_context from .generated.rpc import ServerRpc from .generated.session_events import PermissionRequest, session_event_from_dict -from .jsonrpc import JsonRpcClient, ProcessExitedError -from .sdk_protocol_version import get_sdk_protocol_version from .session import CopilotSession -from .telemetry import get_trace_context, trace_context from .types import ( ConnectionState, CustomAgentConfig, diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 564ccf64ec..da6748d799 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ..jsonrpc import JsonRpcClient + from .._jsonrpc import JsonRpcClient from dataclasses import dataclass diff --git a/python/copilot/session.py b/python/copilot/session.py index e4a17f2f93..90d156c4ce 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -11,6 +11,8 @@ from collections.abc import Callable from typing import Any, Literal, cast +from ._jsonrpc import JsonRpcError, ProcessExitedError +from ._telemetry import get_trace_context, trace_context from .generated.rpc import ( Kind, Level, @@ -23,8 +25,6 @@ SessionToolsHandlePendingToolCallParams, ) from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict -from .jsonrpc import JsonRpcError, ProcessExitedError -from .telemetry import get_trace_context, trace_context from .types import ( Attachment, PermissionRequest, diff --git a/python/test_jsonrpc.py b/python/test_jsonrpc.py index 7c3c8dab2a..c0ab2c6f42 100644 --- a/python/test_jsonrpc.py +++ b/python/test_jsonrpc.py @@ -13,7 +13,7 @@ import pytest -from copilot.jsonrpc import JsonRpcClient +from copilot._jsonrpc import JsonRpcClient class MockProcess: diff --git a/python/test_telemetry.py b/python/test_telemetry.py index 2b4649011b..aec38f8168 100644 --- a/python/test_telemetry.py +++ b/python/test_telemetry.py @@ -4,7 +4,7 @@ from unittest.mock import patch -from copilot.telemetry import get_trace_context, trace_context +from copilot._telemetry import get_trace_context, trace_context from copilot.types import SubprocessConfig, TelemetryConfig diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 3dfa525353..cbbc3df385 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -10,8 +10,9 @@ import fs from "fs/promises"; import type { JSONSchema7 } from "json-schema"; import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from "quicktype-core"; import { - getSessionEventsSchemaPath, getApiSchemaPath, + getSessionEventsSchemaPath, + isRpcMethod, postProcessSchema, writeGeneratedFile, isRpcMethod, @@ -242,7 +243,7 @@ Generated from: api.schema.json from typing import TYPE_CHECKING if TYPE_CHECKING: - from ..jsonrpc import JsonRpcClient + from .._jsonrpc import JsonRpcClient `); lines.push(typesCode); From 3e443fe6d36018bfdffb21bb9591ca7b1bc26421 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Thu, 19 Mar 2026 15:29:48 +0100 Subject: [PATCH 50/61] Fix serialization of SessionEvent (#868) --- dotnet/src/Generated/SessionEvents.cs | 1 + dotnet/test/SessionEventSerializationTests.cs | 180 ++++++++++++++++++ scripts/codegen/csharp.ts | 1 + 3 files changed, 182 insertions(+) create mode 100644 dotnet/test/SessionEventSerializationTests.cs diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 5ef1be3524..08c6bf5e05 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -3527,4 +3527,5 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionEnd))] [JsonSerializable(typeof(UserMessageDataAttachmentsItemSelectionSelectionStart))] [JsonSerializable(typeof(UserMessageEvent))] +[JsonSerializable(typeof(JsonElement))] internal partial class SessionEventsJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/dotnet/test/SessionEventSerializationTests.cs b/dotnet/test/SessionEventSerializationTests.cs new file mode 100644 index 0000000000..e7be64422c --- /dev/null +++ b/dotnet/test/SessionEventSerializationTests.cs @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Collections.Generic; +using System.Text.Json; +using Xunit; + +namespace GitHub.Copilot.SDK.Test; + +public class SessionEventSerializationTests +{ + public static TheoryData JsonElementBackedEvents => new() + { + { + new AssistantMessageEvent + { + Id = Guid.Parse("11111111-1111-1111-1111-111111111111"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("22222222-2222-2222-2222-222222222222"), + Data = new AssistantMessageData + { + MessageId = "msg-1", + Content = "", + ToolRequests = + [ + new AssistantMessageDataToolRequestsItem + { + ToolCallId = "call-1", + Name = "view", + Arguments = ParseJsonElement("""{"path":"README.md"}"""), + Type = AssistantMessageDataToolRequestsItemType.Function, + }, + ], + }, + }, + "assistant.message" + }, + { + new ToolExecutionStartEvent + { + Id = Guid.Parse("33333333-3333-3333-3333-333333333333"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("44444444-4444-4444-4444-444444444444"), + Data = new ToolExecutionStartData + { + ToolCallId = "call-1", + ToolName = "view", + Arguments = ParseJsonElement("""{"path":"README.md"}"""), + }, + }, + "tool.execution_start" + }, + { + new ToolExecutionCompleteEvent + { + Id = Guid.Parse("55555555-5555-5555-5555-555555555555"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"), + ParentId = Guid.Parse("66666666-6666-6666-6666-666666666666"), + Data = new ToolExecutionCompleteData + { + ToolCallId = "call-1", + Success = true, + Result = new ToolExecutionCompleteDataResult + { + Content = "ok", + DetailedContent = "ok", + }, + ToolTelemetry = new Dictionary + { + ["properties"] = ParseJsonElement("""{"command":"view"}"""), + ["metrics"] = ParseJsonElement("""{"resultLength":2}"""), + }, + }, + }, + "tool.execution_complete" + }, + { + new SessionShutdownEvent + { + Id = Guid.Parse("77777777-7777-7777-7777-777777777777"), + Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:52.987Z"), + ParentId = Guid.Parse("88888888-8888-8888-8888-888888888888"), + Data = new SessionShutdownData + { + ShutdownType = SessionShutdownDataShutdownType.Routine, + TotalPremiumRequests = 1, + TotalApiDurationMs = 100, + SessionStartTime = 1773609948932, + CodeChanges = new SessionShutdownDataCodeChanges + { + LinesAdded = 1, + LinesRemoved = 0, + FilesModified = ["README.md"], + }, + ModelMetrics = new Dictionary + { + ["gpt-5.4"] = ParseJsonElement(""" + { + "requests": { + "count": 1, + "cost": 1 + }, + "usage": { + "inputTokens": 10, + "outputTokens": 5, + "cacheReadTokens": 0, + "cacheWriteTokens": 0 + } + } + """), + }, + CurrentModel = "gpt-5.4", + }, + }, + "session.shutdown" + } + }; + + private static JsonElement ParseJsonElement(string json) + { + using var document = JsonDocument.Parse(json); + return document.RootElement.Clone(); + } + + [Theory] + [MemberData(nameof(JsonElementBackedEvents))] + public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEvent sessionEvent, string expectedType) + { + var serialized = sessionEvent.ToJson(); + + using var document = JsonDocument.Parse(serialized); + var root = document.RootElement; + + Assert.Equal(expectedType, root.GetProperty("type").GetString()); + + switch (expectedType) + { + case "assistant.message": + Assert.Equal( + "README.md", + root.GetProperty("data") + .GetProperty("toolRequests")[0] + .GetProperty("arguments") + .GetProperty("path") + .GetString()); + break; + + case "tool.execution_start": + Assert.Equal( + "README.md", + root.GetProperty("data") + .GetProperty("arguments") + .GetProperty("path") + .GetString()); + break; + + case "tool.execution_complete": + Assert.Equal( + "view", + root.GetProperty("data") + .GetProperty("toolTelemetry") + .GetProperty("properties") + .GetProperty("command") + .GetString()); + break; + + case "session.shutdown": + Assert.Equal( + 1, + root.GetProperty("data") + .GetProperty("modelMetrics") + .GetProperty("gpt-5.4") + .GetProperty("requests") + .GetProperty("count") + .GetInt32()); + break; + } + } +} diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 57e8fcbcbb..c44973fb11 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -575,6 +575,7 @@ namespace GitHub.Copilot.SDK; const types = ["SessionEvent", ...variants.flatMap((v) => [v.className, v.dataClassName]), ...nestedClasses.keys()].sort(); lines.push(`[JsonSourceGenerationOptions(`, ` JsonSerializerDefaults.Web,`, ` AllowOutOfOrderMetadataProperties = true,`, ` NumberHandling = JsonNumberHandling.AllowReadingFromString,`, ` DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]`); for (const t of types) lines.push(`[JsonSerializable(typeof(${t}))]`); + lines.push(`[JsonSerializable(typeof(JsonElement))]`); lines.push(`internal partial class SessionEventsJsonContext : JsonSerializerContext;`); return lines.join("\n"); From 1a94402aee68abafb4b3efd0034c3c5a65a127f7 Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Thu, 19 Mar 2026 15:44:19 +0100 Subject: [PATCH 51/61] detach cli lifespan from the context passed to Client.Start (#689) --- go/client.go | 55 ++++++++++++++++++++--------------------------- go/client_test.go | 26 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/go/client.go b/go/client.go index 751ce63479..a2431ad395 100644 --- a/go/client.go +++ b/go/client.go @@ -443,12 +443,12 @@ func (c *Client) ForceStop() { c.RPC = nil } -func (c *Client) ensureConnected() error { +func (c *Client) ensureConnected(ctx context.Context) error { if c.client != nil { return nil } if c.autoStart { - return c.Start(context.Background()) + return c.Start(ctx) } return fmt.Errorf("client not connected. Call Start() first") } @@ -487,7 +487,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses return nil, fmt.Errorf("an OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}") } - if err := c.ensureConnected(); err != nil { + if err := c.ensureConnected(ctx); err != nil { return nil, err } @@ -607,7 +607,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, return nil, fmt.Errorf("an OnPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use &copilot.ResumeSessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}") } - if err := c.ensureConnected(); err != nil { + if err := c.ensureConnected(ctx); err != nil { return nil, err } @@ -715,7 +715,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, // // sessions, err := client.ListSessions(context.Background(), &SessionListFilter{Repository: "owner/repo"}) func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([]SessionMetadata, error) { - if err := c.ensureConnected(); err != nil { + if err := c.ensureConnected(ctx); err != nil { return nil, err } @@ -750,7 +750,7 @@ func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([ // log.Fatal(err) // } func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { - if err := c.ensureConnected(); err != nil { + if err := c.ensureConnected(ctx); err != nil { return err } @@ -797,7 +797,7 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error { // }) // } func (c *Client) GetLastSessionID(ctx context.Context) (*string, error) { - if err := c.ensureConnected(); err != nil { + if err := c.ensureConnected(ctx); err != nil { return nil, err } @@ -829,14 +829,8 @@ func (c *Client) GetLastSessionID(ctx context.Context) (*string, error) { // fmt.Printf("TUI is displaying session: %s\n", *sessionID) // } func (c *Client) GetForegroundSessionID(ctx context.Context) (*string, error) { - if c.client == nil { - if c.autoStart { - if err := c.Start(ctx); err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf("client not connected. Call Start() first") - } + if err := c.ensureConnected(ctx); err != nil { + return nil, err } result, err := c.client.Request("session.getForeground", getForegroundSessionRequest{}) @@ -863,14 +857,8 @@ func (c *Client) GetForegroundSessionID(ctx context.Context) (*string, error) { // log.Fatal(err) // } func (c *Client) SetForegroundSessionID(ctx context.Context, sessionID string) error { - if c.client == nil { - if c.autoStart { - if err := c.Start(ctx); err != nil { - return err - } - } else { - return fmt.Errorf("client not connected. Call Start() first") - } + if err := c.ensureConnected(ctx); err != nil { + return err } result, err := c.client.Request("session.setForeground", setForegroundSessionRequest{SessionID: sessionID}) @@ -1200,7 +1188,7 @@ func (c *Client) startCLIServer(ctx context.Context) error { args = append([]string{cliPath}, args...) } - c.process = exec.CommandContext(ctx, command, args...) + c.process = exec.Command(command, args...) // Configure platform-specific process attributes (e.g., hide window on Windows) configureProcAttr(c.process) @@ -1289,14 +1277,16 @@ func (c *Client) startCLIServer(ctx context.Context) error { c.monitorProcess() scanner := bufio.NewScanner(stdout) - timeout := time.After(10 * time.Second) portRegex := regexp.MustCompile(`listening on port (\d+)`) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + for { select { - case <-timeout: + case <-ctx.Done(): killErr := c.killProcess() - return errors.Join(errors.New("timeout waiting for CLI server to start"), killErr) + return errors.Join(fmt.Errorf("failed waiting for CLI server to start: %w", ctx.Err()), killErr) case <-c.processDone: killErr := c.killProcess() return errors.Join(errors.New("CLI server process exited before reporting port"), killErr) @@ -1368,12 +1358,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error { return fmt.Errorf("server port not available") } - // Create TCP connection that cancels on context done or after 10 seconds + // Merge a 10-second timeout with the caller's context so whichever + // deadline comes first wins. address := net.JoinHostPort(c.actualHost, fmt.Sprintf("%d", c.actualPort)) - dialer := net.Dialer{ - Timeout: 10 * time.Second, - } - conn, err := dialer.DialContext(ctx, "tcp", address) + dialCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + var dialer net.Dialer + conn, err := dialer.DialContext(dialCtx, "tcp", address) if err != nil { return fmt.Errorf("failed to connect to CLI server at %s: %w", address, err) } diff --git a/go/client_test.go b/go/client_test.go index 601215cbe0..d7a526cab6 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -608,6 +608,32 @@ func TestListModelsHandlerCachesResults(t *testing.T) { } } +func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) { + cliPath := findCLIPathForTest() + if cliPath == "" { + t.Skip("CLI not found") + } + + client := NewClient(&ClientOptions{CLIPath: cliPath}) + t.Cleanup(func() { client.ForceStop() }) + + // Start with a context, then cancel it after the client is connected. + ctx, cancel := context.WithCancel(t.Context()) + if err := client.Start(ctx); err != nil { + t.Fatalf("Start failed: %v", err) + } + cancel() // cancel the context that was used for Start + + // The CLI process should still be alive and responsive. + resp, err := client.Ping(t.Context(), "still alive") + if err != nil { + t.Fatalf("Ping after context cancellation failed: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil ping response") + } +} + func TestClient_StartStopRace(t *testing.T) { cliPath := findCLIPathForTest() if cliPath == "" { From 21504acf6b22ee4de0698eaace2013ca3ed57124 Mon Sep 17 00:00:00 2001 From: kirankashyap <46650420+kirankashyap@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:20:18 +0530 Subject: [PATCH 52/61] Fix justfile install 585 (#634) * Add per-language install recipes (#585) * fix #585 * Revert unnecessary changes in install recipes - Restore --ignore-scripts flag for test harness npm ci - Remove extraneous uv venv from Python install (uv run manages venvs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Kiran Kashyap Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- justfile | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/justfile b/justfile index 85cd8c61bf..fd7fc3adbe 100644 --- a/justfile +++ b/justfile @@ -71,15 +71,34 @@ test-dotnet: @echo "=== Testing .NET code ===" @cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj -# Install all dependencies -install: - @echo "=== Installing dependencies ===" - @cd nodejs && npm ci - @cd python && uv pip install -e ".[dev]" +# Install all dependencies across all languages +install: install-go install-python install-nodejs install-dotnet + @echo "✅ All dependencies installed" + +# Install Go dependencies and prerequisites for tests +install-go: install-nodejs install-test-harness + @echo "=== Installing Go dependencies ===" @cd go && go mod download + +# Install Python dependencies and prerequisites for tests +install-python: install-nodejs install-test-harness + @echo "=== Installing Python dependencies ===" + @cd python && uv pip install -e ".[dev]" + +# Install .NET dependencies and prerequisites for tests +install-dotnet: install-nodejs install-test-harness + @echo "=== Installing .NET dependencies ===" @cd dotnet && dotnet restore + +# Install Node.js dependencies +install-nodejs: + @echo "=== Installing Node.js dependencies ===" + @cd nodejs && npm ci + +# Install test harness dependencies (used by E2E tests in all languages) +install-test-harness: + @echo "=== Installing test harness dependencies ===" @cd test/harness && npm ci --ignore-scripts - @echo "✅ All dependencies installed" # Run interactive SDK playground playground: From 0fbe0f66cb6569aad74ce062e34b145abbe93cec Mon Sep 17 00:00:00 2001 From: Charles Lowell <10964656+chlowell@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:55:02 -0700 Subject: [PATCH 53/61] Go: stop RPC client logging expected errors (#609) * Go: RPC client loop expects stdout close * handle body read errors similarly --- go/internal/jsonrpc2/jsonrpc2.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/go/internal/jsonrpc2/jsonrpc2.go b/go/internal/jsonrpc2/jsonrpc2.go index 827a15cb4a..fbc5b931c4 100644 --- a/go/internal/jsonrpc2/jsonrpc2.go +++ b/go/internal/jsonrpc2/jsonrpc2.go @@ -4,8 +4,10 @@ import ( "bufio" "crypto/rand" "encoding/json" + "errors" "fmt" "io" + "os" "reflect" "sync" "sync/atomic" @@ -320,7 +322,7 @@ func (c *Client) readLoop() { line, err := reader.ReadString('\n') if err != nil { // Only log unexpected errors (not EOF or closed pipe during shutdown) - if err != io.EOF && c.running.Load() { + if err != io.EOF && !errors.Is(err, os.ErrClosed) && c.running.Load() { fmt.Printf("Error reading header: %v\n", err) } return @@ -345,7 +347,10 @@ func (c *Client) readLoop() { // Read message body body := make([]byte, contentLength) if _, err := io.ReadFull(reader, body); err != nil { - fmt.Printf("Error reading body: %v\n", err) + // Only log unexpected errors (not EOF or closed pipe during shutdown) + if err != io.EOF && !errors.Is(err, os.ErrClosed) && c.running.Load() { + fmt.Printf("Error reading body: %v\n", err) + } return } From 2b01b618c9e313b6228f9be0c0c0142f70fa2802 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 19 Mar 2026 13:09:28 -0400 Subject: [PATCH 54/61] On branch edburns/java-readme-add-maven-g-a Make the Java row more like the other rows. (#889) * On branch edburns/java-readme-add-maven-g-a Make the Java row more like the other rows. modified: README.md Signed-off-by: Ed Burns * On branch edburns/java-readme-add-maven-g-a Make the Java row more like the other rows. modified: README.md Signed-off-by: Ed Burns * Remove period for easier copy/paste. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Ed Burns Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 087fa44491..65a2339c85 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The GitHub Copilot SDK exposes the same engine behind Copilot CLI: a production- | **Python** | [`python/`](./python/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/python/README.md) | `pip install github-copilot-sdk` | | **Go** | [`go/`](./go/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/go/README.md) | `go get github.com/github/copilot-sdk/go` | | **.NET** | [`dotnet/`](./dotnet/) | [Cookbook](https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/README.md) | `dotnet add package GitHub.Copilot.SDK` | -| **Java** | [`github/copilot-sdk-java`](https://github.com/github/copilot-sdk-java) | WIP | See instructions for [Maven](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#maven) and [Gradle](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#gradle) | +| **Java** | [`github/copilot-sdk-java`](https://github.com/github/copilot-sdk-java) | WIP | Maven coordinates
`com.github:copilot-sdk-java`
See instructions for [Maven](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#maven) and [Gradle](https://github.com/github/copilot-sdk-java?tab=readme-ov-file#gradle) | See the individual SDK READMEs for installation, usage examples, and API reference. From fb6797949bb6fd6c1c09d1180f0aadb7f0400319 Mon Sep 17 00:00:00 2001 From: Steven Molen Date: Thu, 19 Mar 2026 12:12:00 -0500 Subject: [PATCH 55/61] fix(nodejs): add CJS compatibility for VS Code extensions (#546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(nodejs): add CJS compatibility for VS Code extensions (#528) Replace import.meta.resolve with createRequire + path walking in getBundledCliPath(). The new implementation falls back to __filename when import.meta.url is unavailable (shimmed CJS environments like VS Code extensions bundled with esbuild format:"cjs"). Single ESM build output retained — no dual CJS/ESM builds needed. The fallback logic handles both native ESM and shimmed CJS contexts. * docs(nodejs): note CJS bundle and system-installed CLI requirements * Dual ESM/CJS build for CommonJS compatibility (#528) Produce both ESM and CJS outputs from the esbuild config so that consumers using either module system get a working package automatically. - Add a second esbuild.build() call with format:"cjs" outputting to dist/cjs/ - Write a dist/cjs/package.json with type:"commonjs" so Node treats .js as CJS - Update package.json exports with "import" and "require" conditions for both the main and ./extension entry points - Revert getBundledCliPath() to use import.meta.resolve for ESM, with a createRequire + path-walking fallback for CJS contexts - Update CJS compatibility tests to verify the actual dual build - Update README to document CJS/CommonJS support Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> * ci(nodejs): add build step before tests The CJS compatibility tests verify dist/ output, which requires a build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style(nodejs): fix prettier formatting in changed files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(nodejs): verify CLI path resolution in both ESM and CJS builds Replace the cliUrl-based test (which skipped getBundledCliPath()) with tests that construct CopilotClient without cliUrl, actually exercising the bundled CLI resolution in both module formats. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * build(nodejs): suppress expected empty-import-meta warning in CJS build The CJS build intentionally produces empty import.meta — our runtime code detects this and falls back to createRequire. Silence the esbuild warning to avoid confusing contributors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Steve Sanderson Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/nodejs-sdk-tests.yml | 3 ++ nodejs/README.md | 20 +++++++ nodejs/esbuild-copilotsdk-nodejs.ts | 18 +++++++ nodejs/package.json | 22 ++++++-- nodejs/src/client.ts | 34 +++++++++--- nodejs/test/cjs-compat.test.ts | 72 ++++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 nodejs/test/cjs-compat.test.ts diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 9e978a22fa..9dec016675 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -62,6 +62,9 @@ jobs: - name: Typecheck SDK run: npm run typecheck + - name: Build SDK + run: npm run build + - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts diff --git a/nodejs/README.md b/nodejs/README.md index e9d23c529a..6a9059e20d 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -850,6 +850,26 @@ try { - Node.js >= 18.0.0 - GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`) +### CJS / CommonJS Support + +The SDK ships both ESM and CJS builds. Node.js and bundlers (esbuild, webpack, etc.) automatically select the correct format via the `exports` field in `package.json`: + +- `import` / `from` → ESM (`dist/index.js`) +- `require()` → CJS (`dist/cjs/index.cjs`) + +This means the SDK works out of the box in CJS environments such as VS Code extensions bundled with `esbuild format:"cjs"`. + +### System-installed CLI (winget, brew, apt) + +If you installed the Copilot CLI separately rather than relying on the SDK's bundled copy, pass `cliPath` explicitly: + +```typescript +const client = new CopilotClient({ + cliPath: '/usr/local/bin/copilot', // macOS/Linux + // cliPath: 'C:\\path\\to\\copilot.exe', // Windows (winget, etc.) +}); +``` + ## License MIT diff --git a/nodejs/esbuild-copilotsdk-nodejs.ts b/nodejs/esbuild-copilotsdk-nodejs.ts index 059b8cfa60..f65a47236f 100644 --- a/nodejs/esbuild-copilotsdk-nodejs.ts +++ b/nodejs/esbuild-copilotsdk-nodejs.ts @@ -4,6 +4,7 @@ import { execSync } from "child_process"; const entryPoints = globSync("src/**/*.ts"); +// ESM build await esbuild.build({ entryPoints, outbase: "src", @@ -15,5 +16,22 @@ await esbuild.build({ outExtension: { ".js": ".js" }, }); +// CJS build — uses .js extension with a "type":"commonjs" package.json marker +await esbuild.build({ + entryPoints, + outbase: "src", + outdir: "dist/cjs", + format: "cjs", + platform: "node", + target: "es2022", + sourcemap: false, + outExtension: { ".js": ".js" }, + logOverride: { "empty-import-meta": "silent" }, +}); + +// Mark the CJS directory so Node treats .js files as CommonJS +import { writeFileSync } from "fs"; +writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n"); + // Generate .d.ts files execSync("tsc", { stdio: "inherit" }); diff --git a/nodejs/package.json b/nodejs/package.json index 214ef34666..6b0d30f2cf 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -6,16 +6,28 @@ }, "version": "0.1.8", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", - "main": "./dist/index.js", + "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/cjs/index.js" + } }, "./extension": { - "import": "./dist/extension.js", - "types": "./dist/extension.d.ts" + "import": { + "types": "./dist/extension.d.ts", + "default": "./dist/extension.js" + }, + "require": { + "types": "./dist/extension.d.ts", + "default": "./dist/cjs/extension.js" + } } }, "type": "module", diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index b8e7b31dcc..46d932242a 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -14,6 +14,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; import { Socket } from "node:net"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -91,14 +92,35 @@ function getNodeExecPath(): string { /** * Gets the path to the bundled CLI from the @github/copilot package. * Uses index.js directly rather than npm-loader.js (which spawns the native binary). + * + * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions + * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to + * walking node_modules to find the package. */ function getBundledCliPath(): string { - // Find the actual location of the @github/copilot package by resolving its sdk export - const sdkUrl = import.meta.resolve("@github/copilot/sdk"); - const sdkPath = fileURLToPath(sdkUrl); - // sdkPath is like .../node_modules/@github/copilot/sdk/index.js - // Go up two levels to get the package root, then append index.js - return join(dirname(dirname(sdkPath)), "index.js"); + if (typeof import.meta.resolve === "function") { + // ESM: resolve via import.meta.resolve + const sdkUrl = import.meta.resolve("@github/copilot/sdk"); + const sdkPath = fileURLToPath(sdkUrl); + // sdkPath is like .../node_modules/@github/copilot/sdk/index.js + // Go up two levels to get the package root, then append index.js + return join(dirname(dirname(sdkPath)), "index.js"); + } + + // CJS fallback: the @github/copilot package has ESM-only exports so + // require.resolve cannot reach it. Walk the module search paths instead. + const req = createRequire(__filename); + const searchPaths = req.resolve.paths("@github/copilot") ?? []; + for (const base of searchPaths) { + const candidate = join(base, "@github", "copilot", "index.js"); + if (existsSync(candidate)) { + return candidate; + } + } + throw new Error( + `Could not find @github/copilot package. Searched ${searchPaths.length} paths. ` + + `Ensure it is installed, or pass cliPath/cliUrl to CopilotClient.` + ); } /** diff --git a/nodejs/test/cjs-compat.test.ts b/nodejs/test/cjs-compat.test.ts new file mode 100644 index 0000000000..f574037256 --- /dev/null +++ b/nodejs/test/cjs-compat.test.ts @@ -0,0 +1,72 @@ +/** + * Dual ESM/CJS build compatibility tests + * + * Verifies that both the ESM and CJS builds exist and work correctly, + * so consumers using either module system get a working package. + * + * See: https://github.com/github/copilot-sdk/issues/528 + */ + +import { describe, expect, it } from "vitest"; +import { existsSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; + +const distDir = join(import.meta.dirname, "../dist"); + +describe("Dual ESM/CJS build (#528)", () => { + it("ESM dist file should exist", () => { + expect(existsSync(join(distDir, "index.js"))).toBe(true); + }); + + it("CJS dist file should exist", () => { + expect(existsSync(join(distDir, "cjs/index.js"))).toBe(true); + }); + + it("CJS build is requireable and exports CopilotClient", () => { + const script = ` + const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))}); + if (typeof sdk.CopilotClient !== 'function') { + console.error('CopilotClient is not a function'); + process.exit(1); + } + console.log('CJS require: OK'); + `; + const output = execFileSync(process.execPath, ["--eval", script], { + encoding: "utf-8", + timeout: 10000, + cwd: join(import.meta.dirname, ".."), + }); + expect(output).toContain("CJS require: OK"); + }); + + it("CJS build resolves bundled CLI path", () => { + const script = ` + const sdk = require(${JSON.stringify(join(distDir, "cjs/index.js"))}); + const client = new sdk.CopilotClient({ autoStart: false }); + console.log('CJS CLI resolved: OK'); + `; + const output = execFileSync(process.execPath, ["--eval", script], { + encoding: "utf-8", + timeout: 10000, + cwd: join(import.meta.dirname, ".."), + }); + expect(output).toContain("CJS CLI resolved: OK"); + }); + + it("ESM build resolves bundled CLI path", () => { + const esmPath = join(distDir, "index.js"); + const script = ` + import { pathToFileURL } from 'node:url'; + const sdk = await import(pathToFileURL(${JSON.stringify(esmPath)}).href); + const client = new sdk.CopilotClient({ autoStart: false }); + console.log('ESM CLI resolved: OK'); + `; + const output = execFileSync(process.execPath, ["--input-type=module", "--eval", script], { + encoding: "utf-8", + timeout: 10000, + cwd: join(import.meta.dirname, ".."), + }); + expect(output).toContain("ESM CLI resolved: OK"); + }); +}); From 01208ca3aeec203cf46ff6c5465889f4623167ae Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 19 Mar 2026 17:13:41 +0000 Subject: [PATCH 56/61] Remove unnecessary docs Don't want to imply that using system-installed `copilot` is a good idea as the version can be wrong. --- nodejs/README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index 6a9059e20d..e9d23c529a 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -850,26 +850,6 @@ try { - Node.js >= 18.0.0 - GitHub Copilot CLI installed and in PATH (or provide custom `cliPath`) -### CJS / CommonJS Support - -The SDK ships both ESM and CJS builds. Node.js and bundlers (esbuild, webpack, etc.) automatically select the correct format via the `exports` field in `package.json`: - -- `import` / `from` → ESM (`dist/index.js`) -- `require()` → CJS (`dist/cjs/index.cjs`) - -This means the SDK works out of the box in CJS environments such as VS Code extensions bundled with `esbuild format:"cjs"`. - -### System-installed CLI (winget, brew, apt) - -If you installed the Copilot CLI separately rather than relying on the SDK's bundled copy, pass `cliPath` explicitly: - -```typescript -const client = new CopilotClient({ - cliPath: '/usr/local/bin/copilot', // macOS/Linux - // cliPath: 'C:\\path\\to\\copilot.exe', // Windows (winget, etc.) -}); -``` - ## License MIT From 7390a28d0a354b9f43fcd50955264fe26cb16d1e Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 19 Mar 2026 11:10:44 -0700 Subject: [PATCH 57/61] [python] Refactor `CopilotClient.create_session()` and `resume_session()` to have parameters (#587) * Refactor `CopilotClient.create_session()` to have parameters * Address PR review comments for create_session refactor - Add validation for on_permission_request in create_session to fail fast when handler is missing/invalid - Fix lambda signatures to accept two args (request, invocation) in test scenario and docs - Fix permissionDecision key to use camelCase in pre_tool_use hook Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add test for None permission handler validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Merge with main * Fix test to use SubprocessConfig instead of dict Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Change the call signature of `resume_session()` * Fix formatting * Make on_permission_request and model keyword-only in Python SDK Update create_session() and resume_session() signatures so that on_permission_request and model are keyword-only parameters (after *). Update all call sites across test scenarios, unit tests, E2E tests, samples, and documentation to use keyword argument syntax. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python E2E tests for keyword-only create_session parameters - Restore accidentally deleted custom_agents dict entries in test_agent_and_compact_rpc.py - Convert positional on_permission_request to keyword arg in test_compaction.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix formatting * Format docstrings * Fix a merge mistake --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/auth/byok.md | 15 +- docs/features/custom-agents.md | 19 +- docs/features/hooks.md | 40 +-- docs/features/image-input.md | 8 +- docs/features/mcp.md | 39 +-- docs/features/session-persistence.md | 9 +- docs/features/skills.md | 21 +- docs/features/steering-and-queueing.md | 24 +- docs/getting-started.md | 30 +- docs/hooks/error-handling.md | 6 +- docs/hooks/index.md | 8 +- docs/hooks/post-tool-use.md | 6 +- docs/hooks/pre-tool-use.md | 6 +- docs/hooks/session-lifecycle.md | 14 +- docs/hooks/user-prompt-submitted.md | 6 +- docs/setup/azure-managed-identity.md | 44 ++- docs/setup/backend-services.md | 7 +- docs/setup/bundled-cli.md | 4 +- docs/setup/github-oauth.md | 7 +- docs/setup/local-cli.md | 4 +- python/README.md | 169 +++++---- python/copilot/__init__.py | 4 - python/copilot/client.py | 331 ++++++++++-------- python/copilot/session.py | 6 +- python/copilot/types.py | 108 +----- python/e2e/test_agent_and_compact_rpc.py | 96 +++-- python/e2e/test_ask_user.py | 18 +- python/e2e/test_client.py | 6 +- python/e2e/test_compaction.py | 24 +- python/e2e/test_hooks.py | 30 +- python/e2e/test_mcp_and_agents.py | 30 +- python/e2e/test_multi_client.py | 38 +- python/e2e/test_permissions.py | 22 +- python/e2e/test_rpc.py | 10 +- python/e2e/test_session.py | 143 ++++---- python/e2e/test_skills.py | 21 +- python/e2e/test_streaming_fidelity.py | 9 +- python/e2e/test_tools.py | 22 +- python/pyproject.toml | 1 + python/samples/chat.py | 6 +- python/test_client.py | 60 ++-- .../auth/byok-anthropic/python/main.py | 15 +- test/scenarios/auth/byok-azure/python/main.py | 15 +- .../scenarios/auth/byok-ollama/python/main.py | 15 +- .../scenarios/auth/byok-openai/python/main.py | 11 +- test/scenarios/auth/gh-app/python/main.py | 4 +- .../app-backend-to-server/python/main.py | 4 +- .../bundling/app-direct-server/python/main.py | 4 +- .../bundling/container-proxy/python/main.py | 4 +- .../bundling/fully-bundled/python/main.py | 4 +- test/scenarios/callbacks/hooks/python/main.py | 24 +- .../callbacks/permissions/python/main.py | 10 +- .../callbacks/user-input/python/main.py | 12 +- test/scenarios/modes/default/python/main.py | 6 +- test/scenarios/modes/minimal/python/main.py | 13 +- .../prompts/attachments/python/main.py | 11 +- .../prompts/reasoning-effort/python/main.py | 15 +- .../prompts/system-message/python/main.py | 11 +- .../concurrent-sessions/python/main.py | 20 +- .../sessions/infinite-sessions/python/main.py | 15 +- .../sessions/session-resume/python/main.py | 11 +- .../sessions/streaming/python/main.py | 9 +- .../tools/custom-agents/python/main.py | 25 +- .../tools/mcp-servers/python/main.py | 11 +- test/scenarios/tools/no-tools/python/main.py | 11 +- test/scenarios/tools/skills/python/main.py | 14 +- .../tools/tool-filtering/python/main.py | 11 +- .../tools/tool-overrides/python/main.py | 6 +- .../tools/virtual-filesystem/python/main.py | 12 +- .../transport/reconnect/python/main.py | 6 +- test/scenarios/transport/stdio/python/main.py | 4 +- test/scenarios/transport/tcp/python/main.py | 4 +- 72 files changed, 825 insertions(+), 983 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index df334508d4..8d96502802 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -23,7 +23,7 @@ Azure AI Foundry (formerly Azure OpenAI) is a common BYOK deployment target for ```python import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler FOUNDRY_MODEL_URL = "https://your-resource.openai.azure.com/openai/v1/" # Set FOUNDRY_API_KEY environment variable @@ -32,14 +32,11 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5.2-codex", # Your deployment name - "provider": { - "type": "openai", - "base_url": FOUNDRY_MODEL_URL, - "wire_api": "responses", # Use "completions" for older models - "api_key": os.environ["FOUNDRY_API_KEY"], - }, + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", provider={ + "type": "openai", + "base_url": FOUNDRY_MODEL_URL, + "wire_api": "responses", # Use "completions" for older models + "api_key": os.environ["FOUNDRY_API_KEY"], }) done = asyncio.Event() diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index f9c1a37340..47712d9cf0 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -70,9 +70,10 @@ from copilot.types import PermissionRequestResult client = CopilotClient() await client.start() -session = await client.create_session({ - "model": "gpt-4.1", - "custom_agents": [ +session = await client.create_session( + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", + custom_agents=[ { "name": "researcher", "display_name": "Research Agent", @@ -88,8 +89,7 @@ session = await client.create_session({ "prompt": "You are a code editor. Make minimal, surgical changes to files as requested.", }, ], - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), -}) +) ``` @@ -258,8 +258,9 @@ const session = await client.createSession({ ```python -session = await client.create_session({ - "custom_agents": [ +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ { "name": "researcher", "prompt": "You are a research assistant. Analyze code and answer questions.", @@ -269,8 +270,8 @@ session = await client.create_session({ "prompt": "You are a code editor. Make minimal, surgical changes.", }, ], - "agent": "researcher", # Pre-select the researcher agent -}) + agent="researcher", # Pre-select the researcher agent +) ``` diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 5c6c2f2c51..1a01c5f1a4 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -65,15 +65,15 @@ from copilot import CopilotClient client = CopilotClient() await client.start() -session = await client.create_session({ - "hooks": { +session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + hooks={ "on_session_start": on_session_start, "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, # ... add only the hooks you need }, - "on_permission_request": lambda req, inv: {"kind": "approved"}, -}) +) ``` @@ -245,10 +245,10 @@ async def on_pre_tool_use(input_data, invocation): } return {"permissionDecision": "allow"} -session = await client.create_session({ - "hooks": {"on_pre_tool_use": on_pre_tool_use}, - "on_permission_request": lambda req, inv: {"kind": "approved"}, -}) +session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + hooks={"on_pre_tool_use": on_pre_tool_use}, +) ``` @@ -567,16 +567,16 @@ async def on_session_end(input_data, invocation): await f.write(json.dumps(audit_log, indent=2)) return None -session = await client.create_session({ - "hooks": { +session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_session_end": on_session_end, }, - "on_permission_request": lambda req, inv: {"kind": "approved"}, -}) +) ``` @@ -666,13 +666,13 @@ async def on_error_occurred(input_data, invocation): ]) return None -session = await client.create_session({ - "hooks": { +session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + hooks={ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, }, - "on_permission_request": lambda req, inv: {"kind": "approved"}, -}) +) ``` @@ -905,15 +905,15 @@ async def on_session_end(input_data, invocation): ) return None -session = await client.create_session({ - "hooks": { +session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + hooks={ "on_session_start": on_session_start, "on_user_prompt_submitted": on_user_prompt_submitted, "on_pre_tool_use": on_pre_tool_use, "on_session_end": on_session_end, }, - "on_permission_request": lambda req, inv: {"kind": "approved"}, -}) +) ``` diff --git a/docs/features/image-input.md b/docs/features/image-input.md index 1a3bde0a23..acec80d4ae 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -74,10 +74,10 @@ from copilot.types import PermissionRequestResult client = CopilotClient() await client.start() -session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), -}) +session = await client.create_session( + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", +) await session.send( "Describe what you see in this image", diff --git a/docs/features/mcp.md b/docs/features/mcp.md index f1ad381871..62465c0bd7 100644 --- a/docs/features/mcp.md +++ b/docs/features/mcp.md @@ -59,32 +59,29 @@ const session = await client.createSession({ ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5", - "mcp_servers": { - # Local MCP server (stdio) - "my-local-server": { - "type": "local", - "command": "python", - "args": ["./mcp_server.py"], - "env": {"DEBUG": "true"}, - "cwd": "./servers", - "tools": ["*"], - "timeout": 30000, - }, - # Remote MCP server (HTTP) - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/", - "headers": {"Authorization": "Bearer ${TOKEN}"}, - "tools": ["*"], - }, + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5", mcp_servers={ + # Local MCP server (stdio) + "my-local-server": { + "type": "local", + "command": "python", + "args": ["./mcp_server.py"], + "env": {"DEBUG": "true"}, + "cwd": "./servers", + "tools": ["*"], + "timeout": 30000, + }, + # Remote MCP server (HTTP) + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": {"Authorization": "Bearer ${TOKEN}"}, + "tools": ["*"], }, }) diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 59a5d9d505..3b0e9f69b9 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -46,16 +46,13 @@ await session.sendAndWait({ prompt: "Analyze my codebase" }); ### Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() # Create a session with a meaningful ID -session = await client.create_session({ - "session_id": "user-123-task-456", - "model": "gpt-5.2-codex", -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5.2-codex", session_id="user-123-task-456") # Do some work... await session.send_and_wait({"prompt": "Analyze my codebase"}) @@ -160,7 +157,7 @@ await session.sendAndWait({ prompt: "What did we discuss earlier?" }); ```python # Resume from a different client instance (or after restart) -session = await client.resume_session("user-123-task-456") +session = await client.resume_session("user-123-task-456", on_permission_request=PermissionHandler.approve_all) # Continue where you left off await session.send_and_wait({"prompt": "What did we discuss earlier?"}) diff --git a/docs/features/skills.md b/docs/features/skills.md index 1d584ced18..466c637ffe 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -49,14 +49,14 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "skill_directories": [ + session = await client.create_session( + on_permission_request=lambda req, inv: {"kind": "approved"}, + model="gpt-4.1", + skill_directories=[ "./skills/code-review", "./skills/documentation", ], - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), - }) + ) # Copilot now has access to skills in those directories await session.send_and_wait({"prompt": "Review this code for security issues"}) @@ -160,10 +160,13 @@ const session = await client.createSession({ Python ```python -session = await client.create_session({ - "skill_directories": ["./skills"], - "disabled_skills": ["experimental-feature", "deprecated-tool"], -}) +from copilot import PermissionHandler + +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + skill_directories=["./skills"], + disabled_skills=["experimental-feature", "deprecated-tool"], +) ``` diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index ad27c4ee06..7da349e1c2 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -76,10 +76,10 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), - }) + session = await client.create_session( + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", + ) # Start a long-running task msg_id = await session.send({ @@ -235,10 +235,10 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), - }) + session = await client.create_session( + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", + ) # Send an initial task await session.send({"prompt": "Set up the project structure"}) @@ -431,10 +431,10 @@ await session.send({ Python ```python -session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": lambda req, inv: PermissionRequestResult(kind="approved"), -}) +session = await client.create_session( + on_permission_request=lambda req, inv: PermissionRequestResult(kind="approved"), + model="gpt-4.1", +) # Start a task await session.send({"prompt": "Refactor the database layer"}) diff --git a/docs/getting-started.md b/docs/getting-started.md index 15f11e8b77..6c0aee72e3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -135,10 +135,8 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": PermissionHandler.approve_all, - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") + response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) response = await session.send_and_wait({"prompt": "What is 2 + 2?"}) print(response.data.content) @@ -284,11 +282,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": PermissionHandler.approve_all, - "streaming": True, - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True) # Listen for response chunks def handle_event(event): @@ -437,7 +431,7 @@ from copilot.generated.session_events import SessionEvent, SessionEventType client = CopilotClient() -session = client.create_session({"on_permission_request": lambda req, inv: {"kind": "approved"}}) +session = client.create_session(on_permission_request=lambda req, inv: {"kind": "approved"}) # Subscribe to all events unsubscribe = session.on(lambda event: print(f"Event: {event.type}")) @@ -680,12 +674,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": PermissionHandler.approve_all, - "streaming": True, - "tools": [get_weather], - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -950,12 +939,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-4.1", - "on_permission_request": PermissionHandler.approve_all, - "streaming": True, - "tools": [get_weather], - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", streaming=True, tools=[get_weather]) def handle_event(event): if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA: @@ -1314,7 +1298,7 @@ client = CopilotClient({ await client.start() # Use the client normally -session = await client.create_session({"on_permission_request": PermissionHandler.approve_all}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all) # ... ``` diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md index 2e7848bc5e..a67906ac94 100644 --- a/docs/hooks/error-handling.md +++ b/docs/hooks/error-handling.md @@ -146,15 +146,15 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_error_occurred(input_data, invocation): print(f"[{invocation['session_id']}] Error: {input_data['error']}") print(f" Context: {input_data['errorContext']}") print(f" Recoverable: {input_data['recoverable']}") return None -session = await client.create_session({ - "hooks": {"on_error_occurred": on_error_occurred} -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_error_occurred": on_error_occurred}) ``` diff --git a/docs/hooks/index.md b/docs/hooks/index.md index b09701066b..d83b11b2f9 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -53,7 +53,7 @@ const session = await client.createSession({ Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() @@ -70,13 +70,11 @@ async def main(): async def on_session_start(input_data, invocation): return {"additionalContext": "User prefers concise answers."} - session = await client.create_session({ - "hooks": { + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_session_start": on_session_start, - } - }) + }) ``` diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md index 415acce9e6..029e9eb2f7 100644 --- a/docs/hooks/post-tool-use.md +++ b/docs/hooks/post-tool-use.md @@ -145,15 +145,15 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_post_tool_use(input_data, invocation): print(f"[{invocation['session_id']}] Tool: {input_data['toolName']}") print(f" Args: {input_data['toolArgs']}") print(f" Result: {input_data['toolResult']}") return None # Pass through unchanged -session = await client.create_session({ - "hooks": {"on_post_tool_use": on_post_tool_use} -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_post_tool_use": on_post_tool_use}) ``` diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md index df194aaf3e..e1bb974951 100644 --- a/docs/hooks/pre-tool-use.md +++ b/docs/hooks/pre-tool-use.md @@ -153,14 +153,14 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_pre_tool_use(input_data, invocation): print(f"[{invocation['session_id']}] Calling {input_data['toolName']}") print(f" Args: {input_data['toolArgs']}") return {"permissionDecision": "allow"} -session = await client.create_session({ - "hooks": {"on_pre_tool_use": on_pre_tool_use} -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_pre_tool_use": on_pre_tool_use}) ``` diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 93696530e7..4efd33ccc0 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -152,6 +152,8 @@ Package manager: ${projectInfo.packageManager} Python ```python +from copilot import PermissionHandler + async def on_session_start(input_data, invocation): print(f"Session {invocation['session_id']} started ({input_data['source']})") @@ -165,9 +167,7 @@ Package manager: {project_info['packageManager']} """.strip() } -session = await client.create_session({ - "hooks": {"on_session_start": on_session_start} -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_session_start": on_session_start}) ``` @@ -371,6 +371,8 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + session_start_times = {} async def on_session_start(input_data, invocation): @@ -390,12 +392,10 @@ async def on_session_end(input_data, invocation): session_start_times.pop(invocation["session_id"], None) return None -session = await client.create_session({ - "hooks": { +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={ "on_session_start": on_session_start, "on_session_end": on_session_end, - } -}) + }) ``` diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md index 370c37b8c5..2aca7f1cec 100644 --- a/docs/hooks/user-prompt-submitted.md +++ b/docs/hooks/user-prompt-submitted.md @@ -141,13 +141,13 @@ const session = await client.createSession({ Python ```python +from copilot import PermissionHandler + async def on_user_prompt_submitted(input_data, invocation): print(f"[{invocation['session_id']}] User: {input_data['prompt']}") return None -session = await client.create_session({ - "hooks": {"on_user_prompt_submitted": on_user_prompt_submitted} -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, hooks={"on_user_prompt_submitted": on_user_prompt_submitted}) ``` diff --git a/docs/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md index b2fa152645..40d87c5ba1 100644 --- a/docs/setup/azure-managed-identity.md +++ b/docs/setup/azure-managed-identity.md @@ -42,7 +42,7 @@ import asyncio import os from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, ProviderConfig, SessionConfig +from copilot import CopilotClient, PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" @@ -58,15 +58,14 @@ async def main(): await client.start() session = await client.create_session( - SessionConfig( - model="gpt-4.1", - provider=ProviderConfig( - type="openai", - base_url=f"{foundry_url.rstrip('/')}/openai/v1/", - bearer_token=token, # Short-lived bearer token - wire_api="responses", - ), - ) + on_permission_request=PermissionHandler.approve_all, + model="gpt-4.1", + provider={ + "type": "openai", + "base_url": f"{foundry_url.rstrip('/')}/openai/v1/", + "bearer_token": token, # Short-lived bearer token + "wire_api": "responses", + }, ) response = await session.send_and_wait({"prompt": "Hello from Managed Identity!"}) @@ -84,7 +83,7 @@ Bearer tokens expire (typically after ~1 hour). For servers or long-running agen ```python from azure.identity import DefaultAzureCredential -from copilot import CopilotClient, ProviderConfig, SessionConfig +from copilot import CopilotClient, PermissionHandler COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default" @@ -98,24 +97,21 @@ class ManagedIdentityCopilotAgent: self.credential = DefaultAzureCredential() self.client = CopilotClient() - def _get_session_config(self) -> SessionConfig: - """Build a SessionConfig with a fresh bearer token.""" + def _get_provider_config(self) -> dict: + """Build a provider config dict with a fresh bearer token.""" token = self.credential.get_token(COGNITIVE_SERVICES_SCOPE).token - return SessionConfig( - model=self.model, - provider=ProviderConfig( - type="openai", - base_url=f"{self.foundry_url}/openai/v1/", - bearer_token=token, - wire_api="responses", - ), - ) + return { + "type": "openai", + "base_url": f"{self.foundry_url}/openai/v1/", + "bearer_token": token, + "wire_api": "responses", + } async def chat(self, prompt: str) -> str: """Send a prompt and return the response text.""" # Fresh token for each session - config = self._get_session_config() - session = await self.client.create_session(config) + provider = self._get_provider_config() + session = await self.client.create_session(on_permission_request=PermissionHandler.approve_all, model=self.model, provider=provider) response = await session.send_and_wait({"prompt": prompt}) await session.disconnect() diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index a7bc6c8c96..735adf4ff3 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -111,17 +111,14 @@ res.json({ content: response?.data.content }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient({ "cli_url": "localhost:4321", }) await client.start() -session = await client.create_session({ - "session_id": f"user-{user_id}-{int(time.time())}", - "model": "gpt-4.1", -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-{int(time.time())}") response = await session.send_and_wait({"prompt": message}) ``` diff --git a/docs/setup/bundled-cli.md b/docs/setup/bundled-cli.md index 04df0286f1..cdfe6df812 100644 --- a/docs/setup/bundled-cli.md +++ b/docs/setup/bundled-cli.md @@ -85,7 +85,7 @@ await client.stop(); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from pathlib import Path client = CopilotClient({ @@ -93,7 +93,7 @@ client = CopilotClient({ }) await client.start() -session = await client.create_session({"model": "gpt-4.1"}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/docs/setup/github-oauth.md b/docs/setup/github-oauth.md index e7b1c634a5..81d2b25a27 100644 --- a/docs/setup/github-oauth.md +++ b/docs/setup/github-oauth.md @@ -145,7 +145,7 @@ const response = await session.sendAndWait({ prompt: "Hello!" }); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler def create_client_for_user(user_token: str) -> CopilotClient: return CopilotClient({ @@ -157,10 +157,7 @@ def create_client_for_user(user_token: str) -> CopilotClient: client = create_client_for_user("gho_user_access_token") await client.start() -session = await client.create_session({ - "session_id": f"user-{user_id}-session", - "model": "gpt-4.1", -}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1", session_id=f"user-{user_id}-session") response = await session.send_and_wait({"prompt": "Hello!"}) ``` diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index b78e294f28..bb95a4d38c 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -51,12 +51,12 @@ await client.stop(); Python ```python -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler client = CopilotClient() await client.start() -session = await client.create_session({"model": "gpt-4.1"}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-4.1") response = await session.send_and_wait({"prompt": "Hello!"}) print(response.data.content) diff --git a/python/README.md b/python/README.md index 582031d402..2394c351ab 100644 --- a/python/README.md +++ b/python/README.md @@ -33,10 +33,7 @@ async def main(): await client.start() # Create a session (on_permission_request is required) - session = await client.create_session({ - "model": "gpt-5", - "on_permission_request": PermissionHandler.approve_all, - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") # Wait for response using session.idle event done = asyncio.Event() @@ -63,10 +60,7 @@ asyncio.run(main()) Sessions also support the `async with` context manager pattern for automatic cleanup: ```python -async with await client.create_session({ - "model": "gpt-5", - "on_permission_request": PermissionHandler.approve_all, -}) as session: +async with await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") as session: await session.send("What is 2+2?") # session is automatically disconnected when leaving the block ``` @@ -91,7 +85,7 @@ from copilot import CopilotClient, SubprocessConfig client = CopilotClient() # uses bundled CLI, stdio transport await client.start() -session = await client.create_session({"model": "gpt-5"}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") def on_event(event): print(f"Event: {event['type']}") @@ -140,19 +134,59 @@ CopilotClient( - `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). -**SessionConfig Options (for `create_session`):** +**`create_session` Parameters:** + +All parameters are keyword-only: -- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** -- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option. -- `session_id` (str): Custom session ID -- `tools` (list): Custom tools exposed to the CLI -- `system_message` (dict): System message configuration -- `streaming` (bool): Enable streaming delta events -- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. -- `infinite_sessions` (dict): Automatic context compaction configuration - `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. +- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). +- `session_id` (str): Custom session ID for resuming or identifying sessions. +- `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests. +- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option. +- `tools` (list): Custom tools exposed to the CLI. +- `system_message` (dict): System message configuration. +- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`. +- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. - `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section. +- `working_directory` (str): Working directory for the session. Tool operations will be relative to this directory. +- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. +- `streaming` (bool): Enable streaming delta events. +- `mcp_servers` (dict): MCP server configurations for the session. +- `custom_agents` (list): Custom agent configurations for the session. +- `config_dir` (str): Override the default configuration directory location. +- `skill_directories` (list[str]): Directories to load skills from. +- `disabled_skills` (list[str]): List of skill names to disable. +- `infinite_sessions` (dict): Automatic context compaction configuration. + +**`resume_session` Parameters:** + +- `session_id` (str): **Required.** The ID of the session to resume. + +The parameters below are keyword-only: + +- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section. +- `model` (str): Model to use (can change the model when resuming). +- `client_name` (str): Client name to identify the application using the SDK. +- `reasoning_effort` (str): Reasoning effort level ("low", "medium", "high", "xhigh"). +- `tools` (list): Custom tools exposed to the CLI. +- `system_message` (dict): System message configuration. +- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`. +- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set. +- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). +- `hooks` (dict): Hook handlers for session lifecycle events. +- `working_directory` (str): Working directory for the session. +- `provider` (dict): Custom API provider configuration (BYOK). +- `streaming` (bool): Enable streaming delta events. +- `mcp_servers` (dict): MCP server configurations for the session. +- `custom_agents` (list): Custom agent configurations for the session. +- `agent` (str): Name of the custom agent to activate when the session starts. +- `config_dir` (str): Override the default configuration directory location. +- `skill_directories` (list[str]): Directories to load skills from. +- `disabled_skills` (list[str]): List of skill names to disable. +- `infinite_sessions` (dict): Automatic context compaction configuration. +- `disable_resume` (bool): Skip emitting the session.resume event (default: False). +- `on_event` (callable): Event handler registered before the session.resume RPC. **Session Lifecycle Methods:** @@ -189,7 +223,7 @@ Define tools with automatic JSON schema generation using the `@define_tool` deco ```python from pydantic import BaseModel, Field -from copilot import CopilotClient, define_tool +from copilot import CopilotClient, define_tool, PermissionHandler class LookupIssueParams(BaseModel): id: str = Field(description="Issue identifier") @@ -199,10 +233,11 @@ async def lookup_issue(params: LookupIssueParams) -> str: issue = await fetch_issue(params.id) return issue.summary -session = await client.create_session({ - "model": "gpt-5", - "tools": [lookup_issue], -}) +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + tools=[lookup_issue], +) ``` > **Note:** When using `from __future__ import annotations`, define Pydantic models at module level (not inside functions). @@ -212,7 +247,7 @@ session = await client.create_session({ For users who prefer manual schema definition: ```python -from copilot import CopilotClient, Tool +from copilot import CopilotClient, Tool, PermissionHandler async def lookup_issue(invocation): issue_id = invocation["arguments"]["id"] @@ -223,9 +258,10 @@ async def lookup_issue(invocation): "sessionLog": f"Fetched issue {issue_id}", } -session = await client.create_session({ - "model": "gpt-5", - "tools": [ +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + tools=[ Tool( name="lookup_issue", description="Fetch issue details from our tracker", @@ -239,7 +275,7 @@ session = await client.create_session({ handler=lookup_issue, ) ], -}) +) ``` The SDK automatically handles `tool.call`, executes your handler (sync or async), and responds with the final result when the tool completes. @@ -309,16 +345,17 @@ Enable streaming to receive assistant response chunks as they're generated: ```python import asyncio -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler async def main(): client = CopilotClient() await client.start() - session = await client.create_session({ - "model": "gpt-5", - "streaming": True - }) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + streaming=True, + ) # Use asyncio.Event to wait for completion done = asyncio.Event() @@ -369,27 +406,29 @@ By default, sessions use **infinite sessions** which automatically manage contex ```python # Default: infinite sessions enabled with default thresholds -session = await client.create_session({"model": "gpt-5"}) +session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") # Access the workspace path for checkpoints and files print(session.workspace_path) # => ~/.copilot/session-state/{session_id}/ # Custom thresholds -session = await client.create_session({ - "model": "gpt-5", - "infinite_sessions": { +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, # Start compacting at 80% context usage "buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes }, -}) +) # Disable infinite sessions -session = await client.create_session({ - "model": "gpt-5", - "infinite_sessions": {"enabled": False}, -}) +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + infinite_sessions={"enabled": False}, +) ``` When enabled, sessions emit compaction events: @@ -413,14 +452,15 @@ The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own K **Example with Ollama:** ```python -session = await client.create_session({ - "model": "deepseek-coder-v2:16b", # Required when using custom provider - "provider": { +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="deepseek-coder-v2:16b", # Model to use with the custom provider + provider={ "type": "openai", "base_url": "http://localhost:11434/v1", # Ollama endpoint # api_key not required for Ollama }, -}) +) await session.send("Hello!") ``` @@ -430,14 +470,15 @@ await session.send("Hello!") ```python import os -session = await client.create_session({ - "model": "gpt-4", - "provider": { +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-4", + provider={ "type": "openai", "base_url": "https://my-api.example.com/v1", "api_key": os.environ["MY_API_KEY"], }, -}) +) ``` **Example with Azure OpenAI:** @@ -445,9 +486,10 @@ session = await client.create_session({ ```python import os -session = await client.create_session({ - "model": "gpt-4", - "provider": { +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-4", + provider={ "type": "azure", # Must be "azure" for Azure endpoints, NOT "openai" "base_url": "https://my-resource.openai.azure.com", # Just the host, no path "api_key": os.environ["AZURE_OPENAI_KEY"], @@ -455,11 +497,10 @@ session = await client.create_session({ "api_version": "2024-10-21", }, }, -}) +) ``` > **Important notes:** -> - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified. > - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`. > - The `base_url` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically. @@ -594,10 +635,11 @@ async def handle_user_input(request, invocation): "wasFreeform": True, # Whether the answer was freeform (not from choices) } -session = await client.create_session({ - "model": "gpt-5", - "on_user_input_request": handle_user_input, -}) +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + on_user_input_request=handle_user_input, +) ``` ## Session Hooks @@ -641,9 +683,10 @@ async def on_error_occurred(input, invocation): "errorHandling": "retry", # "retry", "skip", or "abort" } -session = await client.create_session({ - "model": "gpt-5", - "hooks": { +session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="gpt-5", + hooks={ "on_pre_tool_use": on_pre_tool_use, "on_post_tool_use": on_post_tool_use, "on_user_prompt_submitted": on_user_prompt_submitted, @@ -651,7 +694,7 @@ session = await client.create_session({ "on_session_end": on_session_end, "on_error_occurred": on_error_occurred, }, -}) +) ``` **Available hooks:** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 03f8e89b74..e1fdf92538 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -30,9 +30,7 @@ PermissionRequestResult, PingResponse, ProviderConfig, - ResumeSessionConfig, SelectionAttachment, - SessionConfig, SessionContext, SessionEvent, SessionListFilter, @@ -73,9 +71,7 @@ "PermissionRequestResult", "PingResponse", "ProviderConfig", - "ResumeSessionConfig", "SelectionAttachment", - "SessionConfig", "SessionContext", "SessionEvent", "SessionListFilter", diff --git a/python/copilot/client.py b/python/copilot/client.py index 81c1459f21..28050088e6 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -5,10 +5,12 @@ to the Copilot CLI server and provides session management capabilities. Example: - >>> from copilot import CopilotClient + >>> from copilot import CopilotClient, PermissionHandler >>> >>> async with CopilotClient() as client: - ... session = await client.create_session() + ... session = await client.create_session( + ... on_permission_request=PermissionHandler.approve_all + ... ) ... await session.send("Hello!") """ @@ -37,11 +39,14 @@ ExternalServerConfig, GetAuthStatusResponse, GetStatusResponse, + InfiniteSessionConfig, + MCPServerConfig, ModelInfo, PingResponse, ProviderConfig, - ResumeSessionConfig, - SessionConfig, + ReasoningEffort, + SessionEvent, + SessionHooks, SessionLifecycleEvent, SessionLifecycleEventType, SessionLifecycleHandler, @@ -49,8 +54,12 @@ SessionMetadata, StopError, SubprocessConfig, + SystemMessageConfig, + Tool, ToolInvocation, ToolResult, + UserInputHandler, + _PermissionHandlerFn, ) HandlerUnsubcribe = Callable[[], None] @@ -101,10 +110,10 @@ class CopilotClient: >>> await client.start() >>> >>> # Create a session and send a message - >>> session = await client.create_session({ - ... "on_permission_request": PermissionHandler.approve_all, - ... "model": "gpt-4", - ... }) + >>> session = await client.create_session( + ... PermissionHandler.approve_all, + ... "gpt-4", + ... ) >>> session.on(lambda event: print(event.type)) >>> await session.send("Hello!") >>> @@ -143,10 +152,12 @@ def __init__( >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) >>> >>> # Custom CLI path with specific log level - >>> client = CopilotClient(SubprocessConfig( - ... cli_path="/usr/local/bin/copilot", - ... log_level="debug", - ... )) + >>> client = CopilotClient( + ... SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... log_level="debug", + ... ) + ... ) """ if config is None: config = SubprocessConfig() @@ -424,7 +435,32 @@ async def force_stop(self) -> None: if not self._is_external_server: self._actual_port = None - async def create_session(self, config: SessionConfig) -> CopilotSession: + async def create_session( + self, + *, + on_permission_request: _PermissionHandlerFn, + model: str | None = None, + session_id: str | None = None, + client_name: str | None = None, + reasoning_effort: ReasoningEffort | None = None, + tools: list[Tool] | None = None, + system_message: SystemMessageConfig | None = None, + available_tools: list[str] | None = None, + excluded_tools: list[str] | None = None, + on_user_input_request: UserInputHandler | None = None, + hooks: SessionHooks | None = None, + working_directory: str | None = None, + provider: ProviderConfig | None = None, + streaming: bool | None = None, + mcp_servers: dict[str, MCPServerConfig] | None = None, + custom_agents: list[CustomAgentConfig] | None = None, + agent: str | None = None, + config_dir: str | None = None, + skill_directories: list[str] | None = None, + disabled_skills: list[str] | None = None, + infinite_sessions: InfiniteSessionConfig | None = None, + on_event: Callable[[SessionEvent], None] | None = None, + ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -433,8 +469,29 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: automatically start the connection. Args: - config: Optional configuration for the session, including model selection, - custom tools, system messages, and more. + on_permission_request: Handler for permission requests from the server. + model: Model to use for this session. + session_id: Custom session ID. + client_name: Client name to identify the application using the SDK. + reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh"). + tools: Custom tools exposed to the CLI. + system_message: System message configuration. + available_tools: List of tool names to allow (takes precedence over excluded_tools). + excluded_tools: List of tool names to disable (ignored if available_tools is set). + on_user_input_request: Handler for user input requests (enables ask_user tool). + hooks: Hook handlers for intercepting session lifecycle events. + working_directory: Working directory for the session. + provider: Custom provider configuration (BYOK - Bring Your Own Key). + streaming: Enable streaming of assistant message and reasoning chunks. + mcp_servers: MCP server configurations for the session. + custom_agents: Custom agent configurations for the session. + agent: Name of the custom agent to activate when the session starts. + config_dir: Override the default configuration directory location. + skill_directories: Directories to load skills from. + disabled_skills: List of skill names to disable. + infinite_sessions: Infinite session configuration for persistent workspaces. + on_event: Event handler registered before the session.create RPC, ensuring + early events (e.g. session.start) are not missed. Returns: A :class:`CopilotSession` instance for the new session. @@ -443,34 +500,30 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: RuntimeError: If the client is not connected and auto_start is disabled. Example: - >>> # Basic session - >>> config = {"on_permission_request": PermissionHandler.approve_all} - >>> session = await client.create_session(config) + >>> session = await client.create_session( + ... on_permission_request=PermissionHandler.approve_all, + ... ) >>> >>> # Session with model and streaming - >>> session = await client.create_session({ - ... "on_permission_request": PermissionHandler.approve_all, - ... "model": "gpt-4", - ... "streaming": True - ... }) + >>> session = await client.create_session( + ... on_permission_request=PermissionHandler.approve_all, + ... model="gpt-4", + ... streaming=True, + ... ) """ + if not on_permission_request or not callable(on_permission_request): + raise ValueError( + "A valid on_permission_request handler is required. " + "Use PermissionHandler.approve_all or provide a custom handler." + ) + if not self._client: if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config - - if not cfg.get("on_permission_request"): - raise ValueError( - "An on_permission_request handler is required when creating a session. " - "For example, to allow all permissions, use " - '{"on_permission_request": PermissionHandler.approve_all}.' - ) - tool_defs = [] - tools = cfg.get("tools") if tools: for tool in tools: definition: dict[str, Any] = { @@ -486,92 +539,61 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: tool_defs.append(definition) payload: dict[str, Any] = {} - if cfg.get("model"): - payload["model"] = cfg["model"] - if cfg.get("client_name"): - payload["clientName"] = cfg["client_name"] - if cfg.get("reasoning_effort"): - payload["reasoningEffort"] = cfg["reasoning_effort"] + if model: + payload["model"] = model + if client_name: + payload["clientName"] = client_name + if reasoning_effort: + payload["reasoningEffort"] = reasoning_effort if tool_defs: payload["tools"] = tool_defs - # Add system message configuration if provided - system_message = cfg.get("system_message") if system_message: payload["systemMessage"] = system_message - # Add tool filtering options - available_tools = cfg.get("available_tools") if available_tools is not None: payload["availableTools"] = available_tools - excluded_tools = cfg.get("excluded_tools") if excluded_tools is not None: payload["excludedTools"] = excluded_tools - # Always enable permission request callback (deny by default if no handler provided) - on_permission_request = cfg.get("on_permission_request") payload["requestPermission"] = True - # Enable user input request callback if handler provided - on_user_input_request = cfg.get("on_user_input_request") if on_user_input_request: payload["requestUserInput"] = True - # Enable hooks callback if any hook handler provided - hooks = cfg.get("hooks") if hooks and any(hooks.values()): payload["hooks"] = True - # Add working directory if provided - working_directory = cfg.get("working_directory") if working_directory: payload["workingDirectory"] = working_directory - # Add streaming option if provided - streaming = cfg.get("streaming") if streaming is not None: payload["streaming"] = streaming - # Add provider configuration if provided - provider = cfg.get("provider") if provider: payload["provider"] = self._convert_provider_to_wire_format(provider) - # Add MCP servers configuration if provided - mcp_servers = cfg.get("mcp_servers") if mcp_servers: payload["mcpServers"] = mcp_servers payload["envValueMode"] = "direct" - # Add custom agents configuration if provided - custom_agents = cfg.get("custom_agents") if custom_agents: payload["customAgents"] = [ - self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents + self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents ] - # Add agent selection if provided - agent = cfg.get("agent") if agent: payload["agent"] = agent - # Add config directory override if provided - config_dir = cfg.get("config_dir") if config_dir: payload["configDir"] = config_dir - # Add skill directories configuration if provided - skill_directories = cfg.get("skill_directories") if skill_directories: payload["skillDirectories"] = skill_directories - # Add disabled skills configuration if provided - disabled_skills = cfg.get("disabled_skills") if disabled_skills: payload["disabledSkills"] = disabled_skills - # Add infinite sessions configuration if provided - infinite_sessions = cfg.get("infinite_sessions") if infinite_sessions: wire_config: dict[str, Any] = {} if "enabled" in infinite_sessions: @@ -589,7 +611,7 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: if not self._client: raise RuntimeError("Client not connected") - session_id = cfg.get("session_id") or str(uuid.uuid4()) + session_id = session_id or str(uuid.uuid4()) payload["sessionId"] = session_id # Propagate W3C Trace Context to CLI if OpenTelemetry is active @@ -605,7 +627,6 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) - on_event = cfg.get("on_event") if on_event: session.on(on_event) with self._sessions_lock: @@ -621,7 +642,33 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: return session - async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession: + async def resume_session( + self, + session_id: str, + *, + on_permission_request: _PermissionHandlerFn, + model: str | None = None, + client_name: str | None = None, + reasoning_effort: ReasoningEffort | None = None, + tools: list[Tool] | None = None, + system_message: SystemMessageConfig | None = None, + available_tools: list[str] | None = None, + excluded_tools: list[str] | None = None, + on_user_input_request: UserInputHandler | None = None, + hooks: SessionHooks | None = None, + working_directory: str | None = None, + provider: ProviderConfig | None = None, + streaming: bool | None = None, + mcp_servers: dict[str, MCPServerConfig] | None = None, + custom_agents: list[CustomAgentConfig] | None = None, + agent: str | None = None, + config_dir: str | None = None, + skill_directories: list[str] | None = None, + disabled_skills: list[str] | None = None, + infinite_sessions: InfiniteSessionConfig | None = None, + disable_resume: bool = False, + on_event: Callable[[SessionEvent], None] | None = None, + ) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -631,7 +678,30 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> Args: session_id: The ID of the session to resume. - config: Optional configuration for the resumed session. + on_permission_request: Handler for permission requests from the server. + model: Model to use for this session. Can change the model when resuming. + client_name: Client name to identify the application using the SDK. + reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh"). + tools: Custom tools exposed to the CLI. + system_message: System message configuration. + available_tools: List of tool names to allow (takes precedence over excluded_tools). + excluded_tools: List of tool names to disable (ignored if available_tools is set). + on_user_input_request: Handler for user input requests (enables ask_user tool). + hooks: Hook handlers for intercepting session lifecycle events. + working_directory: Working directory for the session. + provider: Custom provider configuration (BYOK - Bring Your Own Key). + streaming: Enable streaming of assistant message and reasoning chunks. + mcp_servers: MCP server configurations for the session. + custom_agents: Custom agent configurations for the session. + agent: Name of the custom agent to activate when the session starts. + config_dir: Override the default configuration directory location. + skill_directories: Directories to load skills from. + disabled_skills: List of skill names to disable. + infinite_sessions: Infinite session configuration for persistent workspaces. + disable_resume: When True, skips emitting the session.resume event. + Useful for reconnecting without triggering resume-related side effects. + on_event: Event handler registered before the session.resume RPC, ensuring + early events (e.g. session.start) are not missed. Returns: A :class:`CopilotSession` instance for the resumed session. @@ -640,33 +710,32 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> RuntimeError: If the session does not exist or the client is not connected. Example: - >>> # Resume a previous session - >>> config = {"on_permission_request": PermissionHandler.approve_all} - >>> session = await client.resume_session("session-123", config) + >>> session = await client.resume_session( + ... "session-123", + ... on_permission_request=PermissionHandler.approve_all, + ... ) >>> - >>> # Resume with new tools - >>> session = await client.resume_session("session-123", { - ... "on_permission_request": PermissionHandler.approve_all, - ... "tools": [my_new_tool] - ... }) + >>> # Resume with model and streaming + >>> session = await client.resume_session( + ... "session-123", + ... on_permission_request=PermissionHandler.approve_all, + ... model="gpt-4", + ... streaming=True, + ... ) """ + if not on_permission_request or not callable(on_permission_request): + raise ValueError( + "A valid on_permission_request handler is required. " + "Use PermissionHandler.approve_all or provide a custom handler." + ) + if not self._client: if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") - cfg = config - - if not cfg.get("on_permission_request"): - raise ValueError( - "An on_permission_request handler is required when resuming a session. " - "For example, to allow all permissions, use " - '{"on_permission_request": PermissionHandler.approve_all}.' - ) - tool_defs = [] - tools = cfg.get("tools") if tools: for tool in tools: definition: dict[str, Any] = { @@ -682,104 +751,64 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> tool_defs.append(definition) payload: dict[str, Any] = {"sessionId": session_id} - - # Add client name if provided - client_name = cfg.get("client_name") - if client_name: - payload["clientName"] = client_name - - # Add model if provided - model = cfg.get("model") if model: payload["model"] = model - - if cfg.get("reasoning_effort"): - payload["reasoningEffort"] = cfg["reasoning_effort"] + if client_name: + payload["clientName"] = client_name + if reasoning_effort: + payload["reasoningEffort"] = reasoning_effort if tool_defs: payload["tools"] = tool_defs - # Add system message configuration if provided - system_message = cfg.get("system_message") if system_message: payload["systemMessage"] = system_message - # Add available/excluded tools if provided - available_tools = cfg.get("available_tools") if available_tools is not None: payload["availableTools"] = available_tools - - excluded_tools = cfg.get("excluded_tools") if excluded_tools is not None: payload["excludedTools"] = excluded_tools - provider = cfg.get("provider") - if provider: - payload["provider"] = self._convert_provider_to_wire_format(provider) - - # Add streaming option if provided - streaming = cfg.get("streaming") - if streaming is not None: - payload["streaming"] = streaming - - # Always enable permission request callback (deny by default if no handler provided) - on_permission_request = cfg.get("on_permission_request") payload["requestPermission"] = True - # Enable user input request callback if handler provided - on_user_input_request = cfg.get("on_user_input_request") if on_user_input_request: payload["requestUserInput"] = True - # Enable hooks callback if any hook handler provided - hooks = cfg.get("hooks") if hooks and any(hooks.values()): payload["hooks"] = True - # Add working directory if provided - working_directory = cfg.get("working_directory") if working_directory: payload["workingDirectory"] = working_directory - # Add config directory if provided - config_dir = cfg.get("config_dir") - if config_dir: - payload["configDir"] = config_dir + if streaming is not None: + payload["streaming"] = streaming - # Add disable resume flag if provided - disable_resume = cfg.get("disable_resume") - if disable_resume: - payload["disableResume"] = True + if provider: + payload["provider"] = self._convert_provider_to_wire_format(provider) - # Add MCP servers configuration if provided - mcp_servers = cfg.get("mcp_servers") if mcp_servers: payload["mcpServers"] = mcp_servers payload["envValueMode"] = "direct" - # Add custom agents configuration if provided - custom_agents = cfg.get("custom_agents") if custom_agents: payload["customAgents"] = [ - self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents + self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents ] - # Add agent selection if provided - agent = cfg.get("agent") if agent: payload["agent"] = agent - # Add skill directories configuration if provided - skill_directories = cfg.get("skill_directories") + if config_dir: + payload["configDir"] = config_dir + + if disable_resume: + payload["disableResume"] = True + if skill_directories: payload["skillDirectories"] = skill_directories - # Add disabled skills configuration if provided - disabled_skills = cfg.get("disabled_skills") if disabled_skills: payload["disabledSkills"] = disabled_skills - # Add infinite sessions configuration if provided - infinite_sessions = cfg.get("infinite_sessions") if infinite_sessions: wire_config: dict[str, Any] = {} if "enabled" in infinite_sessions: @@ -804,13 +833,12 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> # Create and register the session before issuing the RPC so that # events emitted by the CLI (e.g. session.start) are not dropped. session = CopilotSession(session_id, self._client, None) - session._register_tools(cfg.get("tools")) + session._register_tools(tools) session._register_permission_handler(on_permission_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) - on_event = cfg.get("on_event") if on_event: session.on(on_event) with self._sessions_lock: @@ -1040,8 +1068,7 @@ async def get_last_session_id(self) -> str | None: Example: >>> last_id = await client.get_last_session_id() >>> if last_id: - ... config = {"on_permission_request": PermissionHandler.approve_all} - ... session = await client.resume_session(last_id, config) + ... session = await client.resume_session(last_id, PermissionHandler.approve_all) """ if not self._client: raise RuntimeError("Client not connected") diff --git a/python/copilot/session.py b/python/copilot/session.py index 90d156c4ce..7a8b9f05d6 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -245,9 +245,7 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]: ... print(f"Assistant: {event.data.content}") ... elif event.type == "session.error": ... print(f"Error: {event.data.message}") - ... >>> unsubscribe = session.on(handle_event) - ... >>> # Later, to stop receiving events: >>> unsubscribe() """ @@ -730,9 +728,7 @@ async def abort(self) -> None: >>> import asyncio >>> >>> # Start a long-running request - >>> task = asyncio.create_task( - ... session.send("Write a very long story...") - ... ) + >>> task = asyncio.create_task(session.send("Write a very long story...")) >>> >>> # Abort after 5 seconds >>> await asyncio.sleep(5) diff --git a/python/copilot/types.py b/python/copilot/types.py index 0a6e98867d..17be065bcf 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -537,63 +537,7 @@ class InfiniteSessionConfig(TypedDict, total=False): buffer_exhaustion_threshold: float -# Configuration for creating a session -class SessionConfig(TypedDict, total=False): - """Configuration for creating a session""" - - session_id: str # Optional custom session ID - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - model: str # Model to use for this session. Use client.list_models() to see available models. - # Reasoning effort level for models that support it. - # Only valid for models where capabilities.supports.reasoning_effort is True. - reasoning_effort: ReasoningEffort - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow (takes precedence over excluded_tools) - available_tools: list[str] - # List of tool names to disable (ignored if available_tools is set) - excluded_tools: list[str] - # Handler for permission requests from the server - on_permission_request: _PermissionHandlerFn - # Handler for user input requests from the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Custom provider configuration (BYOK - Bring Your Own Key) - provider: ProviderConfig - # Enable streaming of assistant message and reasoning chunks - # When True, assistant.message_delta and assistant.reasoning_delta events - # with delta_content are sent as the response is generated - streaming: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Name of the custom agent to activate when the session starts. - # Must match the name of one of the agents in custom_agents. - agent: str - # Override the default configuration directory location. - # When specified, the session will use this directory for storing config and state. - config_dir: str - # Directories to load skills from - skill_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - # When enabled (default), sessions automatically manage context limits and persist state. - # Set to {"enabled": False} to disable. - infinite_sessions: InfiniteSessionConfig - # Optional event handler that is registered on the session before the - # session.create RPC is issued, ensuring early events (e.g. session.start) - # are delivered. Equivalent to calling session.on(handler) immediately - # after creation, but executes earlier in the lifecycle so no events are missed. - on_event: Callable[[SessionEvent], None] - - +# Azure-specific provider options class AzureProviderOptions(TypedDict, total=False): """Azure-specific provider configuration""" @@ -615,56 +559,6 @@ class ProviderConfig(TypedDict, total=False): azure: AzureProviderOptions # Azure-specific options -# Configuration for resuming a session -class ResumeSessionConfig(TypedDict, total=False): - """Configuration for resuming a session""" - - # Client name to identify the application using the SDK. - # Included in the User-Agent header for API requests. - client_name: str - # Model to use for this session. Can change the model when resuming. - model: str - tools: list[Tool] - system_message: SystemMessageConfig # System message configuration - # List of tool names to allow (takes precedence over excluded_tools) - available_tools: list[str] - # List of tool names to disable (ignored if available_tools is set) - excluded_tools: list[str] - provider: ProviderConfig - # Reasoning effort level for models that support it. - reasoning_effort: ReasoningEffort - on_permission_request: _PermissionHandlerFn - # Handler for user input requestsfrom the agent (enables ask_user tool) - on_user_input_request: UserInputHandler - # Hook handlers for intercepting session lifecycle events - hooks: SessionHooks - # Working directory for the session. Tool operations will be relative to this directory. - working_directory: str - # Override the default configuration directory location. - config_dir: str - # Enable streaming of assistant message chunks - streaming: bool - # MCP server configurations for the session - mcp_servers: dict[str, MCPServerConfig] - # Custom agent configurations for the session - custom_agents: list[CustomAgentConfig] - # Name of the custom agent to activate when the session starts. - # Must match the name of one of the agents in custom_agents. - agent: str - # Directories to load skills from - skill_directories: list[str] - # List of skill names to disable - disabled_skills: list[str] - # Infinite session configuration for persistent workspaces and automatic compaction. - infinite_sessions: InfiniteSessionConfig - # When True, skips emitting the session.resume event. - # Useful for reconnecting to a session without triggering resume-related side effects. - disable_resume: bool - # Optional event handler registered before the session.resume RPC is issued, - # ensuring early events are delivered. See SessionConfig.on_event. - on_event: Callable[[SessionEvent], None] - - # Event handler type SessionEventHandler = Callable[[SessionEvent], None] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index ec59586767..63d3e73221 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -19,23 +19,21 @@ async def test_should_list_available_custom_agents(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - }, - { - "name": "another-agent", - "display_name": "Another Agent", - "description": "Another test agent", - "prompt": "You are another agent.", - }, - ], - } + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + }, + { + "name": "another-agent", + "display_name": "Another Agent", + "description": "Another test agent", + "prompt": "You are another agent.", + }, + ], ) result = await session.rpc.agent.list() @@ -59,17 +57,15 @@ async def test_should_return_null_when_no_agent_is_selected(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) result = await session.rpc.agent.get_current() @@ -88,17 +84,15 @@ async def test_should_select_and_get_current_agent(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) # Select the agent @@ -127,17 +121,15 @@ async def test_should_deselect_current_agent(self): try: await client.start() session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "custom_agents": [ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - } + on_permission_request=PermissionHandler.approve_all, + custom_agents=[ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], ) # Select then deselect @@ -161,7 +153,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) result = await session.rpc.agent.list() @@ -178,7 +170,7 @@ class TestSessionCompactionRpc: async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): """Test compacting session history via RPC.""" session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # Send a message to create some history diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index b9800156be..fc4cc60b51 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -30,10 +30,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) await session.send_and_wait( @@ -65,10 +63,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) await session.send_and_wait( @@ -102,10 +98,8 @@ async def on_user_input_request(request, invocation): } session = await ctx.client.create_session( - { - "on_user_input_request": on_user_input_request, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + on_user_input_request=on_user_input_request, ) response = await session.send_and_wait( diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index d7ec39dcdf..d266991f71 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -49,7 +49,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) try: - await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + await client.create_session(on_permission_request=PermissionHandler.approve_all) # Kill the server process to force cleanup to fail process = client._process @@ -72,7 +72,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): async def test_should_force_stop_without_cleanup(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) - await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + await client.create_session(on_permission_request=PermissionHandler.approve_all) await client.force_stop() assert client.get_state() == "disconnected" @@ -210,7 +210,7 @@ async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): # Verify subsequent calls also fail (don't hang) with pytest.raises(Exception) as exc_info2: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session.send("test") # Error message varies by platform (EINVAL on Windows, EPIPE on Linux) diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index 1310407056..beb51e74b0 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -17,16 +17,14 @@ async def test_should_trigger_compaction_with_low_threshold_and_emit_events( ): # Create session with very low compaction thresholds to trigger compaction quickly session = await ctx.client.create_session( - { - "infinite_sessions": { - "enabled": True, - # Trigger background compaction at 0.5% context usage (~1000 tokens) - "background_compaction_threshold": 0.005, - # Block at 1% to ensure compaction runs - "buffer_exhaustion_threshold": 0.01, - }, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + infinite_sessions={ + "enabled": True, + # Trigger background compaction at 0.5% context usage (~1000 tokens) + "background_compaction_threshold": 0.005, + # Block at 1% to ensure compaction runs + "buffer_exhaustion_threshold": 0.01, + }, ) compaction_start_events = [] @@ -70,10 +68,8 @@ async def test_should_not_emit_compaction_events_when_infinite_sessions_disabled self, ctx: E2ETestContext ): session = await ctx.client.create_session( - { - "infinite_sessions": {"enabled": False}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + infinite_sessions={"enabled": False}, ) compaction_events = [] diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index a4956482c6..2858d40f2c 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -24,10 +24,8 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "allow"} session = await ctx.client.create_session( - { - "hooks": {"on_pre_tool_use": on_pre_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + hooks={"on_pre_tool_use": on_pre_tool_use}, ) # Create a file for the model to read @@ -55,10 +53,8 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - { - "hooks": {"on_post_tool_use": on_post_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + hooks={"on_post_tool_use": on_post_tool_use}, ) # Create a file for the model to read @@ -91,13 +87,11 @@ async def on_post_tool_use(input_data, invocation): return None session = await ctx.client.create_session( - { - "hooks": { - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - }, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + hooks={ + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + }, ) write_file(ctx.work_dir, "both.txt", "Testing both hooks!") @@ -128,10 +122,8 @@ async def on_pre_tool_use(input_data, invocation): return {"permissionDecision": "deny"} session = await ctx.client.create_session( - { - "hooks": {"on_pre_tool_use": on_pre_tool_use}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + hooks={"on_pre_tool_use": on_pre_tool_use}, ) # Create a file diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index 8fffbe889a..c4bd894147 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -33,7 +33,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( } session = await ctx.client.create_session( - {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -51,7 +51,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( """Test that MCP server configuration is accepted on session resume""" # Create a session first session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -68,7 +68,8 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - {"mcp_servers": mcp_servers, "on_permission_request": PermissionHandler.approve_all}, + on_permission_request=PermissionHandler.approve_all, + mcp_servers=mcp_servers, ) assert session2.session_id == session_id @@ -95,10 +96,7 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( } session = await ctx.client.create_session( - { - "mcp_servers": mcp_servers, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers ) assert session.session_id is not None @@ -129,7 +127,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( ] session = await ctx.client.create_session( - {"custom_agents": custom_agents, "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, custom_agents=custom_agents ) assert session.session_id is not None @@ -147,7 +145,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( """Test that custom agent configuration is accepted on session resume""" # Create a session first session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -164,10 +162,8 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( session2 = await ctx.client.resume_session( session_id, - { - "custom_agents": custom_agents, - "on_permission_request": PermissionHandler.approve_all, - }, + on_permission_request=PermissionHandler.approve_all, + custom_agents=custom_agents, ) assert session2.session_id == session_id @@ -201,11 +197,9 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe ] session = await ctx.client.create_session( - { - "mcp_servers": mcp_servers, - "custom_agents": custom_agents, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + mcp_servers=mcp_servers, + custom_agents=custom_agents, ) assert session.session_id is not None diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index cb5d90cd2f..c77ae86e16 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -68,7 +68,7 @@ async def setup(self): # Trigger connection by creating and disconnecting an init session init_session = await self._client1.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await init_session.disconnect() @@ -199,15 +199,13 @@ def magic_number(params: SeedParams, invocation: ToolInvocation) -> str: # Client 1 creates a session with a custom tool session1 = await mctx.client1.create_session( - {"on_permission_request": PermissionHandler.approve_all, "tools": [magic_number]} + on_permission_request=PermissionHandler.approve_all, tools=[magic_number] ) # Client 2 resumes with NO tools — should not overwrite client 1's tools session2 = await mctx.client2.resume_session( - session1.session_id, {"on_permission_request": PermissionHandler.approve_all} + session1.session_id, on_permission_request=PermissionHandler.approve_all ) - - # Track events seen by each client client1_events = [] client2_events = [] session1.on(lambda event: client1_events.append(event)) @@ -240,17 +238,15 @@ async def test_one_client_approves_permission_and_both_see_the_result( # Client 1 creates a session and manually approves permission requests session1 = await mctx.client1.create_session( - { - "on_permission_request": lambda request, invocation: ( - permission_requests.append(request) or PermissionRequestResult(kind="approved") - ), - } + on_permission_request=lambda request, invocation: ( + permission_requests.append(request) or PermissionRequestResult(kind="approved") + ), ) # Client 2 resumes — its handler never resolves, so only client 1's approval takes effect session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": lambda request, invocation: asyncio.Future()}, + on_permission_request=lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -288,17 +284,15 @@ async def test_one_client_rejects_permission_and_both_see_the_result( """One client rejects a permission request and both see the result.""" # Client 1 creates a session and denies all permission requests session1 = await mctx.client1.create_session( - { - "on_permission_request": lambda request, invocation: PermissionRequestResult( - kind="denied-interactively-by-user" - ), - } + on_permission_request=lambda request, invocation: PermissionRequestResult( + kind="denied-interactively-by-user" + ), ) # Client 2 resumes — its handler never resolves session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": lambda request, invocation: asyncio.Future()}, + on_permission_request=lambda request, invocation: asyncio.Future(), ) client1_events = [] @@ -355,13 +349,14 @@ def currency_lookup(params: CountryCodeParams, invocation: ToolInvocation) -> st # Client 1 creates a session with tool A session1 = await mctx.client1.create_session( - {"on_permission_request": PermissionHandler.approve_all, "tools": [city_lookup]} + on_permission_request=PermissionHandler.approve_all, tools=[city_lookup] ) # Client 2 resumes with tool B (different tool, union should have both) session2 = await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": PermissionHandler.approve_all, "tools": [currency_lookup]}, + on_permission_request=PermissionHandler.approve_all, + tools=[currency_lookup], ) # Send prompts sequentially to avoid nondeterministic tool_call ordering @@ -402,13 +397,14 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Client 1 creates a session with stable_tool session1 = await mctx.client1.create_session( - {"on_permission_request": PermissionHandler.approve_all, "tools": [stable_tool]} + on_permission_request=PermissionHandler.approve_all, tools=[stable_tool] ) # Client 2 resumes with ephemeral_tool await mctx.client2.resume_session( session1.session_id, - {"on_permission_request": PermissionHandler.approve_all, "tools": [ephemeral_tool]}, + on_permission_request=PermissionHandler.approve_all, + tools=[ephemeral_tool], ) # Verify both tools work before disconnect. diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index d18b15b2da..a673d63b52 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -26,7 +26,7 @@ def on_permission_request( assert invocation["session_id"] == session.session_id return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request=on_permission_request) write_file(ctx.work_dir, "test.txt", "original content") @@ -49,7 +49,7 @@ def on_permission_request( ) -> PermissionRequestResult: return PermissionRequestResult(kind="denied-interactively-by-user") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request=on_permission_request) original_content = "protected content" write_file(ctx.work_dir, "protected.txt", original_content) @@ -70,7 +70,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies( def deny_all(request, invocation): return PermissionRequestResult() - session = await ctx.client.create_session({"on_permission_request": deny_all}) + session = await ctx.client.create_session(on_permission_request=deny_all) denied_events = [] done_event = asyncio.Event() @@ -102,7 +102,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ ): """Test that tool operations are denied after resume when handler explicitly denies""" session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -110,7 +110,7 @@ async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_ def deny_all(request, invocation): return PermissionRequestResult() - session2 = await ctx.client.resume_session(session_id, {"on_permission_request": deny_all}) + session2 = await ctx.client.resume_session(session_id, on_permission_request=deny_all) denied_events = [] done_event = asyncio.Event() @@ -140,7 +140,7 @@ def on_event(event): async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): """Test that sessions work with approve-all permission handler""" session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) message = await session.send_and_wait("What is 2+2?") @@ -162,7 +162,7 @@ async def on_permission_request( await asyncio.sleep(0.01) return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request=on_permission_request) await session.send_and_wait("Run 'echo test' and tell me what happens") @@ -176,7 +176,7 @@ async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestC # Create initial session session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id await session1.send_and_wait("What is 1+1?") @@ -189,7 +189,7 @@ def on_permission_request( return PermissionRequestResult(kind="approved") session2 = await ctx.client.resume_session( - session_id, {"on_permission_request": on_permission_request} + session_id, on_permission_request=on_permission_request ) await session2.send_and_wait("Run 'echo resumed' for me") @@ -207,7 +207,7 @@ def on_permission_request( ) -> PermissionRequestResult: raise RuntimeError("Handler error") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request=on_permission_request) message = await session.send_and_wait("Run 'echo test'. If you can't, say 'failed'.") @@ -232,7 +232,7 @@ def on_permission_request( assert len(request.tool_call_id) > 0 return PermissionRequestResult(kind="approved") - session = await ctx.client.create_session({"on_permission_request": on_permission_request}) + session = await ctx.client.create_session(on_permission_request=on_permission_request) await session.send_and_wait("Run 'echo test'") diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index ddf843ba45..814da067dd 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -78,7 +78,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" ) result = await session.rpc.model.get_current() @@ -92,7 +92,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.generated.rpc import SessionModelSwitchToParams session = await ctx.client.create_session( - {"model": "claude-sonnet-4.5", "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" ) # Get initial model @@ -119,7 +119,7 @@ async def test_get_and_set_session_mode(self): try: await client.start() session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # Get initial mode (default should be interactive) @@ -155,7 +155,7 @@ async def test_read_update_and_delete_plan(self): try: await client.start() session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # Initially plan should not exist @@ -198,7 +198,7 @@ async def test_create_list_and_read_workspace_files(self): try: await client.start() session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # Initially no files diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 272fd94a60..ffb0cd2bc5 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -15,7 +15,7 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"model": "fake-test-model", "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, model="fake-test-model" ) assert session.session_id @@ -32,7 +32,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): async def test_should_have_stateful_conversation(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) assistant_message = await session.send_and_wait("What is 1+1?") @@ -48,10 +48,8 @@ async def test_should_create_a_session_with_appended_systemMessage_config( ): system_message_suffix = "End each response with the phrase 'Have a nice day!'" session = await ctx.client.create_session( - { - "system_message": {"mode": "append", "content": system_message_suffix}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + system_message={"mode": "append", "content": system_message_suffix}, ) await session.send("What is your full name?") @@ -70,10 +68,8 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( ): test_system_message = "You are an assistant called Testy McTestface. Reply succinctly." session = await ctx.client.create_session( - { - "system_message": {"mode": "replace", "content": test_system_message}, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + system_message={"mode": "replace", "content": test_system_message}, ) await session.send("What is your full name?") @@ -88,10 +84,8 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "available_tools": ["view", "edit"], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + available_tools=["view", "edit"], ) await session.send("What is 1+1?") @@ -107,7 +101,7 @@ async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestCon async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"excluded_tools": ["view"], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, excluded_tools=["view"] ) await session.send("What is 1+1?") @@ -130,9 +124,9 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont import asyncio s1, s2, s3 = await asyncio.gather( - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), - ctx.client.create_session({"on_permission_request": PermissionHandler.approve_all}), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), + ctx.client.create_session(on_permission_request=PermissionHandler.approve_all), ) # All sessions should have unique IDs @@ -155,7 +149,7 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestContext): # Create initial session session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") @@ -164,7 +158,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC # Resume using the same client session2 = await ctx.client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} + session_id, on_permission_request=PermissionHandler.approve_all ) assert session2.session_id == session_id answer2 = await get_final_assistant_message(session2) @@ -178,7 +172,7 @@ async def test_should_resume_a_session_using_the_same_client(self, ctx: E2ETestC async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestContext): # Create initial session session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id answer = await session1.send_and_wait("What is 1+1?") @@ -200,7 +194,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont try: session2 = await new_client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} + session_id, on_permission_request=PermissionHandler.approve_all ) assert session2.session_id == session_id @@ -219,7 +213,7 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): await ctx.client.resume_session( - "non-existent-session-id", {"on_permission_request": PermissionHandler.approve_all} + "non-existent-session-id", on_permission_request=PermissionHandler.approve_all ) async def test_should_list_sessions(self, ctx: E2ETestContext): @@ -227,11 +221,11 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): # Create a couple of sessions and send messages to persist them session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session1.send_and_wait("Say hello") session2 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session2.send_and_wait("Say goodbye") @@ -270,7 +264,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Create a session and send a message to persist it session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session.send_and_wait("Hello") session_id = session.session_id @@ -294,7 +288,7 @@ async def test_should_delete_session(self, ctx: E2ETestContext): # Verify we cannot resume the deleted session with pytest.raises(Exception): await ctx.client.resume_session( - session_id, {"on_permission_request": PermissionHandler.approve_all} + session_id, on_permission_request=PermissionHandler.approve_all ) async def test_should_get_last_session_id(self, ctx: E2ETestContext): @@ -302,7 +296,7 @@ async def test_should_get_last_session_id(self, ctx: E2ETestContext): # Create a session and send a message to persist it session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session.send_and_wait("Say hello") @@ -324,21 +318,19 @@ def get_secret_number_handler(invocation): ) session = await ctx.client.create_session( - { - "tools": [ - Tool( - name="get_secret_number", - description="Gets the secret number", - handler=get_secret_number_handler, - parameters={ - "type": "object", - "properties": {"key": {"type": "string", "description": "Key"}}, - "required": ["key"], - }, - ) - ], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + tools=[ + Tool( + name="get_secret_number", + description="Gets the secret number", + handler=get_secret_number_handler, + parameters={ + "type": "object", + "properties": {"key": {"type": "string", "description": "Key"}}, + "required": ["key"], + }, + ) + ], ) answer = await session.send_and_wait("What is the secret number for key ALPHA?") @@ -347,49 +339,43 @@ def get_secret_number_handler(invocation): async def test_should_create_session_with_custom_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": "fake-key", - }, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + provider={ + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "fake-key", + }, ) assert session.session_id async def test_should_create_session_with_azure_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - { - "provider": { - "type": "azure", - "base_url": "https://my-resource.openai.azure.com", - "api_key": "fake-key", - "azure": { - "api_version": "2024-02-15-preview", - }, + on_permission_request=PermissionHandler.approve_all, + provider={ + "type": "azure", + "base_url": "https://my-resource.openai.azure.com", + "api_key": "fake-key", + "azure": { + "api_version": "2024-02-15-preview", }, - "on_permission_request": PermissionHandler.approve_all, - } + }, ) assert session.session_id async def test_should_resume_session_with_custom_provider(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session.session_id # Resume the session with a provider session2 = await ctx.client.resume_session( session_id, - { - "provider": { - "type": "openai", - "base_url": "https://api.openai.com/v1", - "api_key": "fake-key", - }, - "on_permission_request": PermissionHandler.approve_all, + on_permission_request=PermissionHandler.approve_all, + provider={ + "type": "openai", + "base_url": "https://api.openai.com/v1", + "api_key": "fake-key", }, ) @@ -399,7 +385,7 @@ async def test_should_abort_a_session(self, ctx: E2ETestContext): import asyncio session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # Set up event listeners BEFORE sending to avoid race conditions @@ -448,10 +434,8 @@ def capture_early(event): early_events.append(event) session = await ctx.client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - "on_event": capture_early, - } + on_permission_request=PermissionHandler.approve_all, + on_event=capture_early, ) assert any(e.type.value == "session.start" for e in early_events) @@ -491,10 +475,7 @@ async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestCo custom_config_dir = os.path.join(ctx.home_dir, "custom-config") session = await ctx.client.create_session( - { - "config_dir": custom_config_dir, - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, config_dir=custom_config_dir ) assert session.session_id @@ -508,7 +489,7 @@ async def test_session_log_emits_events_at_all_levels(self, ctx: E2ETestContext) import asyncio session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) received_events = [] @@ -552,7 +533,7 @@ async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext) import asyncio session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) model_change_event = asyncio.get_event_loop().create_future() @@ -571,7 +552,7 @@ def on_event(event): async def test_should_accept_blob_attachments(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) # 1x1 transparent PNG pixel, base64-encoded diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 066669f297..9b0599975f 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -56,10 +56,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET """Test that skills are loaded and applied from skillDirectories""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - { - "skill_directories": [skills_dir], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, skill_directories=[skills_dir] ) assert session.session_id is not None @@ -77,11 +74,9 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( """Test that disabledSkills prevents skill from being applied""" skills_dir = create_skill_dir(ctx.work_dir) session = await ctx.client.create_session( - { - "skill_directories": [skills_dir], - "disabled_skills": ["test-skill"], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + skill_directories=[skills_dir], + disabled_skills=["test-skill"], ) assert session.session_id is not None @@ -105,7 +100,7 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( # Create a session without skills first session1 = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) session_id = session1.session_id @@ -117,10 +112,8 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( # Resume with skillDirectories - skill should now be active session2 = await ctx.client.resume_session( session_id, - { - "skill_directories": [skills_dir], - "on_permission_request": PermissionHandler.approve_all, - }, + on_permission_request=PermissionHandler.approve_all, + skill_directories=[skills_dir], ) assert session2.session_id == session_id diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index 7f0d47e29e..05e977e129 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -14,7 +14,7 @@ class TestStreamingFidelity: async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"streaming": True, "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, streaming=True ) events = [] @@ -46,7 +46,7 @@ async def test_should_produce_delta_events_when_streaming_is_enabled(self, ctx: async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"streaming": False, "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, streaming=False ) events = [] @@ -67,7 +67,7 @@ async def test_should_not_produce_deltas_when_streaming_is_disabled(self, ctx: E async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestContext): session = await ctx.client.create_session( - {"streaming": False, "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, streaming=False ) await session.send_and_wait("What is 3 + 6?") await session.disconnect() @@ -88,7 +88,8 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont try: session2 = await new_client.resume_session( session.session_id, - {"streaming": True, "on_permission_request": PermissionHandler.approve_all}, + on_permission_request=PermissionHandler.approve_all, + streaming=True, ) events = [] session2.on(lambda event: events.append(event)) diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py index 5d5823d985..458897d497 100644 --- a/python/e2e/test_tools.py +++ b/python/e2e/test_tools.py @@ -24,7 +24,7 @@ async def test_invokes_built_in_tools(self, ctx: E2ETestContext): f.write("# ELIZA, the only chatbot you'll ever need") session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) await session.send("What's the first line of README.md in this directory?") @@ -40,7 +40,7 @@ def encrypt_string(params: EncryptParams, invocation: ToolInvocation) -> str: return params.input.upper() session = await ctx.client.create_session( - {"tools": [encrypt_string], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, tools=[encrypt_string] ) await session.send("Use encrypt_string to encrypt this string: Hello") @@ -53,7 +53,7 @@ def get_user_location() -> str: raise Exception("Melbourne") session = await ctx.client.create_session( - {"tools": [get_user_location], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, tools=[get_user_location] ) await session.send("What is my location? If you can't find out, just say 'unknown'.") @@ -116,7 +116,7 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]: ] session = await ctx.client.create_session( - {"tools": [db_query], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, tools=[db_query] ) expected_session_id = session.session_id @@ -154,7 +154,7 @@ def tracking_handler(request, invocation): return PermissionRequestResult(kind="no-result") session = await ctx.client.create_session( - {"tools": [safe_lookup], "on_permission_request": tracking_handler} + on_permission_request=tracking_handler, tools=[safe_lookup] ) await session.send("Use safe_lookup to look up 'test123'") @@ -175,7 +175,7 @@ def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str: return f"CUSTOM_GREP_RESULT: {params.query}" session = await ctx.client.create_session( - {"tools": [custom_grep], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, tools=[custom_grep] ) await session.send("Use grep to search for the word 'hello'") @@ -197,10 +197,7 @@ def on_permission_request(request, invocation): return PermissionRequestResult(kind="approved") session = await ctx.client.create_session( - { - "tools": [encrypt_string], - "on_permission_request": on_permission_request, - } + on_permission_request=on_permission_request, tools=[encrypt_string] ) await session.send("Use encrypt_string to encrypt this string: Hello") @@ -228,10 +225,7 @@ def on_permission_request(request, invocation): return PermissionRequestResult(kind="denied-interactively-by-user") session = await ctx.client.create_session( - { - "tools": [encrypt_string], - "on_permission_request": on_permission_request, - } + on_permission_request=on_permission_request, tools=[encrypt_string] ) await session.send("Use encrypt_string to encrypt this string: Hello") diff --git a/python/pyproject.toml b/python/pyproject.toml index ec270f97e5..7c1f8bbf2e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -68,6 +68,7 @@ select = [ ] [tool.ruff.format] +docstring-code-format = true quote-style = "double" indent-style = "space" diff --git a/python/samples/chat.py b/python/samples/chat.py index 908a125d77..ee94c21fec 100644 --- a/python/samples/chat.py +++ b/python/samples/chat.py @@ -9,11 +9,7 @@ async def main(): client = CopilotClient() await client.start() - session = await client.create_session( - { - "on_permission_request": PermissionHandler.approve_all, - } - ) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) def on_event(event): output = None diff --git a/python/test_client.py b/python/test_client.py index 9b7e8eb0f5..9f8f384231 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -24,8 +24,18 @@ async def test_create_session_raises_without_permission_handler(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: - with pytest.raises(ValueError, match="on_permission_request.*is required"): - await client.create_session({}) + with pytest.raises(TypeError, match="on_permission_request"): + await client.create_session() # type: ignore[call-arg] + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_create_session_raises_with_none_permission_handler(self): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + try: + with pytest.raises(ValueError, match="on_permission_request handler is required"): + await client.create_session(on_permission_request=None) # type: ignore[arg-type] finally: await client.force_stop() @@ -35,11 +45,9 @@ async def test_v2_permission_adapter_rejects_no_result(self): await client.start() try: session = await client.create_session( - { - "on_permission_request": lambda request, invocation: PermissionRequestResult( - kind="no-result" - ) - } + on_permission_request=lambda request, invocation: PermissionRequestResult( + kind="no-result" + ) ) with pytest.raises(ValueError, match="protocol v2 server"): await client._handle_permission_request_v2( @@ -57,10 +65,10 @@ async def test_resume_session_raises_without_permission_handler(self): await client.start() try: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) with pytest.raises(ValueError, match="on_permission_request.*is required"): - await client.resume_session(session.session_id, {}) + await client.resume_session(session.session_id, on_permission_request=None) finally: await client.force_stop() @@ -184,7 +192,7 @@ def grep(params) -> str: return "ok" await client.create_session( - {"tools": [grep], "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, tools=[grep] ) tool_defs = captured["session.create"]["tools"] assert len(tool_defs) == 1 @@ -200,7 +208,7 @@ async def test_resume_session_sends_overrides_built_in_tool(self): try: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) captured = {} @@ -218,7 +226,8 @@ def grep(params) -> str: await client.resume_session( session.session_id, - {"tools": [grep], "on_permission_request": PermissionHandler.approve_all}, + on_permission_request=PermissionHandler.approve_all, + tools=[grep], ) tool_defs = captured["session.resume"]["tools"] assert len(tool_defs) == 1 @@ -365,7 +374,7 @@ async def mock_request(method, params): client._client.request = mock_request await client.create_session( - {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all, client_name="my-app" ) assert captured["session.create"]["clientName"] == "my-app" finally: @@ -378,7 +387,7 @@ async def test_resume_session_forwards_client_name(self): try: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) captured = {} @@ -394,7 +403,8 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - {"client_name": "my-app", "on_permission_request": PermissionHandler.approve_all}, + on_permission_request=PermissionHandler.approve_all, + client_name="my-app", ) assert captured["session.resume"]["clientName"] == "my-app" finally: @@ -415,11 +425,9 @@ async def mock_request(method, params): client._client.request = mock_request await client.create_session( - { - "agent": "test-agent", - "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, + agent="test-agent", + custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}], ) assert captured["session.create"]["agent"] == "test-agent" finally: @@ -432,7 +440,7 @@ async def test_resume_session_forwards_agent(self): try: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) captured = {} @@ -447,11 +455,9 @@ async def mock_request(method, params): client._client.request = mock_request await client.resume_session( session.session_id, - { - "agent": "test-agent", - "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}], - "on_permission_request": PermissionHandler.approve_all, - }, + on_permission_request=PermissionHandler.approve_all, + agent="test-agent", + custom_agents=[{"name": "test-agent", "prompt": "You are a test agent."}], ) assert captured["session.resume"]["agent"] == "test-agent" finally: @@ -464,7 +470,7 @@ async def test_set_model_sends_correct_rpc(self): try: session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + on_permission_request=PermissionHandler.approve_all ) captured = {} diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index b76a82e2af..9950020702 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -18,19 +18,20 @@ async def main(): )) try: - session = await client.create_session({ - "model": ANTHROPIC_MODEL, - "provider": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model=ANTHROPIC_MODEL, + provider={ "type": "anthropic", "base_url": ANTHROPIC_BASE_URL, "api_key": ANTHROPIC_API_KEY, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index f19729ab21..57a49f2a50 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -19,9 +19,10 @@ async def main(): )) try: - session = await client.create_session({ - "model": AZURE_OPENAI_MODEL, - "provider": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model=AZURE_OPENAI_MODEL, + provider={ "type": "azure", "base_url": AZURE_OPENAI_ENDPOINT, "api_key": AZURE_OPENAI_API_KEY, @@ -29,12 +30,12 @@ async def main(): "api_version": AZURE_API_VERSION, }, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 517c1bee18..87dad58669 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -17,18 +17,19 @@ async def main(): )) try: - session = await client.create_session({ - "model": OLLAMA_MODEL, - "provider": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model=OLLAMA_MODEL, + provider={ "type": "openai", "base_url": OLLAMA_BASE_URL, }, - "available_tools": [], - "system_message": { + available_tools=[], + system_message={ "mode": "replace", "content": COMPACT_SYSTEM_PROMPT, }, - }) + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 7717982a03..fadd1c79d7 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") @@ -18,14 +18,15 @@ async def main(): )) try: - session = await client.create_session({ - "model": OPENAI_MODEL, - "provider": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model=OPENAI_MODEL, + provider={ "type": "openai", "base_url": OPENAI_BASE_URL, "api_key": OPENAI_API_KEY, }, - }) + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index f4ea5a2e8b..e7f640ae95 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -4,7 +4,7 @@ import time import urllib.request -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig DEVICE_CODE_URL = "https://github.com/login/device/code" @@ -84,7 +84,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait("What is the capital of France?") if response: print(response.data.content) diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 730fba01b4..c9ab35bce4 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -5,7 +5,7 @@ import urllib.request from flask import Flask, request, jsonify -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, PermissionHandler, ExternalServerConfig app = Flask(__name__) @@ -16,7 +16,7 @@ async def ask_copilot(prompt: str) -> str: client = CopilotClient(ExternalServerConfig(url=CLI_URL)) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait(prompt) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index ca366d93dc..07eb74e20a 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, PermissionHandler, ExternalServerConfig async def main(): @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index ca366d93dc..07eb74e20a 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, PermissionHandler, ExternalServerConfig async def main(): @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 947e698ce1..382f9c4f9c 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,7 +10,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 4d0463b9d3..bc9782b6b6 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig hook_log: list[str] = [] @@ -47,18 +47,16 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "hooks": { - "on_session_start": on_session_start, - "on_session_end": on_session_end, - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - "on_user_prompt_submitted": on_user_prompt_submitted, - "on_error_occurred": on_error_occurred, - }, - } + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", + hooks={ + "on_session_start": on_session_start, + "on_session_end": on_session_end, + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_user_prompt_submitted": on_user_prompt_submitted, + "on_error_occurred": on_error_occurred, + }, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 3c4cb66254..e4de98a9ab 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig # Track which tools requested permission permission_log: list[str] = [] @@ -23,11 +23,9 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": log_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + on_permission_request=log_permission, + model="claude-haiku-4.5", + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 7a50431d73..92981861d5 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig input_log: list[str] = [] @@ -27,12 +27,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "on_permission_request": auto_approve_permission, - "on_user_input_request": handle_user_input, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", + on_user_input_request=handle_user_input, + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 8480767924..55f1cb3945 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,9 +10,7 @@ async def main(): )) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - }) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait("Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.") if response: diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index b225e6937b..22f321b225 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,14 +10,15 @@ async def main(): )) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You have no tools. Respond with text only.", }, - }) + ) response = await session.send_and_wait("Use the grep tool to search for 'SDK' in README.md.") if response: diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index b51f95f755..37654e2699 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) sample_file = os.path.join(os.path.dirname(__file__), "..", "sample-data.txt") diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 0900c7001a..8baed649df 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,15 +10,16 @@ async def main(): )) try: - session = await client.create_session({ - "model": "claude-opus-4.6", - "reasoning_effort": "low", - "available_tools": [], - "system_message": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="claude-opus-4.6", + reasoning_effort="low", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely.", }, - }) + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 1fb1337eef..15d354258e 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ) response = await session.send_and_wait( diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index 4c053d7306..5c3994c4c8 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" @@ -15,18 +15,16 @@ async def main(): try: session1, session2 = await asyncio.gather( client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": PIRATE_PROMPT}, + available_tools=[], ), client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": ROBOT_PROMPT}, - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": ROBOT_PROMPT}, + available_tools=[], ), ) diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 96135df310..30aa40cd1a 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,19 +10,20 @@ async def main(): )) try: - session = await client.create_session({ - "model": "claude-haiku-4.5", - "available_tools": [], - "system_message": { + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + available_tools=[], + system_message={ "mode": "replace", "content": "You are a helpful assistant. Answer concisely in one sentence.", }, - "infinite_sessions": { + infinite_sessions={ "enabled": True, "background_compaction_threshold": 0.80, "buffer_exhaustion_threshold": 0.95, }, - }) + ) prompts = [ "What is the capital of France?", diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 818f5adb85..049ca1f83f 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -12,10 +12,9 @@ async def main(): try: # 1. Create a session session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + available_tools=[], ) # 2. Send the secret word @@ -27,7 +26,7 @@ async def main(): session_id = session.session_id # 4. Resume the session with the same ID - resumed = await client.resume_session(session_id) + resumed = await client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all) print("Session resumed") # 5. Ask for the secret word diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 610d5f08d6..20fd4902ec 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -11,10 +11,9 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "streaming": True, - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + streaming=True, ) chunk_count = 0 diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 97762bb100..c30107a2f3 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -11,18 +11,17 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "custom_agents": [ - { - "name": "researcher", - "display_name": "Research Agent", - "description": "A research agent that can only read and search files, not modify them", - "tools": ["grep", "glob", "view"], - "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", - }, - ], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + custom_agents=[ + { + "name": "researcher", + "display_name": "Research Agent", + "description": "A research agent that can only read and search files, not modify them", + "tools": ["grep", "glob", "view"], + "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", + }, + ], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 5d17903dc2..9edd04115c 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -22,8 +22,7 @@ async def main(): "args": args, } - session_config = { - "model": "claude-haiku-4.5", + session_kwargs = { "available_tools": [], "system_message": { "mode": "replace", @@ -31,9 +30,11 @@ async def main(): }, } if mcp_servers: - session_config["mcp_servers"] = mcp_servers + session_kwargs["mcp_servers"] = mcp_servers - session = await client.create_session(session_config) + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", **session_kwargs + ) response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index 1cd2e14389..c9a8047ec6 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -16,11 +16,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": [], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=[], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 00e8506a79..afa871d83c 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -15,14 +15,12 @@ async def main(): skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") session = await client.create_session( - { - "model": "claude-haiku-4.5", - "skill_directories": [skills_dir], - "on_permission_request": lambda _: {"kind": "approved"}, - "hooks": { - "on_pre_tool_use": lambda _: {"permission_decision": "allow"}, - }, - } + on_permission_request=lambda _, __: {"kind": "approved"}, + model="claude-haiku-4.5", + skill_directories=[skills_dir], + hooks={ + "on_pre_tool_use": lambda _, __: {"permissionDecision": "allow"}, + }, ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 95c22dda1f..668bca1978 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" @@ -13,11 +13,10 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, - "available_tools": ["grep", "glob", "view"], - } + on_permission_request=PermissionHandler.approve_all, + model="claude-haiku-4.5", + system_message={"mode": "replace", "content": SYSTEM_PROMPT}, + available_tools=["grep", "glob", "view"], ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 2170fbe625..73c539fe1b 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -23,11 +23,7 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "tools": [custom_grep], - "on_permission_request": PermissionHandler.approve_all, - } + on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", tools=[custom_grep] ) response = await session.send_and_wait( diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 9aba683ccf..92f2593a67 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -53,13 +53,11 @@ async def main(): try: session = await client.create_session( - { - "model": "claude-haiku-4.5", - "available_tools": [], - "tools": [create_file, read_file, list_files], - "on_permission_request": auto_approve_permission, - "hooks": {"on_pre_tool_use": auto_approve_tool}, - } + on_permission_request=auto_approve_permission, + model="claude-haiku-4.5", + available_tools=[], + tools=[create_file, read_file, list_files], + hooks={"on_pre_tool_use": auto_approve_tool}, ) response = await session.send_and_wait( diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 4c5b39b836..d1b8a56965 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, PermissionHandler, ExternalServerConfig async def main(): @@ -12,7 +12,7 @@ async def main(): try: # First session print("--- Session 1 ---") - session1 = await client.create_session({"model": "claude-haiku-4.5"}) + session1 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response1 = await session1.send_and_wait( "What is the capital of France?" @@ -29,7 +29,7 @@ async def main(): # Second session — tests that the server accepts new sessions print("--- Session 2 ---") - session2 = await client.create_session({"model": "claude-haiku-4.5"}) + session2 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response2 = await session2.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 947e698ce1..382f9c4f9c 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, SubprocessConfig +from copilot import CopilotClient, PermissionHandler, SubprocessConfig async def main(): @@ -10,7 +10,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index ca366d93dc..07eb74e20a 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, ExternalServerConfig +from copilot import CopilotClient, PermissionHandler, ExternalServerConfig async def main(): @@ -9,7 +9,7 @@ async def main(): )) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5") response = await session.send_and_wait( "What is the capital of France?" From d82fd624414fbc5ec23751caa18cc1a01b1092ad Mon Sep 17 00:00:00 2001 From: Ron Borysovski Date: Fri, 20 Mar 2026 12:49:10 +0200 Subject: [PATCH 58/61] fix(dotnet): handle unknown session event types gracefully (#881) * fix(dotnet): handle unknown session event types gracefully Add UnknownSessionEvent type and TryFromJson method so that unrecognized event types from newer CLI versions do not crash GetMessagesAsync or real-time event dispatch. * refactor: use IgnoreUnrecognizedTypeDiscriminators per review feedback --- dotnet/src/Generated/SessionEvents.cs | 6 +- dotnet/test/ForwardCompatibilityTests.cs | 100 +++++++++++++++++++++++ scripts/codegen/csharp.ts | 6 +- 3 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 dotnet/test/ForwardCompatibilityTests.cs diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 08c6bf5e05..40d2daf220 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -17,7 +17,7 @@ namespace GitHub.Copilot.SDK; [DebuggerDisplay("{DebuggerDisplay,nq}")] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)] + IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] [JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")] [JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")] @@ -79,7 +79,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")] [JsonDerivedType(typeof(UserInputRequestedEvent), "user_input.requested")] [JsonDerivedType(typeof(UserMessageEvent), "user.message")] -public abstract partial class SessionEvent +public partial class SessionEvent { /// Unique event identifier (UUID v4), generated when the event is emitted. [JsonPropertyName("id")] @@ -102,7 +102,7 @@ public abstract partial class SessionEvent /// The event type discriminator. ///
[JsonIgnore] - public abstract string Type { get; } + public virtual string Type => "unknown"; /// Deserializes a JSON string into a . public static SessionEvent FromJson(string json) => diff --git a/dotnet/test/ForwardCompatibilityTests.cs b/dotnet/test/ForwardCompatibilityTests.cs new file mode 100644 index 0000000000..d3f5b77851 --- /dev/null +++ b/dotnet/test/ForwardCompatibilityTests.cs @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using Xunit; + +namespace GitHub.Copilot.SDK.Test; + +/// +/// Tests for forward-compatible handling of unknown session event types. +/// Verifies that the SDK gracefully handles event types introduced by newer CLI versions. +/// +public class ForwardCompatibilityTests +{ + [Fact] + public void FromJson_KnownEventType_DeserializesNormally() + { + var json = """ + { + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-01-01T00:00:00Z", + "parentId": null, + "type": "user.message", + "data": { + "content": "Hello" + } + } + """; + + var result = SessionEvent.FromJson(json); + + Assert.IsType(result); + Assert.Equal("user.message", result.Type); + } + + [Fact] + public void FromJson_UnknownEventType_ReturnsBaseSessionEvent() + { + var json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef", + "type": "future.feature_from_server", + "data": { "key": "value" } + } + """; + + var result = SessionEvent.FromJson(json); + + Assert.IsType(result); + Assert.Equal("unknown", result.Type); + } + + [Fact] + public void FromJson_UnknownEventType_PreservesBaseMetadata() + { + var json = """ + { + "id": "12345678-1234-1234-1234-123456789abc", + "timestamp": "2026-06-15T10:30:00Z", + "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef", + "type": "future.feature_from_server", + "data": {} + } + """; + + var result = SessionEvent.FromJson(json); + + Assert.Equal(Guid.Parse("12345678-1234-1234-1234-123456789abc"), result.Id); + Assert.Equal(DateTimeOffset.Parse("2026-06-15T10:30:00Z"), result.Timestamp); + Assert.Equal(Guid.Parse("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.ParentId); + } + + [Fact] + public void FromJson_MultipleEvents_MixedKnownAndUnknown() + { + var events = new[] + { + """{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Hi"}}""", + """{"id":"00000000-0000-0000-0000-000000000002","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"future.unknown_type","data":{}}""", + """{"id":"00000000-0000-0000-0000-000000000003","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Bye"}}""", + }; + + var results = events.Select(SessionEvent.FromJson).ToList(); + + Assert.Equal(3, results.Count); + Assert.IsType(results[0]); + Assert.IsType(results[1]); + Assert.IsType(results[2]); + } + + [Fact] + public void SessionEvent_Type_DefaultsToUnknown() + { + var evt = new SessionEvent(); + + Assert.Equal("unknown", evt.Type); + } +} diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index c44973fb11..a48ed47b65 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -522,11 +522,11 @@ namespace GitHub.Copilot.SDK; lines.push(`/// Provides the base class from which all session events derive.`); lines.push(`/// `); lines.push(`[DebuggerDisplay("{DebuggerDisplay,nq}")]`); - lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]`); + lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` IgnoreUnrecognizedTypeDiscriminators = true)]`); for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) { lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`); } - lines.push(`public abstract partial class SessionEvent`, `{`); + lines.push(`public partial class SessionEvent`, `{`); lines.push(...xmlDocComment(baseDesc("id"), " ")); lines.push(` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, ""); lines.push(...xmlDocComment(baseDesc("timestamp"), " ")); @@ -536,7 +536,7 @@ namespace GitHub.Copilot.SDK; lines.push(...xmlDocComment(baseDesc("ephemeral"), " ")); lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, ` [JsonPropertyName("ephemeral")]`, ` public bool? Ephemeral { get; set; }`, ""); lines.push(` /// `, ` /// The event type discriminator.`, ` /// `); - lines.push(` [JsonIgnore]`, ` public abstract string Type { get; }`, ""); + lines.push(` [JsonIgnore]`, ` public virtual string Type => "unknown";`, ""); lines.push(` /// Deserializes a JSON string into a .`); lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, ""); lines.push(` /// Serializes this event to a JSON string.`); From a1240966c0dee71cf017b8ca67a91731ea5471f7 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 20 Mar 2026 06:18:41 -0700 Subject: [PATCH 59/61] fix: Go codegen enum prefixes and type name reconciliation (#883) * Update @github/copilot to 1.0.10-0 - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code * refactor(go): update handwritten files to use prefixed enum constant names Rename all references to copilot session event type and rpc constants to use the new prefixed naming convention matching the generated code: - copilot.SessionCompactionStart -> copilot.SessionEventTypeSessionCompactionStart - copilot.ExternalToolRequested -> copilot.SessionEventTypeExternalToolRequested - copilot.PermissionRequested -> copilot.SessionEventTypePermissionRequested - copilot.ToolExecutionStart -> copilot.SessionEventTypeToolExecutionStart - copilot.AssistantReasoning -> copilot.SessionEventTypeAssistantReasoning - copilot.Abort -> copilot.SessionEventTypeAbort - rpc.Interactive -> rpc.ModeInteractive - rpc.Plan -> rpc.ModePlan - rpc.Warning -> rpc.LevelWarning - rpc.Error -> rpc.LevelError - rpc.Info -> rpc.LevelInfo (and all other constants listed in the rename) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: Go codegen enum prefixes and type name reconciliation - Add 'mcp' to goInitialisms so toPascalCase produces SessionMCP* matching quicktype - Post-process enum constants to use canonical Go TypeNameValue convention (replaces quicktype's Purple/Fluffy/Tentacled prefixes and unprefixed constants) - Reconcile type names: extract actual quicktype-generated struct names and use them in RPC wrapper code instead of recomputing via toPascalCase - Extract field name mappings from quicktype output to handle keyword-avoidance renames - Update all handwritten Go references to use new prefixed constant names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update Go image-input examples to use copilot.AttachmentTypeFile The generated enum constant was renamed from copilot.File to copilot.AttachmentTypeFile to follow Go's TypeNameValue convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(codegen): resolve Python type names from quicktype output for acronyms The Python codegen used toPascalCase() to compute type names like SessionMcpListResult, but quicktype generates SessionMCPListResult (uppercase MCP). This caused runtime NameError in Python scenarios. Apply the same approach as go.ts: after quicktype runs, parse the generated output to extract actual class names and build a case-insensitive lookup map. Use resolveType() in emitMethod() and emitRpcWrapper() instead of recomputing names via toPascalCase(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update Go E2E tests for agent list and multi-client timeout - TestAgentSelectionRpc: CLI now returns built-in agents, so instead of asserting zero agents, verify no custom agents appear when none configured - TestMultiClient: increase broadcast event timeout from 10s to 30s Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix LogAsync compilation error by adding missing url parameter The generated Rpc.LogAsync method added a 'url' parameter between 'ephemeral' and 'cancellationToken', causing a type mismatch when Session.LogAsync passed cancellationToken as the 4th positional arg. Added the 'url' parameter to Session.LogAsync to match the generated Rpc method signature and pass it through correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review comments - valuePascal initialisms, Phase 2 regex, add pr/ado - valuePascal now uses goInitialisms so 'url' -> 'URL', 'mcp' -> 'MCP', etc. - Phase 2 regex uses [\s\S]*? to match multi-line func bodies - Added 'pr' and 'ado' to goInitialisms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python E2E agent list test for built-in agents The CLI now returns built-in/default agents even when no custom agents are configured. Update the assertion to verify no custom test agent names appear in the list, rather than asserting the list is empty. Matches the pattern used in the Go E2E test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Skip multi-client broadcast test across Go, Python, and C# CLI 1.0.7 no longer delivers broadcast external_tool events to secondary clients. Skip the 'both clients see tool request and completion events' test in all three languages with a clear note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Revert multi-client broadcast test skips for CLI 1.0.7 Remove the t.Skip/pytest.skip/Fact(Skip=...) additions that were disabling the multi-client broadcast tests across Go, Python, and C#. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove mcp/pr/ado from goInitialisms to avoid SessionRpc.MCP rename resolveType() already handles type name reconciliation from quicktype output, so these initialisms aren't needed and would cause an unnecessary breaking change to the SessionRpc.Mcp field name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Another Go codegen fix after rebase * Regenerate code * Python formatting Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> * Type name update Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python codegen Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update copilot.Blob to copilot.AttachmentTypeBlob and fix Python Optional codegen - Update Go E2E test, README, and docs to use prefixed enum constant - Fix Python codegen modernizePython to handle deeply nested Optional types - Fix ruff formatting in e2e/test_multi_client.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson --- docs/features/image-input.md | 8 +- dotnet/src/Generated/Rpc.cs | 742 +++++++- dotnet/src/Generated/SessionEvents.cs | 585 ++++++- dotnet/src/Session.cs | 5 +- go/README.md | 2 +- go/generated_session_events.go | 476 ++++-- go/internal/e2e/agent_and_compact_rpc_test.go | 11 +- go/internal/e2e/compaction_test.go | 6 +- go/internal/e2e/multi_client_test.go | 26 +- go/internal/e2e/permissions_test.go | 4 +- go/internal/e2e/rpc_test.go | 12 +- go/internal/e2e/session_test.go | 22 +- go/internal/e2e/testharness/helper.go | 2 +- go/rpc/generated_rpc.go | 619 ++++++- go/samples/chat.go | 4 +- go/session.go | 20 +- nodejs/package-lock.json | 56 +- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/rpc.ts | 477 ++++++ nodejs/src/generated/session-events.ts | 426 ++++- python/copilot/generated/rpc.py | 1496 ++++++++++++++--- python/copilot/generated/session_events.py | 439 ++++- python/e2e/test_agent_and_compact_rpc.py | 10 +- scripts/codegen/go.ts | 143 +- scripts/codegen/python.ts | 57 +- test/harness/package-lock.json | 56 +- test/harness/package.json | 2 +- test/scenarios/prompts/attachments/README.md | 2 +- 29 files changed, 5053 insertions(+), 659 deletions(-) diff --git a/docs/features/image-input.md b/docs/features/image-input.md index acec80d4ae..8295b83d71 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -121,7 +121,7 @@ func main() { Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.File, + Type: copilot.AttachmentTypeFile, Path: &path, }, }, @@ -147,7 +147,7 @@ session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.File, + Type: copilot.AttachmentTypeFile, Path: &path, }, }, @@ -315,7 +315,7 @@ func main() { Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.Blob, + Type: copilot.AttachmentTypeBlob, Data: &base64ImageData, MIMEType: &mimeType, DisplayName: &displayName, @@ -333,7 +333,7 @@ session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.Blob, + Type: copilot.AttachmentTypeBlob, Data: &base64ImageData, // base64-encoded string MIMEType: &mimeType, DisplayName: &displayName, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 6fc593c126..fabe4817ed 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -245,6 +245,10 @@ internal class SessionLogRequest /// When true, the message is transient and not persisted to the session event log on disk. [JsonPropertyName("ephemeral")] public bool? Ephemeral { get; set; } + + /// Optional URL the user can open in their browser for more details. + [JsonPropertyName("url")] + public string? Url { get; set; } } /// RPC data type for SessionModelGetCurrent operations. @@ -577,6 +581,347 @@ internal class SessionAgentDeselectRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SessionAgentReload operations. +[Experimental(Diagnostics.Experimental)] +public class SessionAgentReloadResult +{ + /// Reloaded custom agents. + [JsonPropertyName("agents")] + public List Agents { get => field ??= []; set; } +} + +/// RPC data type for SessionAgentReload operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionAgentReloadRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for Skill operations. +public class Skill +{ + /// Unique identifier for the skill. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Description of what the skill does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Source location type (e.g., project, personal, plugin). + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; + + /// Whether the skill can be invoked by the user as a slash command. + [JsonPropertyName("userInvocable")] + public bool UserInvocable { get; set; } + + /// Whether the skill is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Absolute path to the skill file. + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +/// RPC data type for SessionSkillsList operations. +[Experimental(Diagnostics.Experimental)] +public class SessionSkillsListResult +{ + /// Available skills. + [JsonPropertyName("skills")] + public List Skills { get => field ??= []; set; } +} + +/// RPC data type for SessionSkillsList operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionSkillsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for SessionSkillsEnable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionSkillsEnableResult +{ +} + +/// RPC data type for SessionSkillsEnable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionSkillsEnableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Name of the skill to enable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// RPC data type for SessionSkillsDisable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionSkillsDisableResult +{ +} + +/// RPC data type for SessionSkillsDisable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionSkillsDisableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Name of the skill to disable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// RPC data type for SessionSkillsReload operations. +[Experimental(Diagnostics.Experimental)] +public class SessionSkillsReloadResult +{ +} + +/// RPC data type for SessionSkillsReload operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionSkillsReloadRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for Server operations. +public class Server +{ + /// Server name (config key). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Connection status: connected, failed, pending, disabled, or not_configured. + [JsonPropertyName("status")] + public ServerStatus Status { get; set; } + + /// Configuration source: user, workspace, plugin, or builtin. + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Error message if the server failed to connect. + [JsonPropertyName("error")] + public string? Error { get; set; } +} + +/// RPC data type for SessionMcpList operations. +[Experimental(Diagnostics.Experimental)] +public class SessionMcpListResult +{ + /// Configured MCP servers. + [JsonPropertyName("servers")] + public List Servers { get => field ??= []; set; } +} + +/// RPC data type for SessionMcpList operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionMcpListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for SessionMcpEnable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionMcpEnableResult +{ +} + +/// RPC data type for SessionMcpEnable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionMcpEnableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Name of the MCP server to enable. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; +} + +/// RPC data type for SessionMcpDisable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionMcpDisableResult +{ +} + +/// RPC data type for SessionMcpDisable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionMcpDisableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Name of the MCP server to disable. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; +} + +/// RPC data type for SessionMcpReload operations. +[Experimental(Diagnostics.Experimental)] +public class SessionMcpReloadResult +{ +} + +/// RPC data type for SessionMcpReload operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionMcpReloadRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for Plugin operations. +public class Plugin +{ + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Marketplace the plugin came from. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; + + /// Installed version. + [JsonPropertyName("version")] + public string? Version { get; set; } + + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } +} + +/// RPC data type for SessionPluginsList operations. +[Experimental(Diagnostics.Experimental)] +public class SessionPluginsListResult +{ + /// Installed plugins. + [JsonPropertyName("plugins")] + public List Plugins { get => field ??= []; set; } +} + +/// RPC data type for SessionPluginsList operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionPluginsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for Extension operations. +public class Extension +{ + /// Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper'). + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Extension name (directory name). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/). + [JsonPropertyName("source")] + public ExtensionSource Source { get; set; } + + /// Current status: running, disabled, failed, or starting. + [JsonPropertyName("status")] + public ExtensionStatus Status { get; set; } + + /// Process ID if the extension is running. + [JsonPropertyName("pid")] + public double? Pid { get; set; } +} + +/// RPC data type for SessionExtensionsList operations. +[Experimental(Diagnostics.Experimental)] +public class SessionExtensionsListResult +{ + /// Discovered extensions and their current status. + [JsonPropertyName("extensions")] + public List Extensions { get => field ??= []; set; } +} + +/// RPC data type for SessionExtensionsList operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionExtensionsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for SessionExtensionsEnable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionExtensionsEnableResult +{ +} + +/// RPC data type for SessionExtensionsEnable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionExtensionsEnableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Source-qualified extension ID to enable. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; +} + +/// RPC data type for SessionExtensionsDisable operations. +[Experimental(Diagnostics.Experimental)] +public class SessionExtensionsDisableResult +{ +} + +/// RPC data type for SessionExtensionsDisable operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionExtensionsDisableRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Source-qualified extension ID to disable. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; +} + +/// RPC data type for SessionExtensionsReload operations. +[Experimental(Diagnostics.Experimental)] +public class SessionExtensionsReloadResult +{ +} + +/// RPC data type for SessionExtensionsReload operations. +[Experimental(Diagnostics.Experimental)] +internal class SessionExtensionsReloadRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for SessionCompactionCompact operations. [Experimental(Diagnostics.Experimental)] public class SessionCompactionCompactResult @@ -631,6 +976,74 @@ internal class SessionToolsHandlePendingToolCallRequest public string? Error { get; set; } } +/// RPC data type for SessionCommandsHandlePendingCommand operations. +public class SessionCommandsHandlePendingCommandResult +{ + /// Gets or sets the success value. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// RPC data type for SessionCommandsHandlePendingCommand operations. +internal class SessionCommandsHandlePendingCommandRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Request ID from the command invocation event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Error message if the command handler failed. + [JsonPropertyName("error")] + public string? Error { get; set; } +} + +/// RPC data type for SessionUiElicitation operations. +public class SessionUiElicitationResult +{ + /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). + [JsonPropertyName("action")] + public SessionUiElicitationResultAction Action { get; set; } + + /// The form values submitted by the user (present when action is 'accept'). + [JsonPropertyName("content")] + public Dictionary? Content { get; set; } +} + +/// JSON Schema describing the form fields to present to the user. +public class SessionUiElicitationRequestRequestedSchema +{ + /// Schema type indicator (always 'object'). + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + /// Form field definitions, keyed by field name. + [JsonPropertyName("properties")] + public Dictionary Properties { get => field ??= []; set; } + + /// List of required field names. + [JsonPropertyName("required")] + public List? Required { get; set; } +} + +/// RPC data type for SessionUiElicitation operations. +internal class SessionUiElicitationRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Message describing what information is needed from the user. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// JSON Schema describing the form fields to present to the user. + [JsonPropertyName("requestedSchema")] + public SessionUiElicitationRequestRequestedSchema RequestedSchema { get => field ??= new(); set; } +} + /// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations. public class SessionPermissionsHandlePendingPermissionRequestResult { @@ -739,6 +1152,76 @@ public enum SessionModeGetResultMode } +/// Connection status: connected, failed, pending, disabled, or not_configured. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ServerStatus +{ + /// The connected variant. + [JsonStringEnumMemberName("connected")] + Connected, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The pending variant. + [JsonStringEnumMemberName("pending")] + Pending, + /// The disabled variant. + [JsonStringEnumMemberName("disabled")] + Disabled, + /// The not_configured variant. + [JsonStringEnumMemberName("not_configured")] + NotConfigured, +} + + +/// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/). +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ExtensionSource +{ + /// The project variant. + [JsonStringEnumMemberName("project")] + Project, + /// The user variant. + [JsonStringEnumMemberName("user")] + User, +} + + +/// Current status: running, disabled, failed, or starting. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ExtensionStatus +{ + /// The running variant. + [JsonStringEnumMemberName("running")] + Running, + /// The disabled variant. + [JsonStringEnumMemberName("disabled")] + Disabled, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The starting variant. + [JsonStringEnumMemberName("starting")] + Starting, +} + + +/// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionUiElicitationResultAction +{ + /// The accept variant. + [JsonStringEnumMemberName("accept")] + Accept, + /// The decline variant. + [JsonStringEnumMemberName("decline")] + Decline, + /// The cancel variant. + [JsonStringEnumMemberName("cancel")] + Cancel, +} + + /// Signal to send (default: SIGTERM). [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionShellKillRequestSignal @@ -853,8 +1336,14 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Workspace = new WorkspaceApi(rpc, sessionId); Fleet = new FleetApi(rpc, sessionId); Agent = new AgentApi(rpc, sessionId); + Skills = new SkillsApi(rpc, sessionId); + Mcp = new McpApi(rpc, sessionId); + Plugins = new PluginsApi(rpc, sessionId); + Extensions = new ExtensionsApi(rpc, sessionId); Compaction = new CompactionApi(rpc, sessionId); Tools = new ToolsApi(rpc, sessionId); + Commands = new CommandsApi(rpc, sessionId); + Ui = new UiApi(rpc, sessionId); Permissions = new PermissionsApi(rpc, sessionId); Shell = new ShellApi(rpc, sessionId); } @@ -877,12 +1366,30 @@ internal SessionRpc(JsonRpc rpc, string sessionId) /// Agent APIs. public AgentApi Agent { get; } + /// Skills APIs. + public SkillsApi Skills { get; } + + /// Mcp APIs. + public McpApi Mcp { get; } + + /// Plugins APIs. + public PluginsApi Plugins { get; } + + /// Extensions APIs. + public ExtensionsApi Extensions { get; } + /// Compaction APIs. public CompactionApi Compaction { get; } /// Tools APIs. public ToolsApi Tools { get; } + /// Commands APIs. + public CommandsApi Commands { get; } + + /// Ui APIs. + public UiApi Ui { get; } + /// Permissions APIs. public PermissionsApi Permissions { get; } @@ -890,9 +1397,9 @@ internal SessionRpc(JsonRpc rpc, string sessionId) public ShellApi Shell { get; } /// Calls "session.log". - public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) + public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default) { - var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral }; + var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral, Url = url }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.log", [request], cancellationToken); } } @@ -1080,6 +1587,160 @@ public async Task DeselectAsync(CancellationToken ca var request = new SessionAgentDeselectRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.deselect", [request], cancellationToken); } + + /// Calls "session.agent.reload". + public async Task ReloadAsync(CancellationToken cancellationToken = default) + { + var request = new SessionAgentReloadRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.reload", [request], cancellationToken); + } +} + +/// Provides session-scoped Skills APIs. +[Experimental(Diagnostics.Experimental)] +public class SkillsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal SkillsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.skills.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionSkillsListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.list", [request], cancellationToken); + } + + /// Calls "session.skills.enable". + public async Task EnableAsync(string name, CancellationToken cancellationToken = default) + { + var request = new SessionSkillsEnableRequest { SessionId = _sessionId, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.enable", [request], cancellationToken); + } + + /// Calls "session.skills.disable". + public async Task DisableAsync(string name, CancellationToken cancellationToken = default) + { + var request = new SessionSkillsDisableRequest { SessionId = _sessionId, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.disable", [request], cancellationToken); + } + + /// Calls "session.skills.reload". + public async Task ReloadAsync(CancellationToken cancellationToken = default) + { + var request = new SessionSkillsReloadRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.reload", [request], cancellationToken); + } +} + +/// Provides session-scoped Mcp APIs. +[Experimental(Diagnostics.Experimental)] +public class McpApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal McpApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.mcp.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionMcpListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.list", [request], cancellationToken); + } + + /// Calls "session.mcp.enable". + public async Task EnableAsync(string serverName, CancellationToken cancellationToken = default) + { + var request = new SessionMcpEnableRequest { SessionId = _sessionId, ServerName = serverName }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.enable", [request], cancellationToken); + } + + /// Calls "session.mcp.disable". + public async Task DisableAsync(string serverName, CancellationToken cancellationToken = default) + { + var request = new SessionMcpDisableRequest { SessionId = _sessionId, ServerName = serverName }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.disable", [request], cancellationToken); + } + + /// Calls "session.mcp.reload". + public async Task ReloadAsync(CancellationToken cancellationToken = default) + { + var request = new SessionMcpReloadRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.reload", [request], cancellationToken); + } +} + +/// Provides session-scoped Plugins APIs. +[Experimental(Diagnostics.Experimental)] +public class PluginsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal PluginsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.plugins.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionPluginsListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.plugins.list", [request], cancellationToken); + } +} + +/// Provides session-scoped Extensions APIs. +[Experimental(Diagnostics.Experimental)] +public class ExtensionsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal ExtensionsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.extensions.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionExtensionsListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.list", [request], cancellationToken); + } + + /// Calls "session.extensions.enable". + public async Task EnableAsync(string id, CancellationToken cancellationToken = default) + { + var request = new SessionExtensionsEnableRequest { SessionId = _sessionId, Id = id }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.enable", [request], cancellationToken); + } + + /// Calls "session.extensions.disable". + public async Task DisableAsync(string id, CancellationToken cancellationToken = default) + { + var request = new SessionExtensionsDisableRequest { SessionId = _sessionId, Id = id }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.disable", [request], cancellationToken); + } + + /// Calls "session.extensions.reload". + public async Task ReloadAsync(CancellationToken cancellationToken = default) + { + var request = new SessionExtensionsReloadRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.reload", [request], cancellationToken); + } } /// Provides session-scoped Compaction APIs. @@ -1123,6 +1784,46 @@ public async Task HandlePendingToolCall } } +/// Provides session-scoped Commands APIs. +public class CommandsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal CommandsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.commands.handlePendingCommand". + public async Task HandlePendingCommandAsync(string requestId, string? error = null, CancellationToken cancellationToken = default) + { + var request = new SessionCommandsHandlePendingCommandRequest { SessionId = _sessionId, RequestId = requestId, Error = error }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.commands.handlePendingCommand", [request], cancellationToken); + } +} + +/// Provides session-scoped Ui APIs. +public class UiApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal UiApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.ui.elicitation". + public async Task ElicitationAsync(string message, SessionUiElicitationRequestRequestedSchema requestedSchema, CancellationToken cancellationToken = default) + { + var request = new SessionUiElicitationRequest { SessionId = _sessionId, Message = message, RequestedSchema = requestedSchema }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.ui.elicitation", [request], cancellationToken); + } +} + /// Provides session-scoped Permissions APIs. public class PermissionsApi { @@ -1177,6 +1878,7 @@ public async Task KillAsync(string processId, SessionShe [JsonSerializable(typeof(AccountGetQuotaResult))] [JsonSerializable(typeof(AccountGetQuotaResultQuotaSnapshotsValue))] [JsonSerializable(typeof(Agent))] +[JsonSerializable(typeof(Extension))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelBilling))] [JsonSerializable(typeof(ModelCapabilities))] @@ -1186,6 +1888,8 @@ public async Task KillAsync(string processId, SessionShe [JsonSerializable(typeof(ModelsListResult))] [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResult))] +[JsonSerializable(typeof(Plugin))] +[JsonSerializable(typeof(Server))] [JsonSerializable(typeof(SessionAgentDeselectRequest))] [JsonSerializable(typeof(SessionAgentDeselectResult))] [JsonSerializable(typeof(SessionAgentGetCurrentRequest))] @@ -1193,15 +1897,35 @@ public async Task KillAsync(string processId, SessionShe [JsonSerializable(typeof(SessionAgentGetCurrentResultAgent))] [JsonSerializable(typeof(SessionAgentListRequest))] [JsonSerializable(typeof(SessionAgentListResult))] +[JsonSerializable(typeof(SessionAgentReloadRequest))] +[JsonSerializable(typeof(SessionAgentReloadResult))] [JsonSerializable(typeof(SessionAgentSelectRequest))] [JsonSerializable(typeof(SessionAgentSelectResult))] [JsonSerializable(typeof(SessionAgentSelectResultAgent))] +[JsonSerializable(typeof(SessionCommandsHandlePendingCommandRequest))] +[JsonSerializable(typeof(SessionCommandsHandlePendingCommandResult))] [JsonSerializable(typeof(SessionCompactionCompactRequest))] [JsonSerializable(typeof(SessionCompactionCompactResult))] +[JsonSerializable(typeof(SessionExtensionsDisableRequest))] +[JsonSerializable(typeof(SessionExtensionsDisableResult))] +[JsonSerializable(typeof(SessionExtensionsEnableRequest))] +[JsonSerializable(typeof(SessionExtensionsEnableResult))] +[JsonSerializable(typeof(SessionExtensionsListRequest))] +[JsonSerializable(typeof(SessionExtensionsListResult))] +[JsonSerializable(typeof(SessionExtensionsReloadRequest))] +[JsonSerializable(typeof(SessionExtensionsReloadResult))] [JsonSerializable(typeof(SessionFleetStartRequest))] [JsonSerializable(typeof(SessionFleetStartResult))] [JsonSerializable(typeof(SessionLogRequest))] [JsonSerializable(typeof(SessionLogResult))] +[JsonSerializable(typeof(SessionMcpDisableRequest))] +[JsonSerializable(typeof(SessionMcpDisableResult))] +[JsonSerializable(typeof(SessionMcpEnableRequest))] +[JsonSerializable(typeof(SessionMcpEnableResult))] +[JsonSerializable(typeof(SessionMcpListRequest))] +[JsonSerializable(typeof(SessionMcpListResult))] +[JsonSerializable(typeof(SessionMcpReloadRequest))] +[JsonSerializable(typeof(SessionMcpReloadResult))] [JsonSerializable(typeof(SessionModeGetRequest))] [JsonSerializable(typeof(SessionModeGetResult))] [JsonSerializable(typeof(SessionModeSetRequest))] @@ -1218,18 +1942,32 @@ public async Task KillAsync(string processId, SessionShe [JsonSerializable(typeof(SessionPlanReadResult))] [JsonSerializable(typeof(SessionPlanUpdateRequest))] [JsonSerializable(typeof(SessionPlanUpdateResult))] +[JsonSerializable(typeof(SessionPluginsListRequest))] +[JsonSerializable(typeof(SessionPluginsListResult))] [JsonSerializable(typeof(SessionShellExecRequest))] [JsonSerializable(typeof(SessionShellExecResult))] [JsonSerializable(typeof(SessionShellKillRequest))] [JsonSerializable(typeof(SessionShellKillResult))] +[JsonSerializable(typeof(SessionSkillsDisableRequest))] +[JsonSerializable(typeof(SessionSkillsDisableResult))] +[JsonSerializable(typeof(SessionSkillsEnableRequest))] +[JsonSerializable(typeof(SessionSkillsEnableResult))] +[JsonSerializable(typeof(SessionSkillsListRequest))] +[JsonSerializable(typeof(SessionSkillsListResult))] +[JsonSerializable(typeof(SessionSkillsReloadRequest))] +[JsonSerializable(typeof(SessionSkillsReloadResult))] [JsonSerializable(typeof(SessionToolsHandlePendingToolCallRequest))] [JsonSerializable(typeof(SessionToolsHandlePendingToolCallResult))] +[JsonSerializable(typeof(SessionUiElicitationRequest))] +[JsonSerializable(typeof(SessionUiElicitationRequestRequestedSchema))] +[JsonSerializable(typeof(SessionUiElicitationResult))] [JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))] [JsonSerializable(typeof(SessionWorkspaceCreateFileResult))] [JsonSerializable(typeof(SessionWorkspaceListFilesRequest))] [JsonSerializable(typeof(SessionWorkspaceListFilesResult))] [JsonSerializable(typeof(SessionWorkspaceReadFileRequest))] [JsonSerializable(typeof(SessionWorkspaceReadFileResult))] +[JsonSerializable(typeof(Skill))] [JsonSerializable(typeof(Tool))] [JsonSerializable(typeof(ToolsListRequest))] [JsonSerializable(typeof(ToolsListResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 40d2daf220..2821052d0b 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -29,7 +29,9 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")] [JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")] [JsonDerivedType(typeof(CommandCompletedEvent), "command.completed")] +[JsonDerivedType(typeof(CommandExecuteEvent), "command.execute")] [JsonDerivedType(typeof(CommandQueuedEvent), "command.queued")] +[JsonDerivedType(typeof(CommandsChangedEvent), "commands.changed")] [JsonDerivedType(typeof(ElicitationCompletedEvent), "elicitation.completed")] [JsonDerivedType(typeof(ElicitationRequestedEvent), "elicitation.requested")] [JsonDerivedType(typeof(ExitPlanModeCompletedEvent), "exit_plan_mode.completed")] @@ -38,6 +40,8 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(ExternalToolRequestedEvent), "external_tool.requested")] [JsonDerivedType(typeof(HookEndEvent), "hook.end")] [JsonDerivedType(typeof(HookStartEvent), "hook.start")] +[JsonDerivedType(typeof(McpOauthCompletedEvent), "mcp.oauth_completed")] +[JsonDerivedType(typeof(McpOauthRequiredEvent), "mcp.oauth_required")] [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] [JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")] [JsonDerivedType(typeof(PermissionRequestedEvent), "permission.requested")] @@ -46,14 +50,18 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] [JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] [JsonDerivedType(typeof(SessionErrorEvent), "session.error")] +[JsonDerivedType(typeof(SessionExtensionsLoadedEvent), "session.extensions_loaded")] [JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")] [JsonDerivedType(typeof(SessionIdleEvent), "session.idle")] [JsonDerivedType(typeof(SessionInfoEvent), "session.info")] +[JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] +[JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] [JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] [JsonDerivedType(typeof(SessionResumeEvent), "session.resume")] [JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")] +[JsonDerivedType(typeof(SessionSkillsLoadedEvent), "session.skills_loaded")] [JsonDerivedType(typeof(SessionSnapshotRewindEvent), "session.snapshot_rewind")] [JsonDerivedType(typeof(SessionStartEvent), "session.start")] [JsonDerivedType(typeof(SessionTaskCompleteEvent), "session.task_complete")] @@ -337,7 +345,7 @@ public partial class SessionUsageInfoEvent : SessionEvent public required SessionUsageInfoData Data { get; set; } } -/// Empty payload; the event signals that LLM-powered conversation compaction has begun. +/// Context window breakdown at the start of LLM-powered conversation compaction. /// Represents the session.compaction_start event. public partial class SessionCompactionStartEvent : SessionEvent { @@ -363,7 +371,7 @@ public partial class SessionCompactionCompleteEvent : SessionEvent public required SessionCompactionCompleteData Data { get; set; } } -/// Task completion notification with optional summary from the agent. +/// Task completion notification with summary from the agent. /// Represents the session.task_complete event. public partial class SessionTaskCompleteEvent : SessionEvent { @@ -376,8 +384,7 @@ public partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } -/// User message content with optional attachments, source information, and interaction metadata. -/// Represents the user.message event. +/// Represents the user.message event. public partial class UserMessageEvent : SessionEvent { /// @@ -779,7 +786,7 @@ public partial class UserInputCompletedEvent : SessionEvent public required UserInputCompletedData Data { get; set; } } -/// Structured form elicitation request with JSON schema definition for form fields. +/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect). /// Represents the elicitation.requested event. public partial class ElicitationRequestedEvent : SessionEvent { @@ -805,6 +812,32 @@ public partial class ElicitationCompletedEvent : SessionEvent public required ElicitationCompletedData Data { get; set; } } +/// OAuth authentication request for an MCP server. +/// Represents the mcp.oauth_required event. +public partial class McpOauthRequiredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.oauth_required"; + + /// The mcp.oauth_required event payload. + [JsonPropertyName("data")] + public required McpOauthRequiredData Data { get; set; } +} + +/// MCP OAuth request completion notification. +/// Represents the mcp.oauth_completed event. +public partial class McpOauthCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.oauth_completed"; + + /// The mcp.oauth_completed event payload. + [JsonPropertyName("data")] + public required McpOauthCompletedData Data { get; set; } +} + /// External tool invocation request for client-side tool execution. /// Represents the external_tool.requested event. public partial class ExternalToolRequestedEvent : SessionEvent @@ -844,6 +877,19 @@ public partial class CommandQueuedEvent : SessionEvent public required CommandQueuedData Data { get; set; } } +/// Registered command dispatch request routed to the owning client. +/// Represents the command.execute event. +public partial class CommandExecuteEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "command.execute"; + + /// The command.execute event payload. + [JsonPropertyName("data")] + public required CommandExecuteData Data { get; set; } +} + /// Queued command completion notification signaling UI dismissal. /// Represents the command.completed event. public partial class CommandCompletedEvent : SessionEvent @@ -857,6 +903,19 @@ public partial class CommandCompletedEvent : SessionEvent public required CommandCompletedData Data { get; set; } } +/// SDK command registration change notification. +/// Represents the commands.changed event. +public partial class CommandsChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "commands.changed"; + + /// The commands.changed event payload. + [JsonPropertyName("data")] + public required CommandsChangedData Data { get; set; } +} + /// Plan approval request with plan content and available user actions. /// Represents the exit_plan_mode.requested event. public partial class ExitPlanModeRequestedEvent : SessionEvent @@ -907,6 +966,54 @@ public partial class SessionBackgroundTasksChangedEvent : SessionEvent public required SessionBackgroundTasksChangedData Data { get; set; } } +/// Represents the session.skills_loaded event. +public partial class SessionSkillsLoadedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.skills_loaded"; + + /// The session.skills_loaded event payload. + [JsonPropertyName("data")] + public required SessionSkillsLoadedData Data { get; set; } +} + +/// Represents the session.mcp_servers_loaded event. +public partial class SessionMcpServersLoadedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_servers_loaded"; + + /// The session.mcp_servers_loaded event payload. + [JsonPropertyName("data")] + public required SessionMcpServersLoadedData Data { get; set; } +} + +/// Represents the session.mcp_server_status_changed event. +public partial class SessionMcpServerStatusChangedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_server_status_changed"; + + /// The session.mcp_server_status_changed event payload. + [JsonPropertyName("data")] + public required SessionMcpServerStatusChangedData Data { get; set; } +} + +/// Represents the session.extensions_loaded event. +public partial class SessionExtensionsLoadedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.extensions_loaded"; + + /// The session.extensions_loaded event payload. + [JsonPropertyName("data")] + public required SessionExtensionsLoadedData Data { get; set; } +} + /// Session initialization metadata including context and configuration. public partial class SessionStartData { @@ -1008,6 +1115,11 @@ public partial class SessionErrorData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + + /// Optional URL associated with this error that the user can open in a browser. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public string? Url { get; set; } } /// Payload indicating the agent is idle; includes any background tasks still in flight. @@ -1037,6 +1149,11 @@ public partial class SessionInfoData /// Human-readable informational message for display in the timeline. [JsonPropertyName("message")] public required string Message { get; set; } + + /// Optional URL associated with this message that the user can open in a browser. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public string? Url { get; set; } } /// Warning message for timeline display with categorization. @@ -1049,6 +1166,11 @@ public partial class SessionWarningData /// Human-readable warning message for display in the timeline. [JsonPropertyName("message")] public required string Message { get; set; } + + /// Optional URL associated with this warning that the user can open in a browser. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public string? Url { get; set; } } /// Model change details including previous and new model identifiers. @@ -1222,6 +1344,26 @@ public partial class SessionShutdownData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("currentModel")] public string? CurrentModel { get; set; } + + /// Total tokens in context window at shutdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("currentTokens")] + public double? CurrentTokens { get; set; } + + /// System message token count at shutdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("systemTokens")] + public double? SystemTokens { get; set; } + + /// Non-system message token count at shutdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("conversationTokens")] + public double? ConversationTokens { get; set; } + + /// Tool definitions token count at shutdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolDefinitionsTokens")] + public double? ToolDefinitionsTokens { get; set; } } /// Updated working directory and git context after the change. @@ -1276,11 +1418,45 @@ public partial class SessionUsageInfoData /// Current number of messages in the conversation. [JsonPropertyName("messagesLength")] public required double MessagesLength { get; set; } + + /// Token count from system message(s). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("systemTokens")] + public double? SystemTokens { get; set; } + + /// Token count from non-system messages (user, assistant, tool). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("conversationTokens")] + public double? ConversationTokens { get; set; } + + /// Token count from tool definitions. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolDefinitionsTokens")] + public double? ToolDefinitionsTokens { get; set; } + + /// Whether this is the first usage_info event emitted in this session. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("isInitial")] + public bool? IsInitial { get; set; } } -/// Empty payload; the event signals that LLM-powered conversation compaction has begun. +/// Context window breakdown at the start of LLM-powered conversation compaction. public partial class SessionCompactionStartData { + /// Token count from system message(s) at compaction start. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("systemTokens")] + public double? SystemTokens { get; set; } + + /// Token count from non-system messages (user, assistant, tool) at compaction start. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("conversationTokens")] + public double? ConversationTokens { get; set; } + + /// Token count from tool definitions at compaction start. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolDefinitionsTokens")] + public double? ToolDefinitionsTokens { get; set; } } /// Conversation compaction results including success status, metrics, and optional error details. @@ -1344,18 +1520,38 @@ public partial class SessionCompactionCompleteData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestId")] public string? RequestId { get; set; } + + /// Token count from system message(s) after compaction. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("systemTokens")] + public double? SystemTokens { get; set; } + + /// Token count from non-system messages (user, assistant, tool) after compaction. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("conversationTokens")] + public double? ConversationTokens { get; set; } + + /// Token count from tool definitions after compaction. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolDefinitionsTokens")] + public double? ToolDefinitionsTokens { get; set; } } -/// Task completion notification with optional summary from the agent. +/// Task completion notification with summary from the agent. public partial class SessionTaskCompleteData { - /// Optional summary of the completed task, provided by the agent. + /// Summary of the completed task, provided by the agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("summary")] public string? Summary { get; set; } + + /// Whether the tool call succeeded. False when validation failed (e.g., invalid arguments). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("success")] + public bool? Success { get; set; } } -/// User message content with optional attachments, source information, and interaction metadata. +/// Event payload for . public partial class UserMessageData { /// The user's message text as displayed in the timeline. @@ -1372,10 +1568,10 @@ public partial class UserMessageData [JsonPropertyName("attachments")] public UserMessageDataAttachmentsItem[]? Attachments { get; set; } - /// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command"). + /// Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] - public UserMessageDataSource? Source { get; set; } + public string? Source { get; set; } /// The agent mode that was active when this message was sent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1953,6 +2149,11 @@ public partial class UserInputRequestedData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("allowFreeform")] public bool? AllowFreeform { get; set; } + + /// The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } } /// User input request completion notification signaling UI dismissal. @@ -1963,25 +2164,41 @@ public partial class UserInputCompletedData public required string RequestId { get; set; } } -/// Structured form elicitation request with JSON schema definition for form fields. +/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect). public partial class ElicitationRequestedData { /// Unique identifier for this elicitation request; used to respond via session.respondToElicitation(). [JsonPropertyName("requestId")] public required string RequestId { get; set; } + /// Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + /// The source that initiated the request (MCP server name, or absent for agent-initiated). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("elicitationSource")] + public string? ElicitationSource { get; set; } + /// Message describing what information is needed from the user. [JsonPropertyName("message")] public required string Message { get; set; } - /// Elicitation mode; currently only "form" is supported. Defaults to "form" when absent. + /// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("mode")] - public string? Mode { get; set; } + public ElicitationRequestedDataMode? Mode { get; set; } - /// JSON Schema describing the form fields to present to the user. + /// JSON Schema describing the form fields to present to the user (form mode only). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestedSchema")] - public required ElicitationRequestedDataRequestedSchema RequestedSchema { get; set; } + public ElicitationRequestedDataRequestedSchema? RequestedSchema { get; set; } + + /// URL to open in the user's browser (url mode only). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public string? Url { get; set; } } /// Elicitation request completion notification signaling UI dismissal. @@ -1992,6 +2209,35 @@ public partial class ElicitationCompletedData public required string RequestId { get; set; } } +/// OAuth authentication request for an MCP server. +public partial class McpOauthRequiredData +{ + /// Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth(). + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// Display name of the MCP server that requires OAuth. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// URL of the MCP server that requires OAuth. + [JsonPropertyName("serverUrl")] + public required string ServerUrl { get; set; } + + /// Static OAuth client configuration, if the server specifies one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("staticClientConfig")] + public McpOauthRequiredDataStaticClientConfig? StaticClientConfig { get; set; } +} + +/// MCP OAuth request completion notification. +public partial class McpOauthCompletedData +{ + /// Request ID of the resolved OAuth request. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + /// External tool invocation request for client-side tool execution. public partial class ExternalToolRequestedData { @@ -2047,6 +2293,26 @@ public partial class CommandQueuedData public required string Command { get; set; } } +/// Registered command dispatch request routed to the owning client. +public partial class CommandExecuteData +{ + /// Unique identifier; used to respond via session.commands.handlePendingCommand(). + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// The full command text (e.g., /deploy production). + [JsonPropertyName("command")] + public required string Command { get; set; } + + /// Command name without leading /. + [JsonPropertyName("commandName")] + public required string CommandName { get; set; } + + /// Raw argument string after the command name. + [JsonPropertyName("args")] + public required string Args { get; set; } +} + /// Queued command completion notification signaling UI dismissal. public partial class CommandCompletedData { @@ -2055,6 +2321,14 @@ public partial class CommandCompletedData public required string RequestId { get; set; } } +/// SDK command registration change notification. +public partial class CommandsChangedData +{ + /// Current list of registered SDK commands. + [JsonPropertyName("commands")] + public required CommandsChangedDataCommandsItem[] Commands { get; set; } +} + /// Plan approval request with plan content and available user actions. public partial class ExitPlanModeRequestedData { @@ -2100,6 +2374,42 @@ public partial class SessionBackgroundTasksChangedData { } +/// Event payload for . +public partial class SessionSkillsLoadedData +{ + /// Array of resolved skill metadata. + [JsonPropertyName("skills")] + public required SessionSkillsLoadedDataSkillsItem[] Skills { get; set; } +} + +/// Event payload for . +public partial class SessionMcpServersLoadedData +{ + /// Array of MCP server status summaries. + [JsonPropertyName("servers")] + public required SessionMcpServersLoadedDataServersItem[] Servers { get; set; } +} + +/// Event payload for . +public partial class SessionMcpServerStatusChangedData +{ + /// Name of the MCP server whose status changed. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// New connection status: connected, failed, pending, disabled, or not_configured. + [JsonPropertyName("status")] + public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; } +} + +/// Event payload for . +public partial class SessionExtensionsLoadedData +{ + /// Array of discovered extensions and their status. + [JsonPropertyName("extensions")] + public required SessionExtensionsLoadedDataExtensionsItem[] Extensions { get; set; } +} + /// Working directory and git context at session start. /// Nested data type for SessionStartDataContext. public partial class SessionStartDataContext @@ -2787,6 +3097,27 @@ public partial class SystemNotificationDataKindAgentCompleted : SystemNotificati public string? Prompt { get; set; } } +/// The agent_idle variant of . +public partial class SystemNotificationDataKindAgentIdle : SystemNotificationDataKind +{ + /// + [JsonIgnore] + public override string Type => "agent_idle"; + + /// Unique identifier of the background agent. + [JsonPropertyName("agentId")] + public required string AgentId { get; set; } + + /// Type of the agent (e.g., explore, task, general-purpose). + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } + + /// Human-readable description of the agent task. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + /// The shell_completed variant of . public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind { @@ -2832,6 +3163,7 @@ public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNo TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(SystemNotificationDataKindAgentCompleted), "agent_completed")] +[JsonDerivedType(typeof(SystemNotificationDataKindAgentIdle), "agent_idle")] [JsonDerivedType(typeof(SystemNotificationDataKindShellCompleted), "shell_completed")] [JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")] public partial class SystemNotificationDataKind @@ -3130,7 +3462,7 @@ public partial class PermissionCompletedDataResult public required PermissionCompletedDataResultKind Kind { get; set; } } -/// JSON Schema describing the form fields to present to the user. +/// JSON Schema describing the form fields to present to the user (form mode only). /// Nested data type for ElicitationRequestedDataRequestedSchema. public partial class ElicitationRequestedDataRequestedSchema { @@ -3148,6 +3480,104 @@ public partial class ElicitationRequestedDataRequestedSchema public string[]? Required { get; set; } } +/// Static OAuth client configuration, if the server specifies one. +/// Nested data type for McpOauthRequiredDataStaticClientConfig. +public partial class McpOauthRequiredDataStaticClientConfig +{ + /// OAuth client ID for the server. + [JsonPropertyName("clientId")] + public required string ClientId { get; set; } + + /// Whether this is a public OAuth client. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("publicClient")] + public bool? PublicClient { get; set; } +} + +/// Nested data type for CommandsChangedDataCommandsItem. +public partial class CommandsChangedDataCommandsItem +{ + /// Gets or sets the name value. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Gets or sets the description value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +/// Nested data type for SessionSkillsLoadedDataSkillsItem. +public partial class SessionSkillsLoadedDataSkillsItem +{ + /// Unique identifier for the skill. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Description of what the skill does. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Source location type of the skill (e.g., project, personal, plugin). + [JsonPropertyName("source")] + public required string Source { get; set; } + + /// Whether the skill can be invoked by the user as a slash command. + [JsonPropertyName("userInvocable")] + public required bool UserInvocable { get; set; } + + /// Whether the skill is currently enabled. + [JsonPropertyName("enabled")] + public required bool Enabled { get; set; } + + /// Absolute path to the skill file, if available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +/// Nested data type for SessionMcpServersLoadedDataServersItem. +public partial class SessionMcpServersLoadedDataServersItem +{ + /// Server name (config key). + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Connection status: connected, failed, pending, disabled, or not_configured. + [JsonPropertyName("status")] + public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; } + + /// Configuration source: user, workspace, plugin, or builtin. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Error message if the server failed to connect. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } +} + +/// Nested data type for SessionExtensionsLoadedDataExtensionsItem. +public partial class SessionExtensionsLoadedDataExtensionsItem +{ + /// Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper'). + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// Extension name (directory name). + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Discovery source. + [JsonPropertyName("source")] + public required SessionExtensionsLoadedDataExtensionsItemSource Source { get; set; } + + /// Current status: running, disabled, failed, or starting. + [JsonPropertyName("status")] + public required SessionExtensionsLoadedDataExtensionsItemStatus Status { get; set; } +} + /// Hosting platform type of the repository (github or ado). [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionStartDataContextHostType @@ -3226,42 +3656,6 @@ public enum UserMessageDataAttachmentsItemGithubReferenceReferenceType Discussion, } -/// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command"). -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum UserMessageDataSource -{ - /// The user variant. - [JsonStringEnumMemberName("user")] - User, - /// The autopilot variant. - [JsonStringEnumMemberName("autopilot")] - Autopilot, - /// The skill variant. - [JsonStringEnumMemberName("skill")] - Skill, - /// The system variant. - [JsonStringEnumMemberName("system")] - System, - /// The command variant. - [JsonStringEnumMemberName("command")] - Command, - /// The immediate-prompt variant. - [JsonStringEnumMemberName("immediate-prompt")] - ImmediatePrompt, - /// The jit-instruction variant. - [JsonStringEnumMemberName("jit-instruction")] - JitInstruction, - /// The snippy-blocking variant. - [JsonStringEnumMemberName("snippy-blocking")] - SnippyBlocking, - /// The thinking-exhausted-continuation variant. - [JsonStringEnumMemberName("thinking-exhausted-continuation")] - ThinkingExhaustedContinuation, - /// The other variant. - [JsonStringEnumMemberName("other")] - Other, -} - /// The agent mode that was active when this message was sent. [JsonConverter(typeof(JsonStringEnumConverter))] public enum UserMessageDataAgentMode @@ -3349,6 +3743,69 @@ public enum PermissionCompletedDataResultKind DeniedByContentExclusionPolicy, } +/// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ElicitationRequestedDataMode +{ + /// The form variant. + [JsonStringEnumMemberName("form")] + Form, + /// The url variant. + [JsonStringEnumMemberName("url")] + Url, +} + +/// Connection status: connected, failed, pending, disabled, or not_configured. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionMcpServersLoadedDataServersItemStatus +{ + /// The connected variant. + [JsonStringEnumMemberName("connected")] + Connected, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The pending variant. + [JsonStringEnumMemberName("pending")] + Pending, + /// The disabled variant. + [JsonStringEnumMemberName("disabled")] + Disabled, + /// The not_configured variant. + [JsonStringEnumMemberName("not_configured")] + NotConfigured, +} + +/// Discovery source. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionExtensionsLoadedDataExtensionsItemSource +{ + /// The project variant. + [JsonStringEnumMemberName("project")] + Project, + /// The user variant. + [JsonStringEnumMemberName("user")] + User, +} + +/// Current status: running, disabled, failed, or starting. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionExtensionsLoadedDataExtensionsItemStatus +{ + /// The running variant. + [JsonStringEnumMemberName("running")] + Running, + /// The disabled variant. + [JsonStringEnumMemberName("disabled")] + Disabled, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The starting variant. + [JsonStringEnumMemberName("starting")] + Starting, +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -3379,8 +3836,13 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(AssistantUsageEvent))] [JsonSerializable(typeof(CommandCompletedData))] [JsonSerializable(typeof(CommandCompletedEvent))] +[JsonSerializable(typeof(CommandExecuteData))] +[JsonSerializable(typeof(CommandExecuteEvent))] [JsonSerializable(typeof(CommandQueuedData))] [JsonSerializable(typeof(CommandQueuedEvent))] +[JsonSerializable(typeof(CommandsChangedData))] +[JsonSerializable(typeof(CommandsChangedDataCommandsItem))] +[JsonSerializable(typeof(CommandsChangedEvent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] [JsonSerializable(typeof(ElicitationRequestedData))] @@ -3399,6 +3861,11 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(HookEndEvent))] [JsonSerializable(typeof(HookStartData))] [JsonSerializable(typeof(HookStartEvent))] +[JsonSerializable(typeof(McpOauthCompletedData))] +[JsonSerializable(typeof(McpOauthCompletedEvent))] +[JsonSerializable(typeof(McpOauthRequiredData))] +[JsonSerializable(typeof(McpOauthRequiredDataStaticClientConfig))] +[JsonSerializable(typeof(McpOauthRequiredEvent))] [JsonSerializable(typeof(PendingMessagesModifiedData))] [JsonSerializable(typeof(PendingMessagesModifiedEvent))] [JsonSerializable(typeof(PermissionCompletedData))] @@ -3429,6 +3896,9 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SessionErrorData))] [JsonSerializable(typeof(SessionErrorEvent))] [JsonSerializable(typeof(SessionEvent))] +[JsonSerializable(typeof(SessionExtensionsLoadedData))] +[JsonSerializable(typeof(SessionExtensionsLoadedDataExtensionsItem))] +[JsonSerializable(typeof(SessionExtensionsLoadedEvent))] [JsonSerializable(typeof(SessionHandoffData))] [JsonSerializable(typeof(SessionHandoffDataRepository))] [JsonSerializable(typeof(SessionHandoffEvent))] @@ -3439,6 +3909,11 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SessionIdleEvent))] [JsonSerializable(typeof(SessionInfoData))] [JsonSerializable(typeof(SessionInfoEvent))] +[JsonSerializable(typeof(SessionMcpServerStatusChangedData))] +[JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))] +[JsonSerializable(typeof(SessionMcpServersLoadedData))] +[JsonSerializable(typeof(SessionMcpServersLoadedDataServersItem))] +[JsonSerializable(typeof(SessionMcpServersLoadedEvent))] [JsonSerializable(typeof(SessionModeChangedData))] [JsonSerializable(typeof(SessionModeChangedEvent))] [JsonSerializable(typeof(SessionModelChangeData))] @@ -3451,6 +3926,9 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SessionShutdownData))] [JsonSerializable(typeof(SessionShutdownDataCodeChanges))] [JsonSerializable(typeof(SessionShutdownEvent))] +[JsonSerializable(typeof(SessionSkillsLoadedData))] +[JsonSerializable(typeof(SessionSkillsLoadedDataSkillsItem))] +[JsonSerializable(typeof(SessionSkillsLoadedEvent))] [JsonSerializable(typeof(SessionSnapshotRewindData))] [JsonSerializable(typeof(SessionSnapshotRewindEvent))] [JsonSerializable(typeof(SessionStartData))] @@ -3488,6 +3966,7 @@ public enum PermissionCompletedDataResultKind [JsonSerializable(typeof(SystemNotificationData))] [JsonSerializable(typeof(SystemNotificationDataKind))] [JsonSerializable(typeof(SystemNotificationDataKindAgentCompleted))] +[JsonSerializable(typeof(SystemNotificationDataKindAgentIdle))] [JsonSerializable(typeof(SystemNotificationDataKindShellCompleted))] [JsonSerializable(typeof(SystemNotificationDataKindShellDetachedCompleted))] [JsonSerializable(typeof(SystemNotificationEvent))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 606c0b052d..0014ec7f07 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -749,6 +749,7 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de /// The message to log. /// Log level (default: info). /// When true, the message is not persisted to disk. + /// Optional URL to associate with the log entry. /// Optional cancellation token. /// /// @@ -758,9 +759,9 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de /// await session.LogAsync("Temporary status", ephemeral: true); /// /// - public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) + public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default) { - await Rpc.LogAsync(message, level, ephemeral, cancellationToken); + await Rpc.LogAsync(message, level, ephemeral, url, cancellationToken); } /// diff --git a/go/README.md b/go/README.md index 1d0665130d..8cbb382c32 100644 --- a/go/README.md +++ b/go/README.md @@ -201,7 +201,7 @@ _, err = session.Send(context.Background(), copilot.MessageOptions{ Prompt: "What's in this image?", Attachments: []copilot.Attachment{ { - Type: copilot.Blob, + Type: copilot.AttachmentTypeBlob, Data: &base64ImageData, MIMEType: &mimeType, }, diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 55eea011ea..fbdb1597fa 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -61,15 +61,12 @@ type SessionEvent struct { // // Current context window usage statistics including token and message counts // - // Empty payload; the event signals that LLM-powered conversation compaction has begun + // Context window breakdown at the start of LLM-powered conversation compaction // // Conversation compaction results including success status, metrics, and optional error // details // - // Task completion notification with optional summary from the agent - // - // User message content with optional attachments, source information, and interaction - // metadata + // Task completion notification with summary from the agent // // Empty payload; the event signals that the pending message queue has changed // @@ -135,18 +132,27 @@ type SessionEvent struct { // // User input request completion notification signaling UI dismissal // - // Structured form elicitation request with JSON schema definition for form fields + // Elicitation request; may be form-based (structured input) or URL-based (browser + // redirect) // // Elicitation request completion notification signaling UI dismissal // + // OAuth authentication request for an MCP server + // + // MCP OAuth request completion notification + // // External tool invocation request for client-side tool execution // // External tool completion notification signaling UI dismissal // // Queued slash command dispatch request for client execution // + // Registered command dispatch request routed to the owning client + // // Queued command completion notification signaling UI dismissal // + // SDK command registration change notification + // // Plan approval request with plan content and available user actions // // Plan mode exit completion notification signaling UI dismissal @@ -198,15 +204,12 @@ type SessionEvent struct { // // # Current context window usage statistics including token and message counts // -// Empty payload; the event signals that LLM-powered conversation compaction has begun +// # Context window breakdown at the start of LLM-powered conversation compaction // // Conversation compaction results including success status, metrics, and optional error // details // -// # Task completion notification with optional summary from the agent -// -// User message content with optional attachments, source information, and interaction -// metadata +// # Task completion notification with summary from the agent // // Empty payload; the event signals that the pending message queue has changed // @@ -272,18 +275,27 @@ type SessionEvent struct { // // # User input request completion notification signaling UI dismissal // -// # Structured form elicitation request with JSON schema definition for form fields +// Elicitation request; may be form-based (structured input) or URL-based (browser +// redirect) // // # Elicitation request completion notification signaling UI dismissal // +// # OAuth authentication request for an MCP server +// +// # MCP OAuth request completion notification +// // # External tool invocation request for client-side tool execution // // # External tool completion notification signaling UI dismissal // // # Queued slash command dispatch request for client execution // +// # Registered command dispatch request routed to the owning client +// // # Queued command completion notification signaling UI dismissal // +// # SDK command registration change notification +// // # Plan approval request with plan content and available user actions // // Plan mode exit completion notification signaling UI dismissal @@ -343,6 +355,14 @@ type Data struct { Stack *string `json:"stack,omitempty"` // HTTP status code from the upstream request, if applicable StatusCode *int64 `json:"statusCode,omitempty"` + // Optional URL associated with this error that the user can open in a browser + // + // Optional URL associated with this message that the user can open in a browser + // + // Optional URL associated with this warning that the user can open in a browser + // + // URL to open in the user's browser (url mode only) + URL *string `json:"url,omitempty"` // Background tasks still running when the agent became idle BackgroundTasks *BackgroundTasks `json:"backgroundTasks,omitempty"` // The new display title for the session @@ -383,7 +403,7 @@ type Data struct { SourceType *SourceType `json:"sourceType,omitempty"` // Summary of the work done in the source session // - // Optional summary of the completed task, provided by the agent + // Summary of the completed task, provided by the agent // // Summary of the plan that was created Summary *string `json:"summary,omitempty"` @@ -409,8 +429,20 @@ type Data struct { UpToEventID *string `json:"upToEventId,omitempty"` // Aggregate code change metrics for the session CodeChanges *CodeChanges `json:"codeChanges,omitempty"` + // Non-system message token count at shutdown + // + // Token count from non-system messages (user, assistant, tool) + // + // Token count from non-system messages (user, assistant, tool) at compaction start + // + // Token count from non-system messages (user, assistant, tool) after compaction + ConversationTokens *float64 `json:"conversationTokens,omitempty"` // Model that was selected at the time of shutdown CurrentModel *string `json:"currentModel,omitempty"` + // Total tokens in context window at shutdown + // + // Current number of tokens in the context window + CurrentTokens *float64 `json:"currentTokens,omitempty"` // Error description when shutdownType is "error" ErrorReason *string `json:"errorReason,omitempty"` // Per-model usage breakdown, keyed by model identifier @@ -419,6 +451,22 @@ type Data struct { SessionStartTime *float64 `json:"sessionStartTime,omitempty"` // Whether the session ended normally ("routine") or due to a crash/fatal error ("error") ShutdownType *ShutdownType `json:"shutdownType,omitempty"` + // System message token count at shutdown + // + // Token count from system message(s) + // + // Token count from system message(s) at compaction start + // + // Token count from system message(s) after compaction + SystemTokens *float64 `json:"systemTokens,omitempty"` + // Tool definitions token count at shutdown + // + // Token count from tool definitions + // + // Token count from tool definitions at compaction start + // + // Token count from tool definitions after compaction + ToolDefinitionsTokens *float64 `json:"toolDefinitionsTokens,omitempty"` // Cumulative time spent in API calls during the session, in milliseconds TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"` // Total number of premium API requests used during the session @@ -435,8 +483,8 @@ type Data struct { HeadCommit *string `json:"headCommit,omitempty"` // Hosting platform type of the repository (github or ado) HostType *HostType `json:"hostType,omitempty"` - // Current number of tokens in the context window - CurrentTokens *float64 `json:"currentTokens,omitempty"` + // Whether this is the first usage_info event emitted in this session + IsInitial *bool `json:"isInitial,omitempty"` // Current number of messages in the conversation MessagesLength *float64 `json:"messagesLength,omitempty"` // Checkpoint snapshot number created for recovery @@ -481,6 +529,11 @@ type Data struct { // Request ID of the resolved elicitation request; clients should dismiss any UI for this // request // + // Unique identifier for this OAuth request; used to respond via + // session.respondToMcpOAuth() + // + // Request ID of the resolved OAuth request + // // Unique identifier for this request; used to respond via session.respondToExternalTool() // // Request ID of the resolved external tool request; clients should dismiss any UI for this @@ -488,6 +541,8 @@ type Data struct { // // Unique identifier for this request; used to respond via session.respondToQueuedCommand() // + // Unique identifier; used to respond via session.commands.handlePendingCommand() + // // Request ID of the resolved command request; clients should dismiss any UI for this // request // @@ -498,6 +553,8 @@ type Data struct { RequestID *string `json:"requestId,omitempty"` // Whether compaction completed successfully // + // Whether the tool call succeeded. False when validation failed (e.g., invalid arguments) + // // Whether the tool execution completed successfully // // Whether the hook completed successfully @@ -530,9 +587,9 @@ type Data struct { // // CAPI interaction ID for correlating this tool execution with upstream telemetry InteractionID *string `json:"interactionId,omitempty"` - // Origin of this message, used for timeline filtering and telemetry (e.g., "user", - // "autopilot", "skill", or "command") - Source *Source `json:"source,omitempty"` + // Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected + // messages that should be hidden from the user) + Source *string `json:"source,omitempty"` // Transformed version of the message sent to the model, with XML wrapping, timestamps, and // other augmentations for prompt caching TransformedContent *string `json:"transformedContent,omitempty"` @@ -618,6 +675,12 @@ type Data struct { // // Tool call ID of the parent tool invocation that spawned this sub-agent // + // The LLM-assigned tool call ID that triggered this request; used by remote UIs to + // correlate responses + // + // Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id + // for remote UIs + // // Tool call ID assigned to this external tool invocation ToolCallID *string `json:"toolCallId,omitempty"` // Name of the tool the user wants to invoke @@ -690,22 +753,49 @@ type Data struct { Choices []string `json:"choices,omitempty"` // The question or prompt to present to the user Question *string `json:"question,omitempty"` - // Elicitation mode; currently only "form" is supported. Defaults to "form" when absent. + // The source that initiated the request (MCP server name, or absent for agent-initiated) + ElicitationSource *string `json:"elicitationSource,omitempty"` + // Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to + // "form" when absent. Mode *Mode `json:"mode,omitempty"` - // JSON Schema describing the form fields to present to the user + // JSON Schema describing the form fields to present to the user (form mode only) RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"` + // Display name of the MCP server that requires OAuth + // + // Name of the MCP server whose status changed + ServerName *string `json:"serverName,omitempty"` + // URL of the MCP server that requires OAuth + ServerURL *string `json:"serverUrl,omitempty"` + // Static OAuth client configuration, if the server specifies one + StaticClientConfig *StaticClientConfig `json:"staticClientConfig,omitempty"` // W3C Trace Context traceparent header for the execute_tool span Traceparent *string `json:"traceparent,omitempty"` // W3C Trace Context tracestate header for the execute_tool span Tracestate *string `json:"tracestate,omitempty"` // The slash command text to be executed (e.g., /help, /clear) + // + // The full command text (e.g., /deploy production) Command *string `json:"command,omitempty"` + // Raw argument string after the command name + Args *string `json:"args,omitempty"` + // Command name without leading / + CommandName *string `json:"commandName,omitempty"` + // Current list of registered SDK commands + Commands []DataCommand `json:"commands,omitempty"` // Available actions the user can take (e.g., approve, edit, reject) Actions []string `json:"actions,omitempty"` // Full content of the plan file PlanContent *string `json:"planContent,omitempty"` // The recommended action for the user to take RecommendedAction *string `json:"recommendedAction,omitempty"` + // Array of resolved skill metadata + Skills []Skill `json:"skills,omitempty"` + // Array of MCP server status summaries + Servers []Server `json:"servers,omitempty"` + // New connection status: connected, failed, pending, disabled, or not_configured + Status *ServerStatus `json:"status,omitempty"` + // Array of discovered extensions and their status + Extensions []Extension `json:"extensions,omitempty"` } // A user message attachment — a file, directory, code selection, blob, or GitHub reference @@ -822,6 +912,11 @@ type CodeChanges struct { LinesRemoved float64 `json:"linesRemoved"` } +type DataCommand struct { + Description *string `json:"description,omitempty"` + Name string `json:"name"` +} + // Token usage breakdown for the compaction LLM call type CompactionTokensUsed struct { // Cached input tokens reused in the compaction LLM call @@ -885,6 +980,17 @@ type ErrorClass struct { Stack *string `json:"stack,omitempty"` } +type Extension struct { + // Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper') + ID string `json:"id"` + // Extension name (directory name) + Name string `json:"name"` + // Discovery source + Source Source `json:"source"` + // Current status: running, disabled, failed, or starting + Status ExtensionStatus `json:"status"` +} + // Structured metadata identifying what triggered this notification type KindClass struct { // Unique identifier of the background agent @@ -898,8 +1004,8 @@ type KindClass struct { // The full prompt given to the background agent Prompt *string `json:"prompt,omitempty"` // Whether the agent completed successfully or failed - Status *Status `json:"status,omitempty"` - Type KindType `json:"type"` + Status *KindStatus `json:"status,omitempty"` + Type KindType `json:"type"` // Exit code of the shell command, if available ExitCode *float64 `json:"exitCode,omitempty"` // Unique identifier of the shell session @@ -964,7 +1070,7 @@ type PermissionRequest struct { // Whether the UI can offer session-wide approval for this command pattern CanOfferSessionApproval *bool `json:"canOfferSessionApproval,omitempty"` // Parsed command identifiers found in the command text - Commands []CommandElement `json:"commands,omitempty"` + Commands []PermissionRequestCommand `json:"commands,omitempty"` // The complete shell command text to be executed FullCommandText *string `json:"fullCommandText,omitempty"` // Whether the command includes a file write redirection (e.g., > or >>) @@ -1027,7 +1133,7 @@ type PermissionRequest struct { ToolArgs interface{} `json:"toolArgs"` } -type CommandElement struct { +type PermissionRequestCommand struct { // Command identifier (e.g., executable name) Identifier string `json:"identifier"` // Whether this command is read-only (no side effects) @@ -1068,7 +1174,7 @@ type RepositoryClass struct { Owner string `json:"owner"` } -// JSON Schema describing the form fields to present to the user +// JSON Schema describing the form fields to present to the user (form mode only) type RequestedSchema struct { // Form field definitions, keyed by field name Properties map[string]interface{} `json:"properties"` @@ -1172,6 +1278,40 @@ type ResourceClass struct { Blob *string `json:"blob,omitempty"` } +type Server struct { + // Error message if the server failed to connect + Error *string `json:"error,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration source: user, workspace, plugin, or builtin + Source *string `json:"source,omitempty"` + // Connection status: connected, failed, pending, disabled, or not_configured + Status ServerStatus `json:"status"` +} + +type Skill struct { + // Description of what the skill does + Description string `json:"description"` + // Whether the skill is currently enabled + Enabled bool `json:"enabled"` + // Unique identifier for the skill + Name string `json:"name"` + // Absolute path to the skill file, if available + Path *string `json:"path,omitempty"` + // Source location type of the skill (e.g., project, personal, plugin) + Source string `json:"source"` + // Whether the skill can be invoked by the user as a slash command + UserInvocable bool `json:"userInvocable"` +} + +// Static OAuth client configuration, if the server specifies one +type StaticClientConfig struct { + // OAuth client ID for the server + ClientID string `json:"clientId"` + // Whether this is a public OAuth client + PublicClient *bool `json:"publicClient,omitempty"` +} + // A tool invocation request from the assistant type ToolRequest struct { // Arguments to pass to the tool, format depends on the tool @@ -1193,59 +1333,81 @@ type ToolRequest struct { type AgentMode string const ( - AgentModeAutopilot AgentMode = "autopilot" - AgentModeShell AgentMode = "shell" - Interactive AgentMode = "interactive" - Plan AgentMode = "plan" + AgentModeShell AgentMode = "shell" + AgentModeAutopilot AgentMode = "autopilot" + AgentModeInteractive AgentMode = "interactive" + AgentModePlan AgentMode = "plan" ) // Type of GitHub reference type ReferenceType string const ( - Discussion ReferenceType = "discussion" - Issue ReferenceType = "issue" - PR ReferenceType = "pr" + ReferenceTypeDiscussion ReferenceType = "discussion" + ReferenceTypeIssue ReferenceType = "issue" + ReferenceTypePr ReferenceType = "pr" ) type AttachmentType string const ( - Blob AttachmentType = "blob" - Directory AttachmentType = "directory" - File AttachmentType = "file" - GithubReference AttachmentType = "github_reference" - Selection AttachmentType = "selection" + AttachmentTypeBlob AttachmentType = "blob" + AttachmentTypeDirectory AttachmentType = "directory" + AttachmentTypeFile AttachmentType = "file" + AttachmentTypeGithubReference AttachmentType = "github_reference" + AttachmentTypeSelection AttachmentType = "selection" ) // Hosting platform type of the repository (github or ado) type HostType string const ( - ADO HostType = "ado" - Github HostType = "github" + HostTypeAdo HostType = "ado" + HostTypeGithub HostType = "github" +) + +// Discovery source +type Source string + +const ( + SourceProject Source = "project" + SourceUser Source = "user" +) + +// Current status: running, disabled, failed, or starting +type ExtensionStatus string + +const ( + ExtensionStatusDisabled ExtensionStatus = "disabled" + ExtensionStatusFailed ExtensionStatus = "failed" + ExtensionStatusRunning ExtensionStatus = "running" + ExtensionStatusStarting ExtensionStatus = "starting" ) // Whether the agent completed successfully or failed -type Status string +type KindStatus string const ( - Completed Status = "completed" - Failed Status = "failed" + KindStatusCompleted KindStatus = "completed" + KindStatusFailed KindStatus = "failed" ) type KindType string const ( - AgentCompleted KindType = "agent_completed" - ShellCompleted KindType = "shell_completed" - ShellDetachedCompleted KindType = "shell_detached_completed" + KindTypeAgentCompleted KindType = "agent_completed" + KindTypeAgentIdle KindType = "agent_idle" + KindTypeShellCompleted KindType = "shell_completed" + KindTypeShellDetachedCompleted KindType = "shell_detached_completed" ) +// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to +// "form" when absent. type Mode string const ( - Form Mode = "form" + ModeForm Mode = "form" + ModeURL Mode = "url" ) // The type of operation performed on the plan file @@ -1254,99 +1416,95 @@ const ( type Operation string const ( - Create Operation = "create" - Delete Operation = "delete" - Update Operation = "update" + OperationCreate Operation = "create" + OperationDelete Operation = "delete" + OperationUpdate Operation = "update" ) type PermissionRequestKind string const ( - CustomTool PermissionRequestKind = "custom-tool" - Hook PermissionRequestKind = "hook" - KindShell PermissionRequestKind = "shell" - MCP PermissionRequestKind = "mcp" - Memory PermissionRequestKind = "memory" - Read PermissionRequestKind = "read" - URL PermissionRequestKind = "url" - Write PermissionRequestKind = "write" + PermissionRequestKindCustomTool PermissionRequestKind = "custom-tool" + PermissionRequestKindHook PermissionRequestKind = "hook" + PermissionRequestKindShell PermissionRequestKind = "shell" + PermissionRequestKindURL PermissionRequestKind = "url" + PermissionRequestKindMcp PermissionRequestKind = "mcp" + PermissionRequestKindMemory PermissionRequestKind = "memory" + PermissionRequestKindRead PermissionRequestKind = "read" + PermissionRequestKindWrite PermissionRequestKind = "write" ) type RequestedSchemaType string const ( - Object RequestedSchemaType = "object" + RequestedSchemaTypeObject RequestedSchemaType = "object" ) // Theme variant this icon is intended for type Theme string const ( - Dark Theme = "dark" - Light Theme = "light" + ThemeDark Theme = "dark" + ThemeLight Theme = "light" ) type ContentType string const ( - Audio ContentType = "audio" - Image ContentType = "image" - Resource ContentType = "resource" - ResourceLink ContentType = "resource_link" - Terminal ContentType = "terminal" - Text ContentType = "text" + ContentTypeAudio ContentType = "audio" + ContentTypeImage ContentType = "image" + ContentTypeResource ContentType = "resource" + ContentTypeResourceLink ContentType = "resource_link" + ContentTypeTerminal ContentType = "terminal" + ContentTypeText ContentType = "text" ) // The outcome of the permission request type ResultKind string const ( - Approved ResultKind = "approved" - DeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy" - DeniedByRules ResultKind = "denied-by-rules" - DeniedInteractivelyByUser ResultKind = "denied-interactively-by-user" - DeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user" + ResultKindApproved ResultKind = "approved" + ResultKindDeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy" + ResultKindDeniedByRules ResultKind = "denied-by-rules" + ResultKindDeniedInteractivelyByUser ResultKind = "denied-interactively-by-user" + ResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user" ) // Message role: "system" for system prompts, "developer" for developer-injected instructions type Role string const ( - Developer Role = "developer" - RoleSystem Role = "system" + RoleDeveloper Role = "developer" + RoleSystem Role = "system" ) -// Whether the session ended normally ("routine") or due to a crash/fatal error ("error") -type ShutdownType string +// Connection status: connected, failed, pending, disabled, or not_configured +// +// New connection status: connected, failed, pending, disabled, or not_configured +type ServerStatus string const ( - Error ShutdownType = "error" - Routine ShutdownType = "routine" + ServerStatusConnected ServerStatus = "connected" + ServerStatusDisabled ServerStatus = "disabled" + ServerStatusNotConfigured ServerStatus = "not_configured" + ServerStatusPending ServerStatus = "pending" + ServerStatusFailed ServerStatus = "failed" ) -// Origin of this message, used for timeline filtering and telemetry (e.g., "user", -// "autopilot", "skill", or "command") -type Source string +// Whether the session ended normally ("routine") or due to a crash/fatal error ("error") +type ShutdownType string const ( - Command Source = "command" - ImmediatePrompt Source = "immediate-prompt" - JITInstruction Source = "jit-instruction" - Other Source = "other" - Skill Source = "skill" - SnippyBlocking Source = "snippy-blocking" - SourceAutopilot Source = "autopilot" - SourceSystem Source = "system" - ThinkingExhaustedContinuation Source = "thinking-exhausted-continuation" - User Source = "user" + ShutdownTypeError ShutdownType = "error" + ShutdownTypeRoutine ShutdownType = "routine" ) // Origin type of the session being handed off type SourceType string const ( - Local SourceType = "local" - Remote SourceType = "remote" + SourceTypeLocal SourceType = "local" + SourceTypeRemote SourceType = "remote" ) // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool @@ -1354,74 +1512,82 @@ const ( type ToolRequestType string const ( - Custom ToolRequestType = "custom" - Function ToolRequestType = "function" + ToolRequestTypeCustom ToolRequestType = "custom" + ToolRequestTypeFunction ToolRequestType = "function" ) type SessionEventType string const ( - Abort SessionEventType = "abort" - AssistantIntent SessionEventType = "assistant.intent" - AssistantMessage SessionEventType = "assistant.message" - AssistantMessageDelta SessionEventType = "assistant.message_delta" - AssistantReasoning SessionEventType = "assistant.reasoning" - AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" - AssistantStreamingDelta SessionEventType = "assistant.streaming_delta" - AssistantTurnEnd SessionEventType = "assistant.turn_end" - AssistantTurnStart SessionEventType = "assistant.turn_start" - AssistantUsage SessionEventType = "assistant.usage" - CommandCompleted SessionEventType = "command.completed" - CommandQueued SessionEventType = "command.queued" - ElicitationCompleted SessionEventType = "elicitation.completed" - ElicitationRequested SessionEventType = "elicitation.requested" - ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" - ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" - ExternalToolCompleted SessionEventType = "external_tool.completed" - ExternalToolRequested SessionEventType = "external_tool.requested" - HookEnd SessionEventType = "hook.end" - HookStart SessionEventType = "hook.start" - PendingMessagesModified SessionEventType = "pending_messages.modified" - PermissionCompleted SessionEventType = "permission.completed" - PermissionRequested SessionEventType = "permission.requested" - SessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" - SessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionCompactionStart SessionEventType = "session.compaction_start" - SessionContextChanged SessionEventType = "session.context_changed" - SessionError SessionEventType = "session.error" - SessionHandoff SessionEventType = "session.handoff" - SessionIdle SessionEventType = "session.idle" - SessionInfo SessionEventType = "session.info" - SessionModeChanged SessionEventType = "session.mode_changed" - SessionModelChange SessionEventType = "session.model_change" - SessionPlanChanged SessionEventType = "session.plan_changed" - SessionResume SessionEventType = "session.resume" - SessionShutdown SessionEventType = "session.shutdown" - SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" - SessionStart SessionEventType = "session.start" - SessionTaskComplete SessionEventType = "session.task_complete" - SessionTitleChanged SessionEventType = "session.title_changed" - SessionToolsUpdated SessionEventType = "session.tools_updated" - SessionTruncation SessionEventType = "session.truncation" - SessionUsageInfo SessionEventType = "session.usage_info" - SessionWarning SessionEventType = "session.warning" - SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" - SkillInvoked SessionEventType = "skill.invoked" - SubagentCompleted SessionEventType = "subagent.completed" - SubagentDeselected SessionEventType = "subagent.deselected" - SubagentFailed SessionEventType = "subagent.failed" - SubagentSelected SessionEventType = "subagent.selected" - SubagentStarted SessionEventType = "subagent.started" - SystemMessage SessionEventType = "system.message" - SystemNotification SessionEventType = "system.notification" - ToolExecutionComplete SessionEventType = "tool.execution_complete" - ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" - ToolExecutionProgress SessionEventType = "tool.execution_progress" - ToolExecutionStart SessionEventType = "tool.execution_start" - ToolUserRequested SessionEventType = "tool.user_requested" - UserInputCompleted SessionEventType = "user_input.completed" - UserInputRequested SessionEventType = "user_input.requested" - UserMessage SessionEventType = "user.message" + SessionEventTypeAbort SessionEventType = "abort" + SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" + SessionEventTypeAssistantMessage SessionEventType = "assistant.message" + SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" + SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning" + SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" + SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta" + SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" + SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start" + SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" + SessionEventTypeCommandCompleted SessionEventType = "command.completed" + SessionEventTypeCommandExecute SessionEventType = "command.execute" + SessionEventTypeCommandQueued SessionEventType = "command.queued" + SessionEventTypeCommandsChanged SessionEventType = "commands.changed" + SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed" + SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested" + SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" + SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" + SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed" + SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" + SessionEventTypeHookEnd SessionEventType = "hook.end" + SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypeMcpOauthCompleted SessionEventType = "mcp.oauth_completed" + SessionEventTypeMcpOauthRequired SessionEventType = "mcp.oauth_required" + SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" + SessionEventTypePermissionCompleted SessionEventType = "permission.completed" + SessionEventTypePermissionRequested SessionEventType = "permission.requested" + SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" + SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" + SessionEventTypeSessionError SessionEventType = "session.error" + SessionEventTypeSessionExtensionsLoaded SessionEventType = "session.extensions_loaded" + SessionEventTypeSessionHandoff SessionEventType = "session.handoff" + SessionEventTypeSessionIdle SessionEventType = "session.idle" + SessionEventTypeSessionInfo SessionEventType = "session.info" + SessionEventTypeSessionMcpServerStatusChanged SessionEventType = "session.mcp_server_status_changed" + SessionEventTypeSessionMcpServersLoaded SessionEventType = "session.mcp_servers_loaded" + SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" + SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + SessionEventTypeSessionResume SessionEventType = "session.resume" + SessionEventTypeSessionShutdown SessionEventType = "session.shutdown" + SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded" + SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind" + SessionEventTypeSessionStart SessionEventType = "session.start" + SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete" + SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed" + SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated" + SessionEventTypeSessionTruncation SessionEventType = "session.truncation" + SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info" + SessionEventTypeSessionWarning SessionEventType = "session.warning" + SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" + SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" + SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" + SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" + SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" + SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed" + SessionEventTypeUserInputRequested SessionEventType = "user_input.requested" + SessionEventTypeUserMessage SessionEventType = "user.message" ) type ContextUnion struct { diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go index 338f4da674..cbd52a326e 100644 --- a/go/internal/e2e/agent_and_compact_rpc_test.go +++ b/go/internal/e2e/agent_and_compact_rpc_test.go @@ -215,7 +215,7 @@ func TestAgentSelectionRpc(t *testing.T) { } }) - t.Run("should return empty list when no custom agents configured", func(t *testing.T) { + t.Run("should return no custom agents when none configured", func(t *testing.T) { client := copilot.NewClient(&copilot.ClientOptions{ CLIPath: cliPath, UseStdio: copilot.Bool(true), @@ -238,8 +238,13 @@ func TestAgentSelectionRpc(t *testing.T) { t.Fatalf("Failed to list agents: %v", err) } - if len(result.Agents) != 0 { - t.Errorf("Expected empty agent list, got %d agents", len(result.Agents)) + // The CLI may return built-in/default agents even when no custom agents + // are configured, so just verify none of the known custom agent names appear. + customNames := map[string]bool{"test-agent": true, "another-agent": true} + for _, agent := range result.Agents { + if customNames[agent.Name] { + t.Errorf("Expected no custom agents, but found %q", agent.Name) + } } if err := client.Stop(); err != nil { diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go index aee80704dc..888ab2aa95 100644 --- a/go/internal/e2e/compaction_test.go +++ b/go/internal/e2e/compaction_test.go @@ -36,10 +36,10 @@ func TestCompaction(t *testing.T) { var compactionCompleteEvents []copilot.SessionEvent session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionCompactionStart { + if event.Type == copilot.SessionEventTypeSessionCompactionStart { compactionStartEvents = append(compactionStartEvents, event) } - if event.Type == copilot.SessionCompactionComplete { + if event.Type == copilot.SessionEventTypeSessionCompactionComplete { compactionCompleteEvents = append(compactionCompleteEvents, event) } }) @@ -105,7 +105,7 @@ func TestCompaction(t *testing.T) { var compactionEvents []copilot.SessionEvent session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionCompactionStart || event.Type == copilot.SessionCompactionComplete { + if event.Type == copilot.SessionEventTypeSessionCompactionStart || event.Type == copilot.SessionEventTypeSessionCompactionComplete { compactionEvents = append(compactionEvents, event) } }) diff --git a/go/internal/e2e/multi_client_test.go b/go/internal/e2e/multi_client_test.go index 9571ab58ed..3c7dc34c3d 100644 --- a/go/internal/e2e/multi_client_test.go +++ b/go/internal/e2e/multi_client_test.go @@ -79,13 +79,13 @@ func TestMultiClient(t *testing.T) { client2Completed := make(chan struct{}, 1) session1.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ExternalToolRequested { + if event.Type == copilot.SessionEventTypeExternalToolRequested { select { case client1Requested <- struct{}{}: default: } } - if event.Type == copilot.ExternalToolCompleted { + if event.Type == copilot.SessionEventTypeExternalToolCompleted { select { case client1Completed <- struct{}{}: default: @@ -93,13 +93,13 @@ func TestMultiClient(t *testing.T) { } }) session2.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ExternalToolRequested { + if event.Type == copilot.SessionEventTypeExternalToolRequested { select { case client2Requested <- struct{}{}: default: } } - if event.Type == copilot.ExternalToolCompleted { + if event.Type == copilot.SessionEventTypeExternalToolCompleted { select { case client2Completed <- struct{}{}: default: @@ -120,7 +120,7 @@ func TestMultiClient(t *testing.T) { } // Wait for all broadcast events to arrive on both clients - timeout := time.After(10 * time.Second) + timeout := time.After(30 * time.Second) for _, ch := range []chan struct{}{client1Requested, client2Requested, client1Completed, client2Completed} { select { case <-ch: @@ -197,10 +197,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.requested events mu1.Lock() - c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested) + c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested) mu1.Unlock() mu2.Lock() - c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested) + c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested) mu2.Unlock() if len(c1PermRequested) == 0 { @@ -212,10 +212,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.completed events with approved result mu1.Lock() - c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted) + c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted) mu1.Unlock() mu2.Lock() - c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted) + c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted) mu2.Unlock() if len(c1PermCompleted) == 0 { @@ -293,10 +293,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.requested events mu1.Lock() - c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested) + c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested) mu1.Unlock() mu2.Lock() - c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested) + c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested) mu2.Unlock() if len(c1PermRequested) == 0 { @@ -308,10 +308,10 @@ func TestMultiClient(t *testing.T) { // Both clients should see the denial in the completed event mu1.Lock() - c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted) + c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted) mu1.Unlock() mu2.Lock() - c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted) + c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted) mu2.Unlock() if len(c1PermCompleted) == 0 { diff --git a/go/internal/e2e/permissions_test.go b/go/internal/e2e/permissions_test.go index 328e7e7884..98f6200435 100644 --- a/go/internal/e2e/permissions_test.go +++ b/go/internal/e2e/permissions_test.go @@ -173,7 +173,7 @@ func TestPermissions(t *testing.T) { permissionDenied := false session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ToolExecutionComplete && + if event.Type == copilot.SessionEventTypeToolExecutionComplete && event.Data.Success != nil && !*event.Data.Success && event.Data.Error != nil && event.Data.Error.ErrorClass != nil && strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") { @@ -223,7 +223,7 @@ func TestPermissions(t *testing.T) { permissionDenied := false session2.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ToolExecutionComplete && + if event.Type == copilot.SessionEventTypeToolExecutionComplete && event.Data.Success != nil && !*event.Data.Success && event.Data.Error != nil && event.Data.Error.ErrorClass != nil && strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") { diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index ebcbe1130d..3d69b97ad7 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -219,16 +219,16 @@ func TestSessionRpc(t *testing.T) { if err != nil { t.Fatalf("Failed to get mode: %v", err) } - if initial.Mode != rpc.Interactive { + if initial.Mode != rpc.ModeInteractive { t.Errorf("Expected initial mode 'interactive', got %q", initial.Mode) } // Switch to plan mode - planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Plan}) + planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModePlan}) if err != nil { t.Fatalf("Failed to set mode to plan: %v", err) } - if planResult.Mode != rpc.Plan { + if planResult.Mode != rpc.ModePlan { t.Errorf("Expected mode 'plan', got %q", planResult.Mode) } @@ -237,16 +237,16 @@ func TestSessionRpc(t *testing.T) { if err != nil { t.Fatalf("Failed to get mode after plan: %v", err) } - if afterPlan.Mode != rpc.Plan { + if afterPlan.Mode != rpc.ModePlan { t.Errorf("Expected mode 'plan' after set, got %q", afterPlan.Mode) } // Switch back to interactive - interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Interactive}) + interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModeInteractive}) if err != nil { t.Fatalf("Failed to set mode to interactive: %v", err) } - if interactiveResult.Mode != rpc.Interactive { + if interactiveResult.Mode != rpc.ModeInteractive { t.Errorf("Expected mode 'interactive', got %q", interactiveResult.Mode) } }) diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 052ae15809..1eaeacd1ef 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -506,7 +506,7 @@ func TestSession(t *testing.T) { toolStartCh := make(chan *copilot.SessionEvent, 1) toolStartErrCh := make(chan error, 1) go func() { - evt, err := testharness.GetNextEventOfType(session, copilot.ToolExecutionStart, 60*time.Second) + evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeToolExecutionStart, 60*time.Second) if err != nil { toolStartErrCh <- err } else { @@ -517,7 +517,7 @@ func TestSession(t *testing.T) { sessionIdleCh := make(chan *copilot.SessionEvent, 1) sessionIdleErrCh := make(chan error, 1) go func() { - evt, err := testharness.GetNextEventOfType(session, copilot.SessionIdle, 60*time.Second) + evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeSessionIdle, 60*time.Second) if err != nil { sessionIdleErrCh <- err } else { @@ -565,7 +565,7 @@ func TestSession(t *testing.T) { // Verify messages contain an abort event hasAbortEvent := false for _, msg := range messages { - if msg.Type == copilot.Abort { + if msg.Type == copilot.SessionEventTypeAbort { hasAbortEvent = true break } @@ -913,7 +913,7 @@ func TestSetModelWithReasoningEffort(t *testing.T) { modelChanged := make(chan copilot.SessionEvent, 1) session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionModelChange { + if event.Type == copilot.SessionEventTypeSessionModelChange { select { case modelChanged <- event: default: @@ -964,7 +964,7 @@ func TestSessionBlobAttachment(t *testing.T) { Prompt: "Describe this image", Attachments: []copilot.Attachment{ { - Type: copilot.Blob, + Type: copilot.AttachmentTypeBlob, Data: &data, MIMEType: &mimeType, DisplayName: &displayName, @@ -1028,7 +1028,7 @@ func TestSessionLog(t *testing.T) { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Info message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Info message", 5*time.Second) if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) } @@ -1038,11 +1038,11 @@ func TestSessionLog(t *testing.T) { }) t.Run("should log warning message", func(t *testing.T) { - if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.Warning}); err != nil { + if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.LevelWarning}); err != nil { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionWarning, "Warning message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionWarning, "Warning message", 5*time.Second) if evt.Data.WarningType == nil || *evt.Data.WarningType != "notification" { t.Errorf("Expected warningType 'notification', got %v", evt.Data.WarningType) } @@ -1052,11 +1052,11 @@ func TestSessionLog(t *testing.T) { }) t.Run("should log error message", func(t *testing.T) { - if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.Error}); err != nil { + if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.LevelError}); err != nil { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionError, "Error message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionError, "Error message", 5*time.Second) if evt.Data.ErrorType == nil || *evt.Data.ErrorType != "notification" { t.Errorf("Expected errorType 'notification', got %v", evt.Data.ErrorType) } @@ -1070,7 +1070,7 @@ func TestSessionLog(t *testing.T) { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Ephemeral message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Ephemeral message", 5*time.Second) if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) } diff --git a/go/internal/e2e/testharness/helper.go b/go/internal/e2e/testharness/helper.go index 05947c8061..3b521f3303 100644 --- a/go/internal/e2e/testharness/helper.go +++ b/go/internal/e2e/testharness/helper.go @@ -67,7 +67,7 @@ func GetNextEventOfType(session *copilot.Session, eventType copilot.SessionEvent case result <- &event: default: } - case copilot.SessionError: + case copilot.SessionEventTypeSessionError: msg := "session error" if event.Data.Message != nil { msg = *event.Data.Message diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 401f38305f..b9ba408b53 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -223,10 +223,10 @@ type SessionFleetStartParams struct { // Experimental: SessionAgentListResult is part of an experimental API and may change or be removed. type SessionAgentListResult struct { // Available custom agents - Agents []AgentElement `json:"agents"` + Agents []SessionAgentListResultAgent `json:"agents"` } -type AgentElement struct { +type SessionAgentListResultAgent struct { // Description of the agent's purpose Description string `json:"description"` // Human-readable display name @@ -276,6 +276,161 @@ type SessionAgentSelectParams struct { type SessionAgentDeselectResult struct { } +// Experimental: SessionAgentReloadResult is part of an experimental API and may change or be removed. +type SessionAgentReloadResult struct { + // Reloaded custom agents + Agents []SessionAgentReloadResultAgent `json:"agents"` +} + +type SessionAgentReloadResultAgent struct { + // Description of the agent's purpose + Description string `json:"description"` + // Human-readable display name + DisplayName string `json:"displayName"` + // Unique identifier of the custom agent + Name string `json:"name"` +} + +// Experimental: SessionSkillsListResult is part of an experimental API and may change or be removed. +type SessionSkillsListResult struct { + // Available skills + Skills []Skill `json:"skills"` +} + +type Skill struct { + // Description of what the skill does + Description string `json:"description"` + // Whether the skill is currently enabled + Enabled bool `json:"enabled"` + // Unique identifier for the skill + Name string `json:"name"` + // Absolute path to the skill file + Path *string `json:"path,omitempty"` + // Source location type (e.g., project, personal, plugin) + Source string `json:"source"` + // Whether the skill can be invoked by the user as a slash command + UserInvocable bool `json:"userInvocable"` +} + +// Experimental: SessionSkillsEnableResult is part of an experimental API and may change or be removed. +type SessionSkillsEnableResult struct { +} + +// Experimental: SessionSkillsEnableParams is part of an experimental API and may change or be removed. +type SessionSkillsEnableParams struct { + // Name of the skill to enable + Name string `json:"name"` +} + +// Experimental: SessionSkillsDisableResult is part of an experimental API and may change or be removed. +type SessionSkillsDisableResult struct { +} + +// Experimental: SessionSkillsDisableParams is part of an experimental API and may change or be removed. +type SessionSkillsDisableParams struct { + // Name of the skill to disable + Name string `json:"name"` +} + +// Experimental: SessionSkillsReloadResult is part of an experimental API and may change or be removed. +type SessionSkillsReloadResult struct { +} + +type SessionMCPListResult struct { + // Configured MCP servers + Servers []Server `json:"servers"` +} + +type Server struct { + // Error message if the server failed to connect + Error *string `json:"error,omitempty"` + // Server name (config key) + Name string `json:"name"` + // Configuration source: user, workspace, plugin, or builtin + Source *string `json:"source,omitempty"` + // Connection status: connected, failed, pending, disabled, or not_configured + Status ServerStatus `json:"status"` +} + +type SessionMCPEnableResult struct { +} + +type SessionMCPEnableParams struct { + // Name of the MCP server to enable + ServerName string `json:"serverName"` +} + +type SessionMCPDisableResult struct { +} + +type SessionMCPDisableParams struct { + // Name of the MCP server to disable + ServerName string `json:"serverName"` +} + +type SessionMCPReloadResult struct { +} + +// Experimental: SessionPluginsListResult is part of an experimental API and may change or be removed. +type SessionPluginsListResult struct { + // Installed plugins + Plugins []Plugin `json:"plugins"` +} + +type Plugin struct { + // Whether the plugin is currently enabled + Enabled bool `json:"enabled"` + // Marketplace the plugin came from + Marketplace string `json:"marketplace"` + // Plugin name + Name string `json:"name"` + // Installed version + Version *string `json:"version,omitempty"` +} + +// Experimental: SessionExtensionsListResult is part of an experimental API and may change or be removed. +type SessionExtensionsListResult struct { + // Discovered extensions and their current status + Extensions []Extension `json:"extensions"` +} + +type Extension struct { + // Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper') + ID string `json:"id"` + // Extension name (directory name) + Name string `json:"name"` + // Process ID if the extension is running + PID *int64 `json:"pid,omitempty"` + // Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) + Source Source `json:"source"` + // Current status: running, disabled, failed, or starting + Status ExtensionStatus `json:"status"` +} + +// Experimental: SessionExtensionsEnableResult is part of an experimental API and may change or be removed. +type SessionExtensionsEnableResult struct { +} + +// Experimental: SessionExtensionsEnableParams is part of an experimental API and may change or be removed. +type SessionExtensionsEnableParams struct { + // Source-qualified extension ID to enable + ID string `json:"id"` +} + +// Experimental: SessionExtensionsDisableResult is part of an experimental API and may change or be removed. +type SessionExtensionsDisableResult struct { +} + +// Experimental: SessionExtensionsDisableParams is part of an experimental API and may change or be removed. +type SessionExtensionsDisableParams struct { + // Source-qualified extension ID to disable + ID string `json:"id"` +} + +// Experimental: SessionExtensionsReloadResult is part of an experimental API and may change or be removed. +type SessionExtensionsReloadResult struct { +} + // Experimental: SessionCompactionCompactResult is part of an experimental API and may change or be removed. type SessionCompactionCompactResult struct { // Number of messages removed during compaction @@ -304,6 +459,75 @@ type ResultResult struct { ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"` } +type SessionCommandsHandlePendingCommandResult struct { + Success bool `json:"success"` +} + +type SessionCommandsHandlePendingCommandParams struct { + // Error message if the command handler failed + Error *string `json:"error,omitempty"` + // Request ID from the command invocation event + RequestID string `json:"requestId"` +} + +type SessionUIElicitationResult struct { + // The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + Action Action `json:"action"` + // The form values submitted by the user (present when action is 'accept') + Content map[string]*Content `json:"content,omitempty"` +} + +type SessionUIElicitationParams struct { + // Message describing what information is needed from the user + Message string `json:"message"` + // JSON Schema describing the form fields to present to the user + RequestedSchema RequestedSchema `json:"requestedSchema"` +} + +// JSON Schema describing the form fields to present to the user +type RequestedSchema struct { + // Form field definitions, keyed by field name + Properties map[string]Property `json:"properties"` + // List of required field names + Required []string `json:"required,omitempty"` + // Schema type indicator (always 'object') + Type RequestedSchemaType `json:"type"` +} + +type Property struct { + Default *Content `json:"default"` + Description *string `json:"description,omitempty"` + Enum []string `json:"enum,omitempty"` + EnumNames []string `json:"enumNames,omitempty"` + Title *string `json:"title,omitempty"` + Type PropertyType `json:"type"` + OneOf []OneOf `json:"oneOf,omitempty"` + Items *Items `json:"items,omitempty"` + MaxItems *float64 `json:"maxItems,omitempty"` + MinItems *float64 `json:"minItems,omitempty"` + Format *Format `json:"format,omitempty"` + MaxLength *float64 `json:"maxLength,omitempty"` + MinLength *float64 `json:"minLength,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` +} + +type Items struct { + Enum []string `json:"enum,omitempty"` + Type *ItemsType `json:"type,omitempty"` + AnyOf []AnyOf `json:"anyOf,omitempty"` +} + +type AnyOf struct { + Const string `json:"const"` + Title string `json:"title"` +} + +type OneOf struct { + Const string `json:"const"` + Title string `json:"title"` +} + type SessionPermissionsHandlePendingPermissionRequestResult struct { // Whether the permission request was handled successfully Success bool `json:"success"` @@ -335,6 +559,8 @@ type SessionLogParams struct { Level *Level `json:"level,omitempty"` // Human-readable message Message string `json:"message"` + // Optional URL the user can open in their browser for more details + URL *string `json:"url,omitempty"` } type SessionShellExecResult struct { @@ -371,19 +597,88 @@ type SessionShellKillParams struct { type Mode string const ( - Autopilot Mode = "autopilot" - Interactive Mode = "interactive" - Plan Mode = "plan" + ModeAutopilot Mode = "autopilot" + ModeInteractive Mode = "interactive" + ModePlan Mode = "plan" +) + +// Connection status: connected, failed, pending, disabled, or not_configured +type ServerStatus string + +const ( + ServerStatusConnected ServerStatus = "connected" + ServerStatusNotConfigured ServerStatus = "not_configured" + ServerStatusPending ServerStatus = "pending" + ServerStatusDisabled ServerStatus = "disabled" + ServerStatusFailed ServerStatus = "failed" +) + +// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) +type Source string + +const ( + SourceProject Source = "project" + SourceUser Source = "user" +) + +// Current status: running, disabled, failed, or starting +type ExtensionStatus string + +const ( + ExtensionStatusDisabled ExtensionStatus = "disabled" + ExtensionStatusFailed ExtensionStatus = "failed" + ExtensionStatusRunning ExtensionStatus = "running" + ExtensionStatusStarting ExtensionStatus = "starting" +) + +// The user's response: accept (submitted), decline (rejected), or cancel (dismissed) +type Action string + +const ( + ActionAccept Action = "accept" + ActionCancel Action = "cancel" + ActionDecline Action = "decline" +) + +type Format string + +const ( + FormatDate Format = "date" + FormatDateTime Format = "date-time" + FormatEmail Format = "email" + FormatUri Format = "uri" +) + +type ItemsType string + +const ( + ItemsTypeString ItemsType = "string" +) + +type PropertyType string + +const ( + PropertyTypeArray PropertyType = "array" + PropertyTypeBoolean PropertyType = "boolean" + PropertyTypeString PropertyType = "string" + PropertyTypeInteger PropertyType = "integer" + PropertyTypeNumber PropertyType = "number" +) + +type RequestedSchemaType string + +const ( + RequestedSchemaTypeObject RequestedSchemaType = "object" ) type Kind string const ( - Approved Kind = "approved" - DeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy" - DeniedByRules Kind = "denied-by-rules" - DeniedInteractivelyByUser Kind = "denied-interactively-by-user" - DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" + KindApproved Kind = "approved" + KindDeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy" + KindDeniedByRules Kind = "denied-by-rules" + KindDeniedInteractivelyByUser Kind = "denied-interactively-by-user" + KindDeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" ) // Log severity level. Determines how the message is displayed in the timeline. Defaults to @@ -391,18 +686,18 @@ const ( type Level string const ( - Error Level = "error" - Info Level = "info" - Warning Level = "warning" + LevelError Level = "error" + LevelInfo Level = "info" + LevelWarning Level = "warning" ) // Signal to send (default: SIGTERM) type Signal string const ( - Sigint Signal = "SIGINT" - Sigkill Signal = "SIGKILL" - Sigterm Signal = "SIGTERM" + SignalSIGINT Signal = "SIGINT" + SignalSIGKILL Signal = "SIGKILL" + SignalSIGTERM Signal = "SIGTERM" ) type ResultUnion struct { @@ -410,6 +705,13 @@ type ResultUnion struct { String *string } +type Content struct { + Bool *bool + Double *float64 + String *string + StringArray []string +} + type ServerModelsRpcApi struct { client *jsonrpc2.Client } @@ -740,6 +1042,230 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult return &result, nil } +func (a *AgentRpcApi) Reload(ctx context.Context) (*SessionAgentReloadResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.agent.reload", req) + if err != nil { + return nil, err + } + var result SessionAgentReloadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: SkillsRpcApi contains experimental APIs that may change or be removed. +type SkillsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *SkillsRpcApi) List(ctx context.Context) (*SessionSkillsListResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.skills.list", req) + if err != nil { + return nil, err + } + var result SessionSkillsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SkillsRpcApi) Enable(ctx context.Context, params *SessionSkillsEnableParams) (*SessionSkillsEnableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["name"] = params.Name + } + raw, err := a.client.Request("session.skills.enable", req) + if err != nil { + return nil, err + } + var result SessionSkillsEnableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SkillsRpcApi) Disable(ctx context.Context, params *SessionSkillsDisableParams) (*SessionSkillsDisableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["name"] = params.Name + } + raw, err := a.client.Request("session.skills.disable", req) + if err != nil { + return nil, err + } + var result SessionSkillsDisableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *SkillsRpcApi) Reload(ctx context.Context) (*SessionSkillsReloadResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.skills.reload", req) + if err != nil { + return nil, err + } + var result SessionSkillsReloadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: McpRpcApi contains experimental APIs that may change or be removed. +type McpRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *McpRpcApi) List(ctx context.Context) (*SessionMCPListResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.mcp.list", req) + if err != nil { + return nil, err + } + var result SessionMCPListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMCPEnableParams) (*SessionMCPEnableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + } + raw, err := a.client.Request("session.mcp.enable", req) + if err != nil { + return nil, err + } + var result SessionMCPEnableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMCPDisableParams) (*SessionMCPDisableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + } + raw, err := a.client.Request("session.mcp.disable", req) + if err != nil { + return nil, err + } + var result SessionMCPDisableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *McpRpcApi) Reload(ctx context.Context) (*SessionMCPReloadResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.mcp.reload", req) + if err != nil { + return nil, err + } + var result SessionMCPReloadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: PluginsRpcApi contains experimental APIs that may change or be removed. +type PluginsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *PluginsRpcApi) List(ctx context.Context) (*SessionPluginsListResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.plugins.list", req) + if err != nil { + return nil, err + } + var result SessionPluginsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: ExtensionsRpcApi contains experimental APIs that may change or be removed. +type ExtensionsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *ExtensionsRpcApi) List(ctx context.Context) (*SessionExtensionsListResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.extensions.list", req) + if err != nil { + return nil, err + } + var result SessionExtensionsListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ExtensionsRpcApi) Enable(ctx context.Context, params *SessionExtensionsEnableParams) (*SessionExtensionsEnableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + } + raw, err := a.client.Request("session.extensions.enable", req) + if err != nil { + return nil, err + } + var result SessionExtensionsEnableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ExtensionsRpcApi) Disable(ctx context.Context, params *SessionExtensionsDisableParams) (*SessionExtensionsDisableResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + } + raw, err := a.client.Request("session.extensions.disable", req) + if err != nil { + return nil, err + } + var result SessionExtensionsDisableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ExtensionsRpcApi) Reload(ctx context.Context) (*SessionExtensionsReloadResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.extensions.reload", req) + if err != nil { + return nil, err + } + var result SessionExtensionsReloadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: CompactionRpcApi contains experimental APIs that may change or be removed. type CompactionRpcApi struct { client *jsonrpc2.Client @@ -786,6 +1312,52 @@ func (a *ToolsRpcApi) HandlePendingToolCall(ctx context.Context, params *Session return &result, nil } +type CommandsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *CommandsRpcApi) HandlePendingCommand(ctx context.Context, params *SessionCommandsHandlePendingCommandParams) (*SessionCommandsHandlePendingCommandResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + if params.Error != nil { + req["error"] = *params.Error + } + } + raw, err := a.client.Request("session.commands.handlePendingCommand", req) + if err != nil { + return nil, err + } + var result SessionCommandsHandlePendingCommandResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +type UiRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *UiRpcApi) Elicitation(ctx context.Context, params *SessionUIElicitationParams) (*SessionUIElicitationResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["message"] = params.Message + req["requestedSchema"] = params.RequestedSchema + } + raw, err := a.client.Request("session.ui.elicitation", req) + if err != nil { + return nil, err + } + var result SessionUIElicitationResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + type PermissionsRpcApi struct { client *jsonrpc2.Client sessionID string @@ -864,8 +1436,14 @@ type SessionRpc struct { Workspace *WorkspaceRpcApi Fleet *FleetRpcApi Agent *AgentRpcApi + Skills *SkillsRpcApi + Mcp *McpRpcApi + Plugins *PluginsRpcApi + Extensions *ExtensionsRpcApi Compaction *CompactionRpcApi Tools *ToolsRpcApi + Commands *CommandsRpcApi + Ui *UiRpcApi Permissions *PermissionsRpcApi Shell *ShellRpcApi } @@ -880,6 +1458,9 @@ func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*Sessio if params.Ephemeral != nil { req["ephemeral"] = *params.Ephemeral } + if params.URL != nil { + req["url"] = *params.URL + } } raw, err := a.client.Request("session.log", req) if err != nil { @@ -900,8 +1481,14 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID}, Fleet: &FleetRpcApi{client: client, sessionID: sessionID}, Agent: &AgentRpcApi{client: client, sessionID: sessionID}, + Skills: &SkillsRpcApi{client: client, sessionID: sessionID}, + Mcp: &McpRpcApi{client: client, sessionID: sessionID}, + Plugins: &PluginsRpcApi{client: client, sessionID: sessionID}, + Extensions: &ExtensionsRpcApi{client: client, sessionID: sessionID}, Compaction: &CompactionRpcApi{client: client, sessionID: sessionID}, Tools: &ToolsRpcApi{client: client, sessionID: sessionID}, + Commands: &CommandsRpcApi{client: client, sessionID: sessionID}, + Ui: &UiRpcApi{client: client, sessionID: sessionID}, Permissions: &PermissionsRpcApi{client: client, sessionID: sessionID}, Shell: &ShellRpcApi{client: client, sessionID: sessionID}, } diff --git a/go/samples/chat.go b/go/samples/chat.go index f984f758ae..4d5e98d7de 100644 --- a/go/samples/chat.go +++ b/go/samples/chat.go @@ -35,11 +35,11 @@ func main() { session.On(func(event copilot.SessionEvent) { var output string switch event.Type { - case copilot.AssistantReasoning: + case copilot.SessionEventTypeAssistantReasoning: if event.Data.Content != nil { output = fmt.Sprintf("[reasoning: %s]", *event.Data.Content) } - case copilot.ToolExecutionStart: + case copilot.SessionEventTypeToolExecutionStart: if event.Data.ToolName != nil { output = fmt.Sprintf("[tool: %s]", *event.Data.ToolName) } diff --git a/go/session.go b/go/session.go index d2a5785be1..107ac98248 100644 --- a/go/session.go +++ b/go/session.go @@ -182,17 +182,17 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses unsubscribe := s.On(func(event SessionEvent) { switch event.Type { - case AssistantMessage: + case SessionEventTypeAssistantMessage: mu.Lock() eventCopy := event lastAssistantMessage = &eventCopy mu.Unlock() - case SessionIdle: + case SessionEventTypeSessionIdle: select { case idleCh <- struct{}{}: default: } - case SessionError: + case SessionEventTypeSessionError: errMsg := "session error" if event.Data.Message != nil { errMsg = *event.Data.Message @@ -501,7 +501,7 @@ func (s *Session) processEvents() { // cause RPC deadlocks. func (s *Session) handleBroadcastEvent(event SessionEvent) { switch event.Type { - case ExternalToolRequested: + case SessionEventTypeExternalToolRequested: requestID := event.Data.RequestID toolName := event.Data.ToolName if requestID == nil || toolName == nil { @@ -524,7 +524,7 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler, tp, ts) - case PermissionRequested: + case SessionEventTypePermissionRequested: requestID := event.Data.RequestID if requestID == nil || event.Data.PermissionRequest == nil { return @@ -585,7 +585,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{ RequestID: requestID, Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{ - Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser, + Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser, }, }) } @@ -600,7 +600,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{ RequestID: requestID, Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{ - Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser, + Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser, }, }) return @@ -770,8 +770,8 @@ func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOp // LogOptions configures optional parameters for [Session.Log]. type LogOptions struct { - // Level sets the log severity. Valid values are [rpc.Info] (default), - // [rpc.Warning], and [rpc.Error]. + // Level sets the log severity. Valid values are [rpc.LevelInfo] (default), + // [rpc.LevelWarning], and [rpc.LevelError]. Level rpc.Level // Ephemeral marks the message as transient so it is not persisted // to the session event log on disk. When nil the server decides the @@ -791,7 +791,7 @@ type LogOptions struct { // session.Log(ctx, "Processing started") // // // Warning with options -// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.Warning}) +// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.LevelWarning}) // // // Ephemeral message (not persisted) // session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: copilot.Bool(true)}) diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 0952122f0e..fd56aa84b5 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.4", + "@github/copilot": "^1.0.10-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz", - "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz", + "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.4", - "@github/copilot-darwin-x64": "1.0.4", - "@github/copilot-linux-arm64": "1.0.4", - "@github/copilot-linux-x64": "1.0.4", - "@github/copilot-win32-arm64": "1.0.4", - "@github/copilot-win32-x64": "1.0.4" + "@github/copilot-darwin-arm64": "1.0.10-0", + "@github/copilot-darwin-x64": "1.0.10-0", + "@github/copilot-linux-arm64": "1.0.10-0", + "@github/copilot-linux-x64": "1.0.10-0", + "@github/copilot-win32-arm64": "1.0.10-0", + "@github/copilot-win32-x64": "1.0.10-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz", + "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz", - "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz", + "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz", - "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz", + "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz", - "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz", + "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz", - "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz", + "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz", - "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz", + "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 6b0d30f2cf..7d1822a9cc 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.4", + "@github/copilot": "^1.0.10-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 4f93a271c6..77daced15e 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.4", + "@github/copilot": "^1.0.10-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 16907fdba7..dadb9e79dc 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -462,6 +462,302 @@ export interface SessionAgentDeselectParams { sessionId: string; } +/** @experimental */ +export interface SessionAgentReloadResult { + /** + * Reloaded custom agents + */ + agents: { + /** + * Unique identifier of the custom agent + */ + name: string; + /** + * Human-readable display name + */ + displayName: string; + /** + * Description of the agent's purpose + */ + description: string; + }[]; +} + +/** @experimental */ +export interface SessionAgentReloadParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionSkillsListResult { + /** + * Available skills + */ + skills: { + /** + * Unique identifier for the skill + */ + name: string; + /** + * Description of what the skill does + */ + description: string; + /** + * Source location type (e.g., project, personal, plugin) + */ + source: string; + /** + * Whether the skill can be invoked by the user as a slash command + */ + userInvocable: boolean; + /** + * Whether the skill is currently enabled + */ + enabled: boolean; + /** + * Absolute path to the skill file + */ + path?: string; + }[]; +} + +/** @experimental */ +export interface SessionSkillsListParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionSkillsEnableResult {} + +/** @experimental */ +export interface SessionSkillsEnableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Name of the skill to enable + */ + name: string; +} + +/** @experimental */ +export interface SessionSkillsDisableResult {} + +/** @experimental */ +export interface SessionSkillsDisableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Name of the skill to disable + */ + name: string; +} + +/** @experimental */ +export interface SessionSkillsReloadResult {} + +/** @experimental */ +export interface SessionSkillsReloadParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionMcpListResult { + /** + * Configured MCP servers + */ + servers: { + /** + * Server name (config key) + */ + name: string; + /** + * Connection status: connected, failed, pending, disabled, or not_configured + */ + status: "connected" | "failed" | "pending" | "disabled" | "not_configured"; + /** + * Configuration source: user, workspace, plugin, or builtin + */ + source?: string; + /** + * Error message if the server failed to connect + */ + error?: string; + }[]; +} + +/** @experimental */ +export interface SessionMcpListParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionMcpEnableResult {} + +/** @experimental */ +export interface SessionMcpEnableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Name of the MCP server to enable + */ + serverName: string; +} + +/** @experimental */ +export interface SessionMcpDisableResult {} + +/** @experimental */ +export interface SessionMcpDisableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Name of the MCP server to disable + */ + serverName: string; +} + +/** @experimental */ +export interface SessionMcpReloadResult {} + +/** @experimental */ +export interface SessionMcpReloadParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionPluginsListResult { + /** + * Installed plugins + */ + plugins: { + /** + * Plugin name + */ + name: string; + /** + * Marketplace the plugin came from + */ + marketplace: string; + /** + * Installed version + */ + version?: string; + /** + * Whether the plugin is currently enabled + */ + enabled: boolean; + }[]; +} + +/** @experimental */ +export interface SessionPluginsListParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionExtensionsListResult { + /** + * Discovered extensions and their current status + */ + extensions: { + /** + * Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper') + */ + id: string; + /** + * Extension name (directory name) + */ + name: string; + /** + * Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) + */ + source: "project" | "user"; + /** + * Current status: running, disabled, failed, or starting + */ + status: "running" | "disabled" | "failed" | "starting"; + /** + * Process ID if the extension is running + */ + pid?: number; + }[]; +} + +/** @experimental */ +export interface SessionExtensionsListParams { + /** + * Target session identifier + */ + sessionId: string; +} + +/** @experimental */ +export interface SessionExtensionsEnableResult {} + +/** @experimental */ +export interface SessionExtensionsEnableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Source-qualified extension ID to enable + */ + id: string; +} + +/** @experimental */ +export interface SessionExtensionsDisableResult {} + +/** @experimental */ +export interface SessionExtensionsDisableParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Source-qualified extension ID to disable + */ + id: string; +} + +/** @experimental */ +export interface SessionExtensionsReloadResult {} + +/** @experimental */ +export interface SessionExtensionsReloadParams { + /** + * Target session identifier + */ + sessionId: string; +} + /** @experimental */ export interface SessionCompactionCompactResult { /** @@ -512,6 +808,135 @@ export interface SessionToolsHandlePendingToolCallParams { error?: string; } +export interface SessionCommandsHandlePendingCommandResult { + success: boolean; +} + +export interface SessionCommandsHandlePendingCommandParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Request ID from the command invocation event + */ + requestId: string; + /** + * Error message if the command handler failed + */ + error?: string; +} + +export interface SessionUiElicitationResult { + /** + * The user's response: accept (submitted), decline (rejected), or cancel (dismissed) + */ + action: "accept" | "decline" | "cancel"; + /** + * The form values submitted by the user (present when action is 'accept') + */ + content?: { + [k: string]: string | number | boolean | string[]; + }; +} + +export interface SessionUiElicitationParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Message describing what information is needed from the user + */ + message: string; + /** + * JSON Schema describing the form fields to present to the user + */ + requestedSchema: { + /** + * Schema type indicator (always 'object') + */ + type: "object"; + /** + * Form field definitions, keyed by field name + */ + properties: { + [k: string]: + | { + type: "string"; + title?: string; + description?: string; + enum: string[]; + enumNames?: string[]; + default?: string; + } + | { + type: "string"; + title?: string; + description?: string; + oneOf: { + const: string; + title: string; + }[]; + default?: string; + } + | { + type: "array"; + title?: string; + description?: string; + minItems?: number; + maxItems?: number; + items: { + type: "string"; + enum: string[]; + }; + default?: string[]; + } + | { + type: "array"; + title?: string; + description?: string; + minItems?: number; + maxItems?: number; + items: { + anyOf: { + const: string; + title: string; + }[]; + }; + default?: string[]; + } + | { + type: "boolean"; + title?: string; + description?: string; + default?: boolean; + } + | { + type: "string"; + title?: string; + description?: string; + minLength?: number; + maxLength?: number; + format?: "email" | "uri" | "date" | "date-time"; + default?: string; + } + | { + type: "number" | "integer"; + title?: string; + description?: string; + minimum?: number; + maximum?: number; + default?: number; + }; + }; + /** + * List of required field names + */ + required?: string[]; + }; +} + export interface SessionPermissionsHandlePendingPermissionRequestResult { /** * Whether the permission request was handled successfully @@ -571,6 +996,10 @@ export interface SessionLogParams { * When true, the message is transient and not persisted to the session event log on disk */ ephemeral?: boolean; + /** + * Optional URL the user can open in their browser for more details + */ + url?: string; } export interface SessionShellExecResult { @@ -687,6 +1116,46 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.agent.select", { sessionId, ...params }), deselect: async (): Promise => connection.sendRequest("session.agent.deselect", { sessionId }), + reload: async (): Promise => + connection.sendRequest("session.agent.reload", { sessionId }), + }, + /** @experimental */ + skills: { + list: async (): Promise => + connection.sendRequest("session.skills.list", { sessionId }), + enable: async (params: Omit): Promise => + connection.sendRequest("session.skills.enable", { sessionId, ...params }), + disable: async (params: Omit): Promise => + connection.sendRequest("session.skills.disable", { sessionId, ...params }), + reload: async (): Promise => + connection.sendRequest("session.skills.reload", { sessionId }), + }, + /** @experimental */ + mcp: { + list: async (): Promise => + connection.sendRequest("session.mcp.list", { sessionId }), + enable: async (params: Omit): Promise => + connection.sendRequest("session.mcp.enable", { sessionId, ...params }), + disable: async (params: Omit): Promise => + connection.sendRequest("session.mcp.disable", { sessionId, ...params }), + reload: async (): Promise => + connection.sendRequest("session.mcp.reload", { sessionId }), + }, + /** @experimental */ + plugins: { + list: async (): Promise => + connection.sendRequest("session.plugins.list", { sessionId }), + }, + /** @experimental */ + extensions: { + list: async (): Promise => + connection.sendRequest("session.extensions.list", { sessionId }), + enable: async (params: Omit): Promise => + connection.sendRequest("session.extensions.enable", { sessionId, ...params }), + disable: async (params: Omit): Promise => + connection.sendRequest("session.extensions.disable", { sessionId, ...params }), + reload: async (): Promise => + connection.sendRequest("session.extensions.reload", { sessionId }), }, /** @experimental */ compaction: { @@ -697,6 +1166,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin handlePendingToolCall: async (params: Omit): Promise => connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }), }, + commands: { + handlePendingCommand: async (params: Omit): Promise => + connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }), + }, + ui: { + elicitation: async (params: Omit): Promise => + connection.sendRequest("session.ui.elicitation", { sessionId, ...params }), + }, permissions: { handlePendingPermissionRequest: async (params: Omit): Promise => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index e9d48bc57e..9ad6d3c024 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -212,6 +212,10 @@ export type SessionEvent = * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ providerCallId?: string; + /** + * Optional URL associated with this error that the user can open in a browser + */ + url?: string; }; } | { @@ -325,6 +329,10 @@ export type SessionEvent = * Human-readable informational message for display in the timeline */ message: string; + /** + * Optional URL associated with this message that the user can open in a browser + */ + url?: string; }; } | { @@ -357,6 +365,10 @@ export type SessionEvent = * Human-readable warning message for display in the timeline */ message: string; + /** + * Optional URL associated with this warning that the user can open in a browser + */ + url?: string; }; } | { @@ -741,6 +753,22 @@ export type SessionEvent = * Model that was selected at the time of shutdown */ currentModel?: string; + /** + * Total tokens in context window at shutdown + */ + currentTokens?: number; + /** + * System message token count at shutdown + */ + systemTokens?: number; + /** + * Non-system message token count at shutdown + */ + conversationTokens?: number; + /** + * Tool definitions token count at shutdown + */ + toolDefinitionsTokens?: number; }; } | { @@ -826,6 +854,22 @@ export type SessionEvent = * Current number of messages in the conversation */ messagesLength: number; + /** + * Token count from system message(s) + */ + systemTokens?: number; + /** + * Token count from non-system messages (user, assistant, tool) + */ + conversationTokens?: number; + /** + * Token count from tool definitions + */ + toolDefinitionsTokens?: number; + /** + * Whether this is the first usage_info event emitted in this session + */ + isInitial?: boolean; }; } | { @@ -847,9 +891,22 @@ export type SessionEvent = ephemeral?: boolean; type: "session.compaction_start"; /** - * Empty payload; the event signals that LLM-powered conversation compaction has begun + * Context window breakdown at the start of LLM-powered conversation compaction */ - data: {}; + data: { + /** + * Token count from system message(s) at compaction start + */ + systemTokens?: number; + /** + * Token count from non-system messages (user, assistant, tool) at compaction start + */ + conversationTokens?: number; + /** + * Token count from tool definitions at compaction start + */ + toolDefinitionsTokens?: number; + }; } | { /** @@ -934,6 +991,18 @@ export type SessionEvent = * GitHub request tracing ID (x-github-request-id header) for the compaction LLM call */ requestId?: string; + /** + * Token count from system message(s) after compaction + */ + systemTokens?: number; + /** + * Token count from non-system messages (user, assistant, tool) after compaction + */ + conversationTokens?: number; + /** + * Token count from tool definitions after compaction + */ + toolDefinitionsTokens?: number; }; } | { @@ -955,13 +1024,17 @@ export type SessionEvent = ephemeral?: boolean; type: "session.task_complete"; /** - * Task completion notification with optional summary from the agent + * Task completion notification with summary from the agent */ data: { /** - * Optional summary of the completed task, provided by the agent + * Summary of the completed task, provided by the agent */ summary?: string; + /** + * Whether the tool call succeeded. False when validation failed (e.g., invalid arguments) + */ + success?: boolean; }; } | { @@ -982,9 +1055,6 @@ export type SessionEvent = */ ephemeral?: boolean; type: "user.message"; - /** - * User message content with optional attachments, source information, and interaction metadata - */ data: { /** * The user's message text as displayed in the timeline @@ -1134,19 +1204,9 @@ export type SessionEvent = } )[]; /** - * Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command") + * Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ - source?: - | "user" - | "autopilot" - | "skill" - | "system" - | "command" - | "immediate-prompt" - | "jit-instruction" - | "snippy-blocking" - | "thinking-exhausted-continuation" - | "other"; + source?: string; /** * The agent mode that was active when this message was sent */ @@ -2434,6 +2494,21 @@ export type SessionEvent = */ prompt?: string; } + | { + type: "agent_idle"; + /** + * Unique identifier of the background agent + */ + agentId: string; + /** + * Type of the agent (e.g., explore, task, general-purpose) + */ + agentType: string; + /** + * Human-readable description of the agent task + */ + description?: string; + } | { type: "shell_completed"; /** @@ -2785,6 +2860,10 @@ export type SessionEvent = * Whether the user can provide a free-form text response in addition to predefined choices */ allowFreeform?: boolean; + /** + * The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses + */ + toolCallId?: string; }; } | { @@ -2828,25 +2907,33 @@ export type SessionEvent = ephemeral: true; type: "elicitation.requested"; /** - * Structured form elicitation request with JSON schema definition for form fields + * Elicitation request; may be form-based (structured input) or URL-based (browser redirect) */ data: { /** * Unique identifier for this elicitation request; used to respond via session.respondToElicitation() */ requestId: string; + /** + * Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs + */ + toolCallId?: string; + /** + * The source that initiated the request (MCP server name, or absent for agent-initiated) + */ + elicitationSource?: string; /** * Message describing what information is needed from the user */ message: string; /** - * Elicitation mode; currently only "form" is supported. Defaults to "form" when absent. + * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent. */ - mode?: "form"; + mode?: "form" | "url"; /** - * JSON Schema describing the form fields to present to the user + * JSON Schema describing the form fields to present to the user (form mode only) */ - requestedSchema: { + requestedSchema?: { /** * Schema type indicator (always 'object') */ @@ -2862,6 +2949,10 @@ export type SessionEvent = */ required?: string[]; }; + /** + * URL to open in the user's browser (url mode only) + */ + url?: string; [k: string]: unknown; }; } @@ -2890,6 +2981,77 @@ export type SessionEvent = requestId: string; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "mcp.oauth_required"; + /** + * OAuth authentication request for an MCP server + */ + data: { + /** + * Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth() + */ + requestId: string; + /** + * Display name of the MCP server that requires OAuth + */ + serverName: string; + /** + * URL of the MCP server that requires OAuth + */ + serverUrl: string; + /** + * Static OAuth client configuration, if the server specifies one + */ + staticClientConfig?: { + /** + * OAuth client ID for the server + */ + clientId: string; + /** + * Whether this is a public OAuth client + */ + publicClient?: boolean; + }; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "mcp.oauth_completed"; + /** + * MCP OAuth request completion notification + */ + data: { + /** + * Request ID of the resolved OAuth request + */ + requestId: string; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted @@ -2995,6 +3157,43 @@ export type SessionEvent = command: string; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "command.execute"; + /** + * Registered command dispatch request routed to the owning client + */ + data: { + /** + * Unique identifier; used to respond via session.commands.handlePendingCommand() + */ + requestId: string; + /** + * The full command text (e.g., /deploy production) + */ + command: string; + /** + * Command name without leading / + */ + commandName: string; + /** + * Raw argument string after the command name + */ + args: string; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted @@ -3020,6 +3219,34 @@ export type SessionEvent = requestId: string; }; } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "commands.changed"; + /** + * SDK command registration change notification + */ + data: { + /** + * Current list of registered SDK commands + */ + commands: { + name: string; + description?: string; + }[]; + }; + } | { /** * Unique event identifier (UUID v4), generated when the event is emitted @@ -3121,4 +3348,155 @@ export type SessionEvent = ephemeral: true; type: "session.background_tasks_changed"; data: {}; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.skills_loaded"; + data: { + /** + * Array of resolved skill metadata + */ + skills: { + /** + * Unique identifier for the skill + */ + name: string; + /** + * Description of what the skill does + */ + description: string; + /** + * Source location type of the skill (e.g., project, personal, plugin) + */ + source: string; + /** + * Whether the skill can be invoked by the user as a slash command + */ + userInvocable: boolean; + /** + * Whether the skill is currently enabled + */ + enabled: boolean; + /** + * Absolute path to the skill file, if available + */ + path?: string; + }[]; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.mcp_servers_loaded"; + data: { + /** + * Array of MCP server status summaries + */ + servers: { + /** + * Server name (config key) + */ + name: string; + /** + * Connection status: connected, failed, pending, disabled, or not_configured + */ + status: "connected" | "failed" | "pending" | "disabled" | "not_configured"; + /** + * Configuration source: user, workspace, plugin, or builtin + */ + source?: string; + /** + * Error message if the server failed to connect + */ + error?: string; + }[]; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.mcp_server_status_changed"; + data: { + /** + * Name of the MCP server whose status changed + */ + serverName: string; + /** + * New connection status: connected, failed, pending, disabled, or not_configured + */ + status: "connected" | "failed" | "pending" | "disabled" | "not_configured"; + }; + } + | { + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + ephemeral: true; + type: "session.extensions_loaded"; + data: { + /** + * Array of discovered extensions and their status + */ + extensions: { + /** + * Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper') + */ + id: string; + /** + * Extension name (directory name) + */ + name: string; + /** + * Discovery source + */ + source: "project" | "user"; + /** + * Current status: running, disabled, failed, or starting + */ + status: "running" | "disabled" | "failed" | "starting"; + }[]; + }; }; diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index da6748d799..14ae307d70 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -74,6 +74,11 @@ def to_enum(c: type[EnumT], x: Any) -> EnumT: return x.value +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + + @dataclass class PingResult: message: str @@ -762,7 +767,7 @@ def to_dict(self) -> dict: @dataclass -class AgentElement: +class SessionAgentListResultAgent: description: str """Description of the agent's purpose""" @@ -773,12 +778,12 @@ class AgentElement: """Unique identifier of the custom agent""" @staticmethod - def from_dict(obj: Any) -> 'AgentElement': + def from_dict(obj: Any) -> 'SessionAgentListResultAgent': assert isinstance(obj, dict) description = from_str(obj.get("description")) display_name = from_str(obj.get("displayName")) name = from_str(obj.get("name")) - return AgentElement(description, display_name, name) + return SessionAgentListResultAgent(description, display_name, name) def to_dict(self) -> dict: result: dict = {} @@ -791,18 +796,18 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentListResult: - agents: list[AgentElement] + agents: list[SessionAgentListResultAgent] """Available custom agents""" @staticmethod def from_dict(obj: Any) -> 'SessionAgentListResult': assert isinstance(obj, dict) - agents = from_list(AgentElement.from_dict, obj.get("agents")) + agents = from_list(SessionAgentListResultAgent.from_dict, obj.get("agents")) return SessionAgentListResult(agents) def to_dict(self) -> dict: result: dict = {} - result["agents"] = from_list(lambda x: to_class(AgentElement, x), self.agents) + result["agents"] = from_list(lambda x: to_class(SessionAgentListResultAgent, x), self.agents) return result @@ -929,334 +934,1129 @@ def to_dict(self) -> dict: return result -# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionCompactionCompactResult: - messages_removed: float - """Number of messages removed during compaction""" +class SessionAgentReloadResultAgent: + description: str + """Description of the agent's purpose""" - success: bool - """Whether compaction completed successfully""" + display_name: str + """Human-readable display name""" - tokens_removed: float - """Number of tokens freed by compaction""" + name: str + """Unique identifier of the custom agent""" @staticmethod - def from_dict(obj: Any) -> 'SessionCompactionCompactResult': + def from_dict(obj: Any) -> 'SessionAgentReloadResultAgent': assert isinstance(obj, dict) - messages_removed = from_float(obj.get("messagesRemoved")) - success = from_bool(obj.get("success")) - tokens_removed = from_float(obj.get("tokensRemoved")) - return SessionCompactionCompactResult(messages_removed, success, tokens_removed) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + return SessionAgentReloadResultAgent(description, display_name, name) def to_dict(self) -> dict: result: dict = {} - result["messagesRemoved"] = to_float(self.messages_removed) - result["success"] = from_bool(self.success) - result["tokensRemoved"] = to_float(self.tokens_removed) + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionToolsHandlePendingToolCallResult: - success: bool - """Whether the tool call result was handled successfully""" +class SessionAgentReloadResult: + agents: list[SessionAgentReloadResultAgent] + """Reloaded custom agents""" @staticmethod - def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult': + def from_dict(obj: Any) -> 'SessionAgentReloadResult': assert isinstance(obj, dict) - success = from_bool(obj.get("success")) - return SessionToolsHandlePendingToolCallResult(success) + agents = from_list(SessionAgentReloadResultAgent.from_dict, obj.get("agents")) + return SessionAgentReloadResult(agents) def to_dict(self) -> dict: result: dict = {} - result["success"] = from_bool(self.success) + result["agents"] = from_list(lambda x: to_class(SessionAgentReloadResultAgent, x), self.agents) return result @dataclass -class ResultResult: - text_result_for_llm: str - error: str | None = None - result_type: str | None = None - tool_telemetry: dict[str, Any] | None = None +class Skill: + description: str + """Description of what the skill does""" + + enabled: bool + """Whether the skill is currently enabled""" + + name: str + """Unique identifier for the skill""" + + source: str + """Source location type (e.g., project, personal, plugin)""" + + user_invocable: bool + """Whether the skill can be invoked by the user as a slash command""" + + path: str | None = None + """Absolute path to the skill file""" @staticmethod - def from_dict(obj: Any) -> 'ResultResult': + def from_dict(obj: Any) -> 'Skill': assert isinstance(obj, dict) - text_result_for_llm = from_str(obj.get("textResultForLlm")) - error = from_union([from_str, from_none], obj.get("error")) - result_type = from_union([from_str, from_none], obj.get("resultType")) - tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) - return ResultResult(text_result_for_llm, error, result_type, tool_telemetry) + description = from_str(obj.get("description")) + enabled = from_bool(obj.get("enabled")) + name = from_str(obj.get("name")) + source = from_str(obj.get("source")) + user_invocable = from_bool(obj.get("userInvocable")) + path = from_union([from_str, from_none], obj.get("path")) + return Skill(description, enabled, name, source, user_invocable, path) def to_dict(self) -> dict: result: dict = {} - result["textResultForLlm"] = from_str(self.text_result_for_llm) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.result_type is not None: - result["resultType"] = from_union([from_str, from_none], self.result_type) - if self.tool_telemetry is not None: - result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) + result["description"] = from_str(self.description) + result["enabled"] = from_bool(self.enabled) + result["name"] = from_str(self.name) + result["source"] = from_str(self.source) + result["userInvocable"] = from_bool(self.user_invocable) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionToolsHandlePendingToolCallParams: - request_id: str - error: str | None = None - result: ResultResult | str | None = None +class SessionSkillsListResult: + skills: list[Skill] + """Available skills""" @staticmethod - def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams': + def from_dict(obj: Any) -> 'SessionSkillsListResult': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - error = from_union([from_str, from_none], obj.get("error")) - result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result")) - return SessionToolsHandlePendingToolCallParams(request_id, error, result) + skills = from_list(Skill.from_dict, obj.get("skills")) + return SessionSkillsListResult(skills) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - if self.error is not None: - result["error"] = from_union([from_str, from_none], self.error) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result) + result["skills"] = from_list(lambda x: to_class(Skill, x), self.skills) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionPermissionsHandlePendingPermissionRequestResult: - success: bool - """Whether the permission request was handled successfully""" - +class SessionSkillsEnableResult: @staticmethod - def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult': + def from_dict(obj: Any) -> 'SessionSkillsEnableResult': assert isinstance(obj, dict) - success = from_bool(obj.get("success")) - return SessionPermissionsHandlePendingPermissionRequestResult(success) + return SessionSkillsEnableResult() def to_dict(self) -> dict: result: dict = {} - result["success"] = from_bool(self.success) return result -class Kind(Enum): - APPROVED = "approved" - DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" - DENIED_BY_RULES = "denied-by-rules" - DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" - DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionSkillsEnableParams: + name: str + """Name of the skill to enable""" + @staticmethod + def from_dict(obj: Any) -> 'SessionSkillsEnableParams': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return SessionSkillsEnableParams(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + return result -@dataclass -class SessionPermissionsHandlePendingPermissionRequestParamsResult: - kind: Kind - rules: list[Any] | None = None - feedback: str | None = None - message: str | None = None - path: str | None = None +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionSkillsDisableResult: @staticmethod - def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult': + def from_dict(obj: Any) -> 'SessionSkillsDisableResult': assert isinstance(obj, dict) - kind = Kind(obj.get("kind")) - rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules")) - feedback = from_union([from_str, from_none], obj.get("feedback")) - message = from_union([from_str, from_none], obj.get("message")) - path = from_union([from_str, from_none], obj.get("path")) - return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path) + return SessionSkillsDisableResult() def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(Kind, self.kind) - if self.rules is not None: - result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules) - if self.feedback is not None: - result["feedback"] = from_union([from_str, from_none], self.feedback) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionPermissionsHandlePendingPermissionRequestParams: - request_id: str - result: SessionPermissionsHandlePendingPermissionRequestParamsResult +class SessionSkillsDisableParams: + name: str + """Name of the skill to disable""" @staticmethod - def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams': + def from_dict(obj: Any) -> 'SessionSkillsDisableParams': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result")) - return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result) + name = from_str(obj.get("name")) + return SessionSkillsDisableParams(name) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result) + result["name"] = from_str(self.name) return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class SessionLogResult: - event_id: UUID - """The unique identifier of the emitted session event""" - +class SessionSkillsReloadResult: @staticmethod - def from_dict(obj: Any) -> 'SessionLogResult': + def from_dict(obj: Any) -> 'SessionSkillsReloadResult': assert isinstance(obj, dict) - event_id = UUID(obj.get("eventId")) - return SessionLogResult(event_id) + return SessionSkillsReloadResult() def to_dict(self) -> dict: result: dict = {} - result["eventId"] = str(self.event_id) return result -class Level(Enum): - """Log severity level. Determines how the message is displayed in the timeline. Defaults to - "info". - """ - ERROR = "error" - INFO = "info" - WARNING = "warning" +class ServerStatus(Enum): + """Connection status: connected, failed, pending, disabled, or not_configured""" + + CONNECTED = "connected" + DISABLED = "disabled" + FAILED = "failed" + NOT_CONFIGURED = "not_configured" + PENDING = "pending" @dataclass -class SessionLogParams: - message: str - """Human-readable message""" +class Server: + name: str + """Server name (config key)""" - ephemeral: bool | None = None - """When true, the message is transient and not persisted to the session event log on disk""" + status: ServerStatus + """Connection status: connected, failed, pending, disabled, or not_configured""" - level: Level | None = None - """Log severity level. Determines how the message is displayed in the timeline. Defaults to - "info". - """ + error: str | None = None + """Error message if the server failed to connect""" + + source: str | None = None + """Configuration source: user, workspace, plugin, or builtin""" @staticmethod - def from_dict(obj: Any) -> 'SessionLogParams': + def from_dict(obj: Any) -> 'Server': assert isinstance(obj, dict) - message = from_str(obj.get("message")) - ephemeral = from_union([from_bool, from_none], obj.get("ephemeral")) - level = from_union([Level, from_none], obj.get("level")) - return SessionLogParams(message, ephemeral, level) + name = from_str(obj.get("name")) + status = ServerStatus(obj.get("status")) + error = from_union([from_str, from_none], obj.get("error")) + source = from_union([from_str, from_none], obj.get("source")) + return Server(name, status, error, source) def to_dict(self) -> dict: result: dict = {} - result["message"] = from_str(self.message) - if self.ephemeral is not None: - result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral) - if self.level is not None: - result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level) + result["name"] = from_str(self.name) + result["status"] = to_enum(ServerStatus, self.status) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) return result @dataclass -class SessionShellExecResult: - process_id: str - """Unique identifier for tracking streamed output""" +class SessionMCPListResult: + servers: list[Server] + """Configured MCP servers""" @staticmethod - def from_dict(obj: Any) -> 'SessionShellExecResult': + def from_dict(obj: Any) -> 'SessionMCPListResult': assert isinstance(obj, dict) - process_id = from_str(obj.get("processId")) - return SessionShellExecResult(process_id) + servers = from_list(Server.from_dict, obj.get("servers")) + return SessionMCPListResult(servers) def to_dict(self) -> dict: result: dict = {} - result["processId"] = from_str(self.process_id) + result["servers"] = from_list(lambda x: to_class(Server, x), self.servers) return result @dataclass -class SessionShellExecParams: - command: str - """Shell command to execute""" - - cwd: str | None = None - """Working directory (defaults to session working directory)""" - - timeout: float | None = None - """Timeout in milliseconds (default: 30000)""" - +class SessionMCPEnableResult: @staticmethod - def from_dict(obj: Any) -> 'SessionShellExecParams': + def from_dict(obj: Any) -> 'SessionMCPEnableResult': assert isinstance(obj, dict) - command = from_str(obj.get("command")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - timeout = from_union([from_float, from_none], obj.get("timeout")) - return SessionShellExecParams(command, cwd, timeout) + return SessionMCPEnableResult() def to_dict(self) -> dict: result: dict = {} - result["command"] = from_str(self.command) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.timeout is not None: - result["timeout"] = from_union([to_float, from_none], self.timeout) return result @dataclass -class SessionShellKillResult: - killed: bool - """Whether the signal was sent successfully""" +class SessionMCPEnableParams: + server_name: str + """Name of the MCP server to enable""" @staticmethod - def from_dict(obj: Any) -> 'SessionShellKillResult': + def from_dict(obj: Any) -> 'SessionMCPEnableParams': assert isinstance(obj, dict) - killed = from_bool(obj.get("killed")) - return SessionShellKillResult(killed) + server_name = from_str(obj.get("serverName")) + return SessionMCPEnableParams(server_name) def to_dict(self) -> dict: result: dict = {} - result["killed"] = from_bool(self.killed) + result["serverName"] = from_str(self.server_name) return result -class Signal(Enum): - """Signal to send (default: SIGTERM)""" +@dataclass +class SessionMCPDisableResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPDisableResult': + assert isinstance(obj, dict) + return SessionMCPDisableResult() - SIGINT = "SIGINT" - SIGKILL = "SIGKILL" - SIGTERM = "SIGTERM" + def to_dict(self) -> dict: + result: dict = {} + return result @dataclass -class SessionShellKillParams: - process_id: str - """Process identifier returned by shell.exec""" - - signal: Signal | None = None - """Signal to send (default: SIGTERM)""" +class SessionMCPDisableParams: + server_name: str + """Name of the MCP server to disable""" @staticmethod - def from_dict(obj: Any) -> 'SessionShellKillParams': + def from_dict(obj: Any) -> 'SessionMCPDisableParams': assert isinstance(obj, dict) - process_id = from_str(obj.get("processId")) - signal = from_union([Signal, from_none], obj.get("signal")) - return SessionShellKillParams(process_id, signal) + server_name = from_str(obj.get("serverName")) + return SessionMCPDisableParams(server_name) def to_dict(self) -> dict: result: dict = {} - result["processId"] = from_str(self.process_id) - if self.signal is not None: - result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal) + result["serverName"] = from_str(self.server_name) return result -def ping_result_from_dict(s: Any) -> PingResult: - return PingResult.from_dict(s) - - +@dataclass +class SessionMCPReloadResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionMCPReloadResult': + assert isinstance(obj, dict) + return SessionMCPReloadResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + + +@dataclass +class Plugin: + enabled: bool + """Whether the plugin is currently enabled""" + + marketplace: str + """Marketplace the plugin came from""" + + name: str + """Plugin name""" + + version: str | None = None + """Installed version""" + + @staticmethod + def from_dict(obj: Any) -> 'Plugin': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + marketplace = from_str(obj.get("marketplace")) + name = from_str(obj.get("name")) + version = from_union([from_str, from_none], obj.get("version")) + return Plugin(enabled, marketplace, name, version) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["marketplace"] = from_str(self.marketplace) + result["name"] = from_str(self.name) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionPluginsListResult: + plugins: list[Plugin] + """Installed plugins""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionPluginsListResult': + assert isinstance(obj, dict) + plugins = from_list(Plugin.from_dict, obj.get("plugins")) + return SessionPluginsListResult(plugins) + + def to_dict(self) -> dict: + result: dict = {} + result["plugins"] = from_list(lambda x: to_class(Plugin, x), self.plugins) + return result + + +class Source(Enum): + """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)""" + + PROJECT = "project" + USER = "user" + + +class ExtensionStatus(Enum): + """Current status: running, disabled, failed, or starting""" + + DISABLED = "disabled" + FAILED = "failed" + RUNNING = "running" + STARTING = "starting" + + +@dataclass +class Extension: + id: str + """Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')""" + + name: str + """Extension name (directory name)""" + + source: Source + """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)""" + + status: ExtensionStatus + """Current status: running, disabled, failed, or starting""" + + pid: int | None = None + """Process ID if the extension is running""" + + @staticmethod + def from_dict(obj: Any) -> 'Extension': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + name = from_str(obj.get("name")) + source = Source(obj.get("source")) + status = ExtensionStatus(obj.get("status")) + pid = from_union([from_int, from_none], obj.get("pid")) + return Extension(id, name, source, status, pid) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["name"] = from_str(self.name) + result["source"] = to_enum(Source, self.source) + result["status"] = to_enum(ExtensionStatus, self.status) + if self.pid is not None: + result["pid"] = from_union([from_int, from_none], self.pid) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsListResult: + extensions: list[Extension] + """Discovered extensions and their current status""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsListResult': + assert isinstance(obj, dict) + extensions = from_list(Extension.from_dict, obj.get("extensions")) + return SessionExtensionsListResult(extensions) + + def to_dict(self) -> dict: + result: dict = {} + result["extensions"] = from_list(lambda x: to_class(Extension, x), self.extensions) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsEnableResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsEnableResult': + assert isinstance(obj, dict) + return SessionExtensionsEnableResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsEnableParams: + id: str + """Source-qualified extension ID to enable""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsEnableParams': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return SessionExtensionsEnableParams(id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsDisableResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsDisableResult': + assert isinstance(obj, dict) + return SessionExtensionsDisableResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsDisableParams: + id: str + """Source-qualified extension ID to disable""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsDisableParams': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return SessionExtensionsDisableParams(id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionExtensionsReloadResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionExtensionsReloadResult': + assert isinstance(obj, dict) + return SessionExtensionsReloadResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionCompactionCompactResult: + messages_removed: float + """Number of messages removed during compaction""" + + success: bool + """Whether compaction completed successfully""" + + tokens_removed: float + """Number of tokens freed by compaction""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionCompactionCompactResult': + assert isinstance(obj, dict) + messages_removed = from_float(obj.get("messagesRemoved")) + success = from_bool(obj.get("success")) + tokens_removed = from_float(obj.get("tokensRemoved")) + return SessionCompactionCompactResult(messages_removed, success, tokens_removed) + + def to_dict(self) -> dict: + result: dict = {} + result["messagesRemoved"] = to_float(self.messages_removed) + result["success"] = from_bool(self.success) + result["tokensRemoved"] = to_float(self.tokens_removed) + return result + + +@dataclass +class SessionToolsHandlePendingToolCallResult: + success: bool + """Whether the tool call result was handled successfully""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return SessionToolsHandlePendingToolCallResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + + +@dataclass +class ResultResult: + text_result_for_llm: str + error: str | None = None + result_type: str | None = None + tool_telemetry: dict[str, Any] | None = None + + @staticmethod + def from_dict(obj: Any) -> 'ResultResult': + assert isinstance(obj, dict) + text_result_for_llm = from_str(obj.get("textResultForLlm")) + error = from_union([from_str, from_none], obj.get("error")) + result_type = from_union([from_str, from_none], obj.get("resultType")) + tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry")) + return ResultResult(text_result_for_llm, error, result_type, tool_telemetry) + + def to_dict(self) -> dict: + result: dict = {} + result["textResultForLlm"] = from_str(self.text_result_for_llm) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.result_type is not None: + result["resultType"] = from_union([from_str, from_none], self.result_type) + if self.tool_telemetry is not None: + result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry) + return result + + +@dataclass +class SessionToolsHandlePendingToolCallParams: + request_id: str + error: str | None = None + result: ResultResult | str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + error = from_union([from_str, from_none], obj.get("error")) + result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result")) + return SessionToolsHandlePendingToolCallParams(request_id, error, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.result is not None: + result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result) + return result + + +@dataclass +class SessionCommandsHandlePendingCommandResult: + success: bool + + @staticmethod + def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return SessionCommandsHandlePendingCommandResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + + +@dataclass +class SessionCommandsHandlePendingCommandParams: + request_id: str + """Request ID from the command invocation event""" + + error: str | None = None + """Error message if the command handler failed""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandParams': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + error = from_union([from_str, from_none], obj.get("error")) + return SessionCommandsHandlePendingCommandParams(request_id, error) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + return result + + +class Action(Enum): + """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)""" + + ACCEPT = "accept" + CANCEL = "cancel" + DECLINE = "decline" + + +@dataclass +class SessionUIElicitationResult: + action: Action + """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)""" + + content: dict[str, float | bool | list[str] | str] | None = None + """The form values submitted by the user (present when action is 'accept')""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionUIElicitationResult': + assert isinstance(obj, dict) + action = Action(obj.get("action")) + content = from_union([lambda x: from_dict(lambda x: from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], obj.get("content")) + return SessionUIElicitationResult(action, content) + + def to_dict(self) -> dict: + result: dict = {} + result["action"] = to_enum(Action, self.action) + if self.content is not None: + result["content"] = from_union([lambda x: from_dict(lambda x: from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], self.content) + return result + + +class Format(Enum): + DATE = "date" + DATE_TIME = "date-time" + EMAIL = "email" + URI = "uri" + + +@dataclass +class AnyOf: + const: str + title: str + + @staticmethod + def from_dict(obj: Any) -> 'AnyOf': + assert isinstance(obj, dict) + const = from_str(obj.get("const")) + title = from_str(obj.get("title")) + return AnyOf(const, title) + + def to_dict(self) -> dict: + result: dict = {} + result["const"] = from_str(self.const) + result["title"] = from_str(self.title) + return result + + +class ItemsType(Enum): + STRING = "string" + + +@dataclass +class Items: + enum: list[str] | None = None + type: ItemsType | None = None + any_of: list[AnyOf] | None = None + + @staticmethod + def from_dict(obj: Any) -> 'Items': + assert isinstance(obj, dict) + enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum")) + type = from_union([ItemsType, from_none], obj.get("type")) + any_of = from_union([lambda x: from_list(AnyOf.from_dict, x), from_none], obj.get("anyOf")) + return Items(enum, type, any_of) + + def to_dict(self) -> dict: + result: dict = {} + if self.enum is not None: + result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum) + if self.type is not None: + result["type"] = from_union([lambda x: to_enum(ItemsType, x), from_none], self.type) + if self.any_of is not None: + result["anyOf"] = from_union([lambda x: from_list(lambda x: to_class(AnyOf, x), x), from_none], self.any_of) + return result + + +@dataclass +class OneOf: + const: str + title: str + + @staticmethod + def from_dict(obj: Any) -> 'OneOf': + assert isinstance(obj, dict) + const = from_str(obj.get("const")) + title = from_str(obj.get("title")) + return OneOf(const, title) + + def to_dict(self) -> dict: + result: dict = {} + result["const"] = from_str(self.const) + result["title"] = from_str(self.title) + return result + + +class PropertyType(Enum): + ARRAY = "array" + BOOLEAN = "boolean" + INTEGER = "integer" + NUMBER = "number" + STRING = "string" + + +@dataclass +class Property: + type: PropertyType + default: float | bool | list[str] | str | None = None + description: str | None = None + enum: list[str] | None = None + enum_names: list[str] | None = None + title: str | None = None + one_of: list[OneOf] | None = None + items: Items | None = None + max_items: float | None = None + min_items: float | None = None + format: Format | None = None + max_length: float | None = None + min_length: float | None = None + maximum: float | None = None + minimum: float | None = None + + @staticmethod + def from_dict(obj: Any) -> 'Property': + assert isinstance(obj, dict) + type = PropertyType(obj.get("type")) + default = from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], obj.get("default")) + description = from_union([from_str, from_none], obj.get("description")) + enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum")) + enum_names = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enumNames")) + title = from_union([from_str, from_none], obj.get("title")) + one_of = from_union([lambda x: from_list(OneOf.from_dict, x), from_none], obj.get("oneOf")) + items = from_union([Items.from_dict, from_none], obj.get("items")) + max_items = from_union([from_float, from_none], obj.get("maxItems")) + min_items = from_union([from_float, from_none], obj.get("minItems")) + format = from_union([Format, from_none], obj.get("format")) + max_length = from_union([from_float, from_none], obj.get("maxLength")) + min_length = from_union([from_float, from_none], obj.get("minLength")) + maximum = from_union([from_float, from_none], obj.get("maximum")) + minimum = from_union([from_float, from_none], obj.get("minimum")) + return Property(type, default, description, enum, enum_names, title, one_of, items, max_items, min_items, format, max_length, min_length, maximum, minimum) + + def to_dict(self) -> dict: + result: dict = {} + result["type"] = to_enum(PropertyType, self.type) + if self.default is not None: + result["default"] = from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], self.default) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.enum is not None: + result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum) + if self.enum_names is not None: + result["enumNames"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum_names) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + if self.one_of is not None: + result["oneOf"] = from_union([lambda x: from_list(lambda x: to_class(OneOf, x), x), from_none], self.one_of) + if self.items is not None: + result["items"] = from_union([lambda x: to_class(Items, x), from_none], self.items) + if self.max_items is not None: + result["maxItems"] = from_union([to_float, from_none], self.max_items) + if self.min_items is not None: + result["minItems"] = from_union([to_float, from_none], self.min_items) + if self.format is not None: + result["format"] = from_union([lambda x: to_enum(Format, x), from_none], self.format) + if self.max_length is not None: + result["maxLength"] = from_union([to_float, from_none], self.max_length) + if self.min_length is not None: + result["minLength"] = from_union([to_float, from_none], self.min_length) + if self.maximum is not None: + result["maximum"] = from_union([to_float, from_none], self.maximum) + if self.minimum is not None: + result["minimum"] = from_union([to_float, from_none], self.minimum) + return result + + +class RequestedSchemaType(Enum): + OBJECT = "object" + + +@dataclass +class RequestedSchema: + """JSON Schema describing the form fields to present to the user""" + + properties: dict[str, Property] + """Form field definitions, keyed by field name""" + + type: RequestedSchemaType + """Schema type indicator (always 'object')""" + + required: list[str] | None = None + """List of required field names""" + + @staticmethod + def from_dict(obj: Any) -> 'RequestedSchema': + assert isinstance(obj, dict) + properties = from_dict(Property.from_dict, obj.get("properties")) + type = RequestedSchemaType(obj.get("type")) + required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required")) + return RequestedSchema(properties, type, required) + + def to_dict(self) -> dict: + result: dict = {} + result["properties"] = from_dict(lambda x: to_class(Property, x), self.properties) + result["type"] = to_enum(RequestedSchemaType, self.type) + if self.required is not None: + result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required) + return result + + +@dataclass +class SessionUIElicitationParams: + message: str + """Message describing what information is needed from the user""" + + requested_schema: RequestedSchema + """JSON Schema describing the form fields to present to the user""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionUIElicitationParams': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + requested_schema = RequestedSchema.from_dict(obj.get("requestedSchema")) + return SessionUIElicitationParams(message, requested_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + result["requestedSchema"] = to_class(RequestedSchema, self.requested_schema) + return result + + +@dataclass +class SessionPermissionsHandlePendingPermissionRequestResult: + success: bool + """Whether the permission request was handled successfully""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return SessionPermissionsHandlePendingPermissionRequestResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + + +class Kind(Enum): + APPROVED = "approved" + DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" + DENIED_BY_RULES = "denied-by-rules" + DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" + DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" + + +@dataclass +class SessionPermissionsHandlePendingPermissionRequestParamsResult: + kind: Kind + rules: list[Any] | None = None + feedback: str | None = None + message: str | None = None + path: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult': + assert isinstance(obj, dict) + kind = Kind(obj.get("kind")) + rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules")) + feedback = from_union([from_str, from_none], obj.get("feedback")) + message = from_union([from_str, from_none], obj.get("message")) + path = from_union([from_str, from_none], obj.get("path")) + return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(Kind, self.kind) + if self.rules is not None: + result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules) + if self.feedback is not None: + result["feedback"] = from_union([from_str, from_none], self.feedback) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) + return result + + +@dataclass +class SessionPermissionsHandlePendingPermissionRequestParams: + request_id: str + result: SessionPermissionsHandlePendingPermissionRequestParamsResult + + @staticmethod + def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result")) + return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result) + return result + + +@dataclass +class SessionLogResult: + event_id: UUID + """The unique identifier of the emitted session event""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogResult': + assert isinstance(obj, dict) + event_id = UUID(obj.get("eventId")) + return SessionLogResult(event_id) + + def to_dict(self) -> dict: + result: dict = {} + result["eventId"] = str(self.event_id) + return result + + +class Level(Enum): + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + ERROR = "error" + INFO = "info" + WARNING = "warning" + + +@dataclass +class SessionLogParams: + message: str + """Human-readable message""" + + ephemeral: bool | None = None + """When true, the message is transient and not persisted to the session event log on disk""" + + level: Level | None = None + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + url: str | None = None + """Optional URL the user can open in their browser for more details""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogParams': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + ephemeral = from_union([from_bool, from_none], obj.get("ephemeral")) + level = from_union([Level, from_none], obj.get("level")) + url = from_union([from_str, from_none], obj.get("url")) + return SessionLogParams(message, ephemeral, level, url) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.ephemeral is not None: + result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral) + if self.level is not None: + result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) + return result + + +@dataclass +class SessionShellExecResult: + process_id: str + """Unique identifier for tracking streamed output""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellExecResult': + assert isinstance(obj, dict) + process_id = from_str(obj.get("processId")) + return SessionShellExecResult(process_id) + + def to_dict(self) -> dict: + result: dict = {} + result["processId"] = from_str(self.process_id) + return result + + +@dataclass +class SessionShellExecParams: + command: str + """Shell command to execute""" + + cwd: str | None = None + """Working directory (defaults to session working directory)""" + + timeout: float | None = None + """Timeout in milliseconds (default: 30000)""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellExecParams': + assert isinstance(obj, dict) + command = from_str(obj.get("command")) + cwd = from_union([from_str, from_none], obj.get("cwd")) + timeout = from_union([from_float, from_none], obj.get("timeout")) + return SessionShellExecParams(command, cwd, timeout) + + def to_dict(self) -> dict: + result: dict = {} + result["command"] = from_str(self.command) + if self.cwd is not None: + result["cwd"] = from_union([from_str, from_none], self.cwd) + if self.timeout is not None: + result["timeout"] = from_union([to_float, from_none], self.timeout) + return result + + +@dataclass +class SessionShellKillResult: + killed: bool + """Whether the signal was sent successfully""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellKillResult': + assert isinstance(obj, dict) + killed = from_bool(obj.get("killed")) + return SessionShellKillResult(killed) + + def to_dict(self) -> dict: + result: dict = {} + result["killed"] = from_bool(self.killed) + return result + + +class Signal(Enum): + """Signal to send (default: SIGTERM)""" + + SIGINT = "SIGINT" + SIGKILL = "SIGKILL" + SIGTERM = "SIGTERM" + + +@dataclass +class SessionShellKillParams: + process_id: str + """Process identifier returned by shell.exec""" + + signal: Signal | None = None + """Signal to send (default: SIGTERM)""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionShellKillParams': + assert isinstance(obj, dict) + process_id = from_str(obj.get("processId")) + signal = from_union([Signal, from_none], obj.get("signal")) + return SessionShellKillParams(process_id, signal) + + def to_dict(self) -> dict: + result: dict = {} + result["processId"] = from_str(self.process_id) + if self.signal is not None: + result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal) + return result + + +def ping_result_from_dict(s: Any) -> PingResult: + return PingResult.from_dict(s) + + def ping_result_to_dict(x: PingResult) -> Any: return to_class(PingResult, x) @@ -1477,6 +2277,166 @@ def session_agent_deselect_result_to_dict(x: SessionAgentDeselectResult) -> Any: return to_class(SessionAgentDeselectResult, x) +def session_agent_reload_result_from_dict(s: Any) -> SessionAgentReloadResult: + return SessionAgentReloadResult.from_dict(s) + + +def session_agent_reload_result_to_dict(x: SessionAgentReloadResult) -> Any: + return to_class(SessionAgentReloadResult, x) + + +def session_skills_list_result_from_dict(s: Any) -> SessionSkillsListResult: + return SessionSkillsListResult.from_dict(s) + + +def session_skills_list_result_to_dict(x: SessionSkillsListResult) -> Any: + return to_class(SessionSkillsListResult, x) + + +def session_skills_enable_result_from_dict(s: Any) -> SessionSkillsEnableResult: + return SessionSkillsEnableResult.from_dict(s) + + +def session_skills_enable_result_to_dict(x: SessionSkillsEnableResult) -> Any: + return to_class(SessionSkillsEnableResult, x) + + +def session_skills_enable_params_from_dict(s: Any) -> SessionSkillsEnableParams: + return SessionSkillsEnableParams.from_dict(s) + + +def session_skills_enable_params_to_dict(x: SessionSkillsEnableParams) -> Any: + return to_class(SessionSkillsEnableParams, x) + + +def session_skills_disable_result_from_dict(s: Any) -> SessionSkillsDisableResult: + return SessionSkillsDisableResult.from_dict(s) + + +def session_skills_disable_result_to_dict(x: SessionSkillsDisableResult) -> Any: + return to_class(SessionSkillsDisableResult, x) + + +def session_skills_disable_params_from_dict(s: Any) -> SessionSkillsDisableParams: + return SessionSkillsDisableParams.from_dict(s) + + +def session_skills_disable_params_to_dict(x: SessionSkillsDisableParams) -> Any: + return to_class(SessionSkillsDisableParams, x) + + +def session_skills_reload_result_from_dict(s: Any) -> SessionSkillsReloadResult: + return SessionSkillsReloadResult.from_dict(s) + + +def session_skills_reload_result_to_dict(x: SessionSkillsReloadResult) -> Any: + return to_class(SessionSkillsReloadResult, x) + + +def session_mcp_list_result_from_dict(s: Any) -> SessionMCPListResult: + return SessionMCPListResult.from_dict(s) + + +def session_mcp_list_result_to_dict(x: SessionMCPListResult) -> Any: + return to_class(SessionMCPListResult, x) + + +def session_mcp_enable_result_from_dict(s: Any) -> SessionMCPEnableResult: + return SessionMCPEnableResult.from_dict(s) + + +def session_mcp_enable_result_to_dict(x: SessionMCPEnableResult) -> Any: + return to_class(SessionMCPEnableResult, x) + + +def session_mcp_enable_params_from_dict(s: Any) -> SessionMCPEnableParams: + return SessionMCPEnableParams.from_dict(s) + + +def session_mcp_enable_params_to_dict(x: SessionMCPEnableParams) -> Any: + return to_class(SessionMCPEnableParams, x) + + +def session_mcp_disable_result_from_dict(s: Any) -> SessionMCPDisableResult: + return SessionMCPDisableResult.from_dict(s) + + +def session_mcp_disable_result_to_dict(x: SessionMCPDisableResult) -> Any: + return to_class(SessionMCPDisableResult, x) + + +def session_mcp_disable_params_from_dict(s: Any) -> SessionMCPDisableParams: + return SessionMCPDisableParams.from_dict(s) + + +def session_mcp_disable_params_to_dict(x: SessionMCPDisableParams) -> Any: + return to_class(SessionMCPDisableParams, x) + + +def session_mcp_reload_result_from_dict(s: Any) -> SessionMCPReloadResult: + return SessionMCPReloadResult.from_dict(s) + + +def session_mcp_reload_result_to_dict(x: SessionMCPReloadResult) -> Any: + return to_class(SessionMCPReloadResult, x) + + +def session_plugins_list_result_from_dict(s: Any) -> SessionPluginsListResult: + return SessionPluginsListResult.from_dict(s) + + +def session_plugins_list_result_to_dict(x: SessionPluginsListResult) -> Any: + return to_class(SessionPluginsListResult, x) + + +def session_extensions_list_result_from_dict(s: Any) -> SessionExtensionsListResult: + return SessionExtensionsListResult.from_dict(s) + + +def session_extensions_list_result_to_dict(x: SessionExtensionsListResult) -> Any: + return to_class(SessionExtensionsListResult, x) + + +def session_extensions_enable_result_from_dict(s: Any) -> SessionExtensionsEnableResult: + return SessionExtensionsEnableResult.from_dict(s) + + +def session_extensions_enable_result_to_dict(x: SessionExtensionsEnableResult) -> Any: + return to_class(SessionExtensionsEnableResult, x) + + +def session_extensions_enable_params_from_dict(s: Any) -> SessionExtensionsEnableParams: + return SessionExtensionsEnableParams.from_dict(s) + + +def session_extensions_enable_params_to_dict(x: SessionExtensionsEnableParams) -> Any: + return to_class(SessionExtensionsEnableParams, x) + + +def session_extensions_disable_result_from_dict(s: Any) -> SessionExtensionsDisableResult: + return SessionExtensionsDisableResult.from_dict(s) + + +def session_extensions_disable_result_to_dict(x: SessionExtensionsDisableResult) -> Any: + return to_class(SessionExtensionsDisableResult, x) + + +def session_extensions_disable_params_from_dict(s: Any) -> SessionExtensionsDisableParams: + return SessionExtensionsDisableParams.from_dict(s) + + +def session_extensions_disable_params_to_dict(x: SessionExtensionsDisableParams) -> Any: + return to_class(SessionExtensionsDisableParams, x) + + +def session_extensions_reload_result_from_dict(s: Any) -> SessionExtensionsReloadResult: + return SessionExtensionsReloadResult.from_dict(s) + + +def session_extensions_reload_result_to_dict(x: SessionExtensionsReloadResult) -> Any: + return to_class(SessionExtensionsReloadResult, x) + + def session_compaction_compact_result_from_dict(s: Any) -> SessionCompactionCompactResult: return SessionCompactionCompactResult.from_dict(s) @@ -1501,6 +2461,38 @@ def session_tools_handle_pending_tool_call_params_to_dict(x: SessionToolsHandleP return to_class(SessionToolsHandlePendingToolCallParams, x) +def session_commands_handle_pending_command_result_from_dict(s: Any) -> SessionCommandsHandlePendingCommandResult: + return SessionCommandsHandlePendingCommandResult.from_dict(s) + + +def session_commands_handle_pending_command_result_to_dict(x: SessionCommandsHandlePendingCommandResult) -> Any: + return to_class(SessionCommandsHandlePendingCommandResult, x) + + +def session_commands_handle_pending_command_params_from_dict(s: Any) -> SessionCommandsHandlePendingCommandParams: + return SessionCommandsHandlePendingCommandParams.from_dict(s) + + +def session_commands_handle_pending_command_params_to_dict(x: SessionCommandsHandlePendingCommandParams) -> Any: + return to_class(SessionCommandsHandlePendingCommandParams, x) + + +def session_ui_elicitation_result_from_dict(s: Any) -> SessionUIElicitationResult: + return SessionUIElicitationResult.from_dict(s) + + +def session_ui_elicitation_result_to_dict(x: SessionUIElicitationResult) -> Any: + return to_class(SessionUIElicitationResult, x) + + +def session_ui_elicitation_params_from_dict(s: Any) -> SessionUIElicitationParams: + return SessionUIElicitationParams.from_dict(s) + + +def session_ui_elicitation_params_to_dict(x: SessionUIElicitationParams) -> Any: + return to_class(SessionUIElicitationParams, x) + + def session_permissions_handle_pending_permission_request_result_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestResult: return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(s) @@ -1706,6 +2698,88 @@ async def select(self, params: SessionAgentSelectParams, *, timeout: float | Non async def deselect(self, *, timeout: float | None = None) -> SessionAgentDeselectResult: return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def reload(self, *, timeout: float | None = None) -> SessionAgentReloadResult: + return SessionAgentReloadResult.from_dict(await self._client.request("session.agent.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class SkillsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, *, timeout: float | None = None) -> SessionSkillsListResult: + return SessionSkillsListResult.from_dict(await self._client.request("session.skills.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def enable(self, params: SessionSkillsEnableParams, *, timeout: float | None = None) -> SessionSkillsEnableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionSkillsEnableResult.from_dict(await self._client.request("session.skills.enable", params_dict, **_timeout_kwargs(timeout))) + + async def disable(self, params: SessionSkillsDisableParams, *, timeout: float | None = None) -> SessionSkillsDisableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionSkillsDisableResult.from_dict(await self._client.request("session.skills.disable", params_dict, **_timeout_kwargs(timeout))) + + async def reload(self, *, timeout: float | None = None) -> SessionSkillsReloadResult: + return SessionSkillsReloadResult.from_dict(await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class McpApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, *, timeout: float | None = None) -> SessionMCPListResult: + return SessionMCPListResult.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def enable(self, params: SessionMCPEnableParams, *, timeout: float | None = None) -> SessionMCPEnableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionMCPEnableResult.from_dict(await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout))) + + async def disable(self, params: SessionMCPDisableParams, *, timeout: float | None = None) -> SessionMCPDisableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionMCPDisableResult.from_dict(await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout))) + + async def reload(self, *, timeout: float | None = None) -> SessionMCPReloadResult: + return SessionMCPReloadResult.from_dict(await self._client.request("session.mcp.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class PluginsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, *, timeout: float | None = None) -> SessionPluginsListResult: + return SessionPluginsListResult.from_dict(await self._client.request("session.plugins.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class ExtensionsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, *, timeout: float | None = None) -> SessionExtensionsListResult: + return SessionExtensionsListResult.from_dict(await self._client.request("session.extensions.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def enable(self, params: SessionExtensionsEnableParams, *, timeout: float | None = None) -> SessionExtensionsEnableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionExtensionsEnableResult.from_dict(await self._client.request("session.extensions.enable", params_dict, **_timeout_kwargs(timeout))) + + async def disable(self, params: SessionExtensionsDisableParams, *, timeout: float | None = None) -> SessionExtensionsDisableResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionExtensionsDisableResult.from_dict(await self._client.request("session.extensions.disable", params_dict, **_timeout_kwargs(timeout))) + + async def reload(self, *, timeout: float | None = None) -> SessionExtensionsReloadResult: + return SessionExtensionsReloadResult.from_dict(await self._client.request("session.extensions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + # Experimental: this API group is experimental and may change or be removed. class CompactionApi: @@ -1728,6 +2802,28 @@ async def handle_pending_tool_call(self, params: SessionToolsHandlePendingToolCa return SessionToolsHandlePendingToolCallResult.from_dict(await self._client.request("session.tools.handlePendingToolCall", params_dict, **_timeout_kwargs(timeout))) +class CommandsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def handle_pending_command(self, params: SessionCommandsHandlePendingCommandParams, *, timeout: float | None = None) -> SessionCommandsHandlePendingCommandResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionCommandsHandlePendingCommandResult.from_dict(await self._client.request("session.commands.handlePendingCommand", params_dict, **_timeout_kwargs(timeout))) + + +class UiApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def elicitation(self, params: SessionUIElicitationParams, *, timeout: float | None = None) -> SessionUIElicitationResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionUIElicitationResult.from_dict(await self._client.request("session.ui.elicitation", params_dict, **_timeout_kwargs(timeout))) + + class PermissionsApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -1766,8 +2862,14 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.workspace = WorkspaceApi(client, session_id) self.fleet = FleetApi(client, session_id) self.agent = AgentApi(client, session_id) + self.skills = SkillsApi(client, session_id) + self.mcp = McpApi(client, session_id) + self.plugins = PluginsApi(client, session_id) + self.extensions = ExtensionsApi(client, session_id) self.compaction = CompactionApi(client, session_id) self.tools = ToolsApi(client, session_id) + self.commands = CommandsApi(client, session_id) + self.ui = UiApi(client, session_id) self.permissions = PermissionsApi(client, session_id) self.shell = ShellApi(client, session_id) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 3fc3133991..f3970b8155 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -419,6 +419,26 @@ def to_dict(self) -> dict: return result +@dataclass +class DataCommand: + name: str + description: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'DataCommand': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + description = from_union([from_str, from_none], obj.get("description")) + return DataCommand(name, description) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + return result + + @dataclass class CompactionTokensUsed: """Token usage breakdown for the compaction LLM call""" @@ -605,7 +625,55 @@ def to_dict(self) -> dict: return result -class Status(Enum): +class Source(Enum): + """Discovery source""" + + PROJECT = "project" + USER = "user" + + +class ExtensionStatus(Enum): + """Current status: running, disabled, failed, or starting""" + + DISABLED = "disabled" + FAILED = "failed" + RUNNING = "running" + STARTING = "starting" + + +@dataclass +class Extension: + id: str + """Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')""" + + name: str + """Extension name (directory name)""" + + source: Source + """Discovery source""" + + status: ExtensionStatus + """Current status: running, disabled, failed, or starting""" + + @staticmethod + def from_dict(obj: Any) -> 'Extension': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + name = from_str(obj.get("name")) + source = Source(obj.get("source")) + status = ExtensionStatus(obj.get("status")) + return Extension(id, name, source, status) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["name"] = from_str(self.name) + result["source"] = to_enum(Source, self.source) + result["status"] = to_enum(ExtensionStatus, self.status) + return result + + +class KindStatus(Enum): """Whether the agent completed successfully or failed""" COMPLETED = "completed" @@ -614,6 +682,7 @@ class Status(Enum): class KindType(Enum): AGENT_COMPLETED = "agent_completed" + AGENT_IDLE = "agent_idle" SHELL_COMPLETED = "shell_completed" SHELL_DETACHED_COMPLETED = "shell_detached_completed" @@ -637,7 +706,7 @@ class KindClass: prompt: str | None = None """The full prompt given to the background agent""" - status: Status | None = None + status: KindStatus | None = None """Whether the agent completed successfully or failed""" exit_code: float | None = None @@ -657,7 +726,7 @@ def from_dict(obj: Any) -> 'KindClass': agent_type = from_union([from_str, from_none], obj.get("agentType")) description = from_union([from_str, from_none], obj.get("description")) prompt = from_union([from_str, from_none], obj.get("prompt")) - status = from_union([Status, from_none], obj.get("status")) + status = from_union([KindStatus, from_none], obj.get("status")) exit_code = from_union([from_float, from_none], obj.get("exitCode")) shell_id = from_union([from_str, from_none], obj.get("shellId")) return KindClass(type, agent_id, agent_type, description, prompt, status, exit_code, shell_id) @@ -674,7 +743,7 @@ def to_dict(self) -> dict: if self.prompt is not None: result["prompt"] = from_union([from_str, from_none], self.prompt) if self.status is not None: - result["status"] = from_union([lambda x: to_enum(Status, x), from_none], self.status) + result["status"] = from_union([lambda x: to_enum(KindStatus, x), from_none], self.status) if self.exit_code is not None: result["exitCode"] = from_union([to_float, from_none], self.exit_code) if self.shell_id is not None: @@ -709,7 +778,11 @@ def to_dict(self) -> dict: class Mode(Enum): + """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to + "form" when absent. + """ FORM = "form" + URL = "url" @dataclass @@ -803,7 +876,7 @@ class Operation(Enum): @dataclass -class Command: +class PermissionRequestCommand: identifier: str """Command identifier (e.g., executable name)""" @@ -811,11 +884,11 @@ class Command: """Whether this command is read-only (no side effects)""" @staticmethod - def from_dict(obj: Any) -> 'Command': + def from_dict(obj: Any) -> 'PermissionRequestCommand': assert isinstance(obj, dict) identifier = from_str(obj.get("identifier")) read_only = from_bool(obj.get("readOnly")) - return Command(identifier, read_only) + return PermissionRequestCommand(identifier, read_only) def to_dict(self) -> dict: result: dict = {} @@ -878,7 +951,7 @@ class PermissionRequest: can_offer_session_approval: bool | None = None """Whether the UI can offer session-wide approval for this command pattern""" - commands: list[Command] | None = None + commands: list[PermissionRequestCommand] | None = None """Parsed command identifiers found in the command text""" full_command_text: str | None = None @@ -967,7 +1040,7 @@ def from_dict(obj: Any) -> 'PermissionRequest': assert isinstance(obj, dict) kind = PermissionRequestKind(obj.get("kind")) can_offer_session_approval = from_union([from_bool, from_none], obj.get("canOfferSessionApproval")) - commands = from_union([lambda x: from_list(Command.from_dict, x), from_none], obj.get("commands")) + commands = from_union([lambda x: from_list(PermissionRequestCommand.from_dict, x), from_none], obj.get("commands")) full_command_text = from_union([from_str, from_none], obj.get("fullCommandText")) has_write_file_redirection = from_union([from_bool, from_none], obj.get("hasWriteFileRedirection")) intention = from_union([from_str, from_none], obj.get("intention")) @@ -999,7 +1072,7 @@ def to_dict(self) -> dict: if self.can_offer_session_approval is not None: result["canOfferSessionApproval"] = from_union([from_bool, from_none], self.can_offer_session_approval) if self.commands is not None: - result["commands"] = from_union([lambda x: from_list(lambda x: to_class(Command, x), x), from_none], self.commands) + result["commands"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRequestCommand, x), x), from_none], self.commands) if self.full_command_text is not None: result["fullCommandText"] = from_union([from_str, from_none], self.full_command_text) if self.has_write_file_redirection is not None: @@ -1138,7 +1211,7 @@ class RequestedSchemaType(Enum): @dataclass class RequestedSchema: - """JSON Schema describing the form fields to present to the user""" + """JSON Schema describing the form fields to present to the user (form mode only)""" properties: dict[str, Any] """Form field definitions, keyed by field name""" @@ -1430,6 +1503,52 @@ class Role(Enum): SYSTEM = "system" +class ServerStatus(Enum): + """Connection status: connected, failed, pending, disabled, or not_configured + + New connection status: connected, failed, pending, disabled, or not_configured + """ + CONNECTED = "connected" + DISABLED = "disabled" + FAILED = "failed" + NOT_CONFIGURED = "not_configured" + PENDING = "pending" + + +@dataclass +class Server: + name: str + """Server name (config key)""" + + status: ServerStatus + """Connection status: connected, failed, pending, disabled, or not_configured""" + + error: str | None = None + """Error message if the server failed to connect""" + + source: str | None = None + """Configuration source: user, workspace, plugin, or builtin""" + + @staticmethod + def from_dict(obj: Any) -> 'Server': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + status = ServerStatus(obj.get("status")) + error = from_union([from_str, from_none], obj.get("error")) + source = from_union([from_str, from_none], obj.get("source")) + return Server(name, status, error, source) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["status"] = to_enum(ServerStatus, self.status) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + + class ShutdownType(Enum): """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" @@ -1437,20 +1556,47 @@ class ShutdownType(Enum): ROUTINE = "routine" -class Source(Enum): - """Origin of this message, used for timeline filtering and telemetry (e.g., "user", - "autopilot", "skill", or "command") - """ - AUTOPILOT = "autopilot" - COMMAND = "command" - IMMEDIATE_PROMPT = "immediate-prompt" - JIT_INSTRUCTION = "jit-instruction" - OTHER = "other" - SKILL = "skill" - SNIPPY_BLOCKING = "snippy-blocking" - SYSTEM = "system" - THINKING_EXHAUSTED_CONTINUATION = "thinking-exhausted-continuation" - USER = "user" +@dataclass +class Skill: + description: str + """Description of what the skill does""" + + enabled: bool + """Whether the skill is currently enabled""" + + name: str + """Unique identifier for the skill""" + + source: str + """Source location type of the skill (e.g., project, personal, plugin)""" + + user_invocable: bool + """Whether the skill can be invoked by the user as a slash command""" + + path: str | None = None + """Absolute path to the skill file, if available""" + + @staticmethod + def from_dict(obj: Any) -> 'Skill': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + enabled = from_bool(obj.get("enabled")) + name = from_str(obj.get("name")) + source = from_str(obj.get("source")) + user_invocable = from_bool(obj.get("userInvocable")) + path = from_union([from_str, from_none], obj.get("path")) + return Skill(description, enabled, name, source, user_invocable, path) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["enabled"] = from_bool(self.enabled) + result["name"] = from_str(self.name) + result["source"] = from_str(self.source) + result["userInvocable"] = from_bool(self.user_invocable) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) + return result class SourceType(Enum): @@ -1460,6 +1606,31 @@ class SourceType(Enum): REMOTE = "remote" +@dataclass +class StaticClientConfig: + """Static OAuth client configuration, if the server specifies one""" + + client_id: str + """OAuth client ID for the server""" + + public_client: bool | None = None + """Whether this is a public OAuth client""" + + @staticmethod + def from_dict(obj: Any) -> 'StaticClientConfig': + assert isinstance(obj, dict) + client_id = from_str(obj.get("clientId")) + public_client = from_union([from_bool, from_none], obj.get("publicClient")) + return StaticClientConfig(client_id, public_client) + + def to_dict(self) -> dict: + result: dict = {} + result["clientId"] = from_str(self.client_id) + if self.public_client is not None: + result["publicClient"] = from_union([from_bool, from_none], self.public_client) + return result + + class ToolRequestType(Enum): """Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. @@ -1555,15 +1726,12 @@ class Data: Current context window usage statistics including token and message counts - Empty payload; the event signals that LLM-powered conversation compaction has begun + Context window breakdown at the start of LLM-powered conversation compaction Conversation compaction results including success status, metrics, and optional error details - Task completion notification with optional summary from the agent - - User message content with optional attachments, source information, and interaction - metadata + Task completion notification with summary from the agent Empty payload; the event signals that the pending message queue has changed @@ -1629,18 +1797,27 @@ class Data: User input request completion notification signaling UI dismissal - Structured form elicitation request with JSON schema definition for form fields + Elicitation request; may be form-based (structured input) or URL-based (browser + redirect) Elicitation request completion notification signaling UI dismissal + OAuth authentication request for an MCP server + + MCP OAuth request completion notification + External tool invocation request for client-side tool execution External tool completion notification signaling UI dismissal Queued slash command dispatch request for client execution + Registered command dispatch request routed to the owning client + Queued command completion notification signaling UI dismissal + SDK command registration change notification + Plan approval request with plan content and available user actions Plan mode exit completion notification signaling UI dismissal @@ -1716,6 +1893,15 @@ class Data: status_code: int | None = None """HTTP status code from the upstream request, if applicable""" + url: str | None = None + """Optional URL associated with this error that the user can open in a browser + + Optional URL associated with this message that the user can open in a browser + + Optional URL associated with this warning that the user can open in a browser + + URL to open in the user's browser (url mode only) + """ background_tasks: BackgroundTasks | None = None """Background tasks still running when the agent became idle""" @@ -1772,7 +1958,7 @@ class Data: summary: str | None = None """Summary of the work done in the source session - Optional summary of the completed task, provided by the agent + Summary of the completed task, provided by the agent Summary of the plan that was created """ @@ -1809,9 +1995,23 @@ class Data: code_changes: CodeChanges | None = None """Aggregate code change metrics for the session""" + conversation_tokens: float | None = None + """Non-system message token count at shutdown + + Token count from non-system messages (user, assistant, tool) + + Token count from non-system messages (user, assistant, tool) at compaction start + + Token count from non-system messages (user, assistant, tool) after compaction + """ current_model: str | None = None """Model that was selected at the time of shutdown""" + current_tokens: float | None = None + """Total tokens in context window at shutdown + + Current number of tokens in the context window + """ error_reason: str | None = None """Error description when shutdownType is "error\"""" @@ -1824,6 +2024,24 @@ class Data: shutdown_type: ShutdownType | None = None """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" + system_tokens: float | None = None + """System message token count at shutdown + + Token count from system message(s) + + Token count from system message(s) at compaction start + + Token count from system message(s) after compaction + """ + tool_definitions_tokens: float | None = None + """Tool definitions token count at shutdown + + Token count from tool definitions + + Token count from tool definitions at compaction start + + Token count from tool definitions after compaction + """ total_api_duration_ms: float | None = None """Cumulative time spent in API calls during the session, in milliseconds""" @@ -1848,8 +2066,8 @@ class Data: host_type: HostType | None = None """Hosting platform type of the repository (github or ado)""" - current_tokens: float | None = None - """Current number of tokens in the context window""" + is_initial: bool | None = None + """Whether this is the first usage_info event emitted in this session""" messages_length: float | None = None """Current number of messages in the conversation""" @@ -1905,6 +2123,11 @@ class Data: Request ID of the resolved elicitation request; clients should dismiss any UI for this request + Unique identifier for this OAuth request; used to respond via + session.respondToMcpOAuth() + + Request ID of the resolved OAuth request + Unique identifier for this request; used to respond via session.respondToExternalTool() Request ID of the resolved external tool request; clients should dismiss any UI for this @@ -1912,6 +2135,8 @@ class Data: Unique identifier for this request; used to respond via session.respondToQueuedCommand() + Unique identifier; used to respond via session.commands.handlePendingCommand() + Request ID of the resolved command request; clients should dismiss any UI for this request @@ -1923,6 +2148,8 @@ class Data: success: bool | None = None """Whether compaction completed successfully + Whether the tool call succeeded. False when validation failed (e.g., invalid arguments) + Whether the tool execution completed successfully Whether the hook completed successfully @@ -1961,9 +2188,9 @@ class Data: CAPI interaction ID for correlating this tool execution with upstream telemetry """ - source: Source | None = None - """Origin of this message, used for timeline filtering and telemetry (e.g., "user", - "autopilot", "skill", or "command") + source: str | None = None + """Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected + messages that should be hidden from the user) """ transformed_content: str | None = None """Transformed version of the message sent to the model, with XML wrapping, timestamps, and @@ -2077,6 +2304,12 @@ class Data: Tool call ID of the parent tool invocation that spawned this sub-agent + The LLM-assigned tool call ID that triggered this request; used by remote UIs to + correlate responses + + Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id + for remote UIs + Tool call ID assigned to this external tool invocation """ tool_name: str | None = None @@ -2176,11 +2409,26 @@ class Data: question: str | None = None """The question or prompt to present to the user""" - mode: Mode | None = None - """Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.""" + elicitation_source: str | None = None + """The source that initiated the request (MCP server name, or absent for agent-initiated)""" + mode: Mode | None = None + """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to + "form" when absent. + """ requested_schema: RequestedSchema | None = None - """JSON Schema describing the form fields to present to the user""" + """JSON Schema describing the form fields to present to the user (form mode only)""" + + server_name: str | None = None + """Display name of the MCP server that requires OAuth + + Name of the MCP server whose status changed + """ + server_url: str | None = None + """URL of the MCP server that requires OAuth""" + + static_client_config: StaticClientConfig | None = None + """Static OAuth client configuration, if the server specifies one""" traceparent: str | None = None """W3C Trace Context traceparent header for the execute_tool span""" @@ -2189,7 +2437,18 @@ class Data: """W3C Trace Context tracestate header for the execute_tool span""" command: str | None = None - """The slash command text to be executed (e.g., /help, /clear)""" + """The slash command text to be executed (e.g., /help, /clear) + + The full command text (e.g., /deploy production) + """ + args: str | None = None + """Raw argument string after the command name""" + + command_name: str | None = None + """Command name without leading /""" + + commands: list[DataCommand] | None = None + """Current list of registered SDK commands""" actions: list[str] | None = None """Available actions the user can take (e.g., approve, edit, reject)""" @@ -2200,6 +2459,18 @@ class Data: recommended_action: str | None = None """The recommended action for the user to take""" + skills: list[Skill] | None = None + """Array of resolved skill metadata""" + + servers: list[Server] | None = None + """Array of MCP server status summaries""" + + status: ServerStatus | None = None + """New connection status: connected, failed, pending, disabled, or not_configured""" + + extensions: list[Extension] | None = None + """Array of discovered extensions and their status""" + @staticmethod def from_dict(obj: Any) -> 'Data': assert isinstance(obj, dict) @@ -2219,6 +2490,7 @@ def from_dict(obj: Any) -> 'Data': provider_call_id = from_union([from_str, from_none], obj.get("providerCallId")) stack = from_union([from_str, from_none], obj.get("stack")) status_code = from_union([from_int, from_none], obj.get("statusCode")) + url = from_union([from_str, from_none], obj.get("url")) background_tasks = from_union([BackgroundTasks.from_dict, from_none], obj.get("backgroundTasks")) title = from_union([from_str, from_none], obj.get("title")) info_type = from_union([from_str, from_none], obj.get("infoType")) @@ -2246,11 +2518,15 @@ def from_dict(obj: Any) -> 'Data': events_removed = from_union([from_float, from_none], obj.get("eventsRemoved")) up_to_event_id = from_union([from_str, from_none], obj.get("upToEventId")) code_changes = from_union([CodeChanges.from_dict, from_none], obj.get("codeChanges")) + conversation_tokens = from_union([from_float, from_none], obj.get("conversationTokens")) current_model = from_union([from_str, from_none], obj.get("currentModel")) + current_tokens = from_union([from_float, from_none], obj.get("currentTokens")) error_reason = from_union([from_str, from_none], obj.get("errorReason")) model_metrics = from_union([lambda x: from_dict(ModelMetric.from_dict, x), from_none], obj.get("modelMetrics")) session_start_time = from_union([from_float, from_none], obj.get("sessionStartTime")) shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType")) + system_tokens = from_union([from_float, from_none], obj.get("systemTokens")) + tool_definitions_tokens = from_union([from_float, from_none], obj.get("toolDefinitionsTokens")) total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs")) total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests")) base_commit = from_union([from_str, from_none], obj.get("baseCommit")) @@ -2259,7 +2535,7 @@ def from_dict(obj: Any) -> 'Data': git_root = from_union([from_str, from_none], obj.get("gitRoot")) head_commit = from_union([from_str, from_none], obj.get("headCommit")) host_type = from_union([HostType, from_none], obj.get("hostType")) - current_tokens = from_union([from_float, from_none], obj.get("currentTokens")) + is_initial = from_union([from_bool, from_none], obj.get("isInitial")) messages_length = from_union([from_float, from_none], obj.get("messagesLength")) checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber")) checkpoint_path = from_union([from_str, from_none], obj.get("checkpointPath")) @@ -2277,7 +2553,7 @@ def from_dict(obj: Any) -> 'Data': attachments = from_union([lambda x: from_list(Attachment.from_dict, x), from_none], obj.get("attachments")) content = from_union([from_str, from_none], obj.get("content")) interaction_id = from_union([from_str, from_none], obj.get("interactionId")) - source = from_union([Source, from_none], obj.get("source")) + source = from_union([from_str, from_none], obj.get("source")) transformed_content = from_union([from_str, from_none], obj.get("transformedContent")) turn_id = from_union([from_str, from_none], obj.get("turnId")) intent = from_union([from_str, from_none], obj.get("intent")) @@ -2332,15 +2608,26 @@ def from_dict(obj: Any) -> 'Data': allow_freeform = from_union([from_bool, from_none], obj.get("allowFreeform")) choices = from_union([lambda x: from_list(from_str, x), from_none], obj.get("choices")) question = from_union([from_str, from_none], obj.get("question")) + elicitation_source = from_union([from_str, from_none], obj.get("elicitationSource")) mode = from_union([Mode, from_none], obj.get("mode")) requested_schema = from_union([RequestedSchema.from_dict, from_none], obj.get("requestedSchema")) + server_name = from_union([from_str, from_none], obj.get("serverName")) + server_url = from_union([from_str, from_none], obj.get("serverUrl")) + static_client_config = from_union([StaticClientConfig.from_dict, from_none], obj.get("staticClientConfig")) traceparent = from_union([from_str, from_none], obj.get("traceparent")) tracestate = from_union([from_str, from_none], obj.get("tracestate")) command = from_union([from_str, from_none], obj.get("command")) + args = from_union([from_str, from_none], obj.get("args")) + command_name = from_union([from_str, from_none], obj.get("commandName")) + commands = from_union([lambda x: from_list(DataCommand.from_dict, x), from_none], obj.get("commands")) actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions")) plan_content = from_union([from_str, from_none], obj.get("planContent")) recommended_action = from_union([from_str, from_none], obj.get("recommendedAction")) - return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, mode, requested_schema, traceparent, tracestate, command, actions, plan_content, recommended_action) + skills = from_union([lambda x: from_list(Skill.from_dict, x), from_none], obj.get("skills")) + servers = from_union([lambda x: from_list(Server.from_dict, x), from_none], obj.get("servers")) + status = from_union([ServerStatus, from_none], obj.get("status")) + extensions = from_union([lambda x: from_list(Extension.from_dict, x), from_none], obj.get("extensions")) + return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, servers, status, extensions) def to_dict(self) -> dict: result: dict = {} @@ -2376,6 +2663,8 @@ def to_dict(self) -> dict: result["stack"] = from_union([from_str, from_none], self.stack) if self.status_code is not None: result["statusCode"] = from_union([from_int, from_none], self.status_code) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) if self.background_tasks is not None: result["backgroundTasks"] = from_union([lambda x: to_class(BackgroundTasks, x), from_none], self.background_tasks) if self.title is not None: @@ -2430,8 +2719,12 @@ def to_dict(self) -> dict: result["upToEventId"] = from_union([from_str, from_none], self.up_to_event_id) if self.code_changes is not None: result["codeChanges"] = from_union([lambda x: to_class(CodeChanges, x), from_none], self.code_changes) + if self.conversation_tokens is not None: + result["conversationTokens"] = from_union([to_float, from_none], self.conversation_tokens) if self.current_model is not None: result["currentModel"] = from_union([from_str, from_none], self.current_model) + if self.current_tokens is not None: + result["currentTokens"] = from_union([to_float, from_none], self.current_tokens) if self.error_reason is not None: result["errorReason"] = from_union([from_str, from_none], self.error_reason) if self.model_metrics is not None: @@ -2440,6 +2733,10 @@ def to_dict(self) -> dict: result["sessionStartTime"] = from_union([to_float, from_none], self.session_start_time) if self.shutdown_type is not None: result["shutdownType"] = from_union([lambda x: to_enum(ShutdownType, x), from_none], self.shutdown_type) + if self.system_tokens is not None: + result["systemTokens"] = from_union([to_float, from_none], self.system_tokens) + if self.tool_definitions_tokens is not None: + result["toolDefinitionsTokens"] = from_union([to_float, from_none], self.tool_definitions_tokens) if self.total_api_duration_ms is not None: result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms) if self.total_premium_requests is not None: @@ -2456,8 +2753,8 @@ def to_dict(self) -> dict: result["headCommit"] = from_union([from_str, from_none], self.head_commit) if self.host_type is not None: result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type) - if self.current_tokens is not None: - result["currentTokens"] = from_union([to_float, from_none], self.current_tokens) + if self.is_initial is not None: + result["isInitial"] = from_union([from_bool, from_none], self.is_initial) if self.messages_length is not None: result["messagesLength"] = from_union([to_float, from_none], self.messages_length) if self.checkpoint_number is not None: @@ -2493,7 +2790,7 @@ def to_dict(self) -> dict: if self.interaction_id is not None: result["interactionId"] = from_union([from_str, from_none], self.interaction_id) if self.source is not None: - result["source"] = from_union([lambda x: to_enum(Source, x), from_none], self.source) + result["source"] = from_union([from_str, from_none], self.source) if self.transformed_content is not None: result["transformedContent"] = from_union([from_str, from_none], self.transformed_content) if self.turn_id is not None: @@ -2602,22 +2899,44 @@ def to_dict(self) -> dict: result["choices"] = from_union([lambda x: from_list(from_str, x), from_none], self.choices) if self.question is not None: result["question"] = from_union([from_str, from_none], self.question) + if self.elicitation_source is not None: + result["elicitationSource"] = from_union([from_str, from_none], self.elicitation_source) if self.mode is not None: result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode) if self.requested_schema is not None: result["requestedSchema"] = from_union([lambda x: to_class(RequestedSchema, x), from_none], self.requested_schema) + if self.server_name is not None: + result["serverName"] = from_union([from_str, from_none], self.server_name) + if self.server_url is not None: + result["serverUrl"] = from_union([from_str, from_none], self.server_url) + if self.static_client_config is not None: + result["staticClientConfig"] = from_union([lambda x: to_class(StaticClientConfig, x), from_none], self.static_client_config) if self.traceparent is not None: result["traceparent"] = from_union([from_str, from_none], self.traceparent) if self.tracestate is not None: result["tracestate"] = from_union([from_str, from_none], self.tracestate) if self.command is not None: result["command"] = from_union([from_str, from_none], self.command) + if self.args is not None: + result["args"] = from_union([from_str, from_none], self.args) + if self.command_name is not None: + result["commandName"] = from_union([from_str, from_none], self.command_name) + if self.commands is not None: + result["commands"] = from_union([lambda x: from_list(lambda x: to_class(DataCommand, x), x), from_none], self.commands) if self.actions is not None: result["actions"] = from_union([lambda x: from_list(from_str, x), from_none], self.actions) if self.plan_content is not None: result["planContent"] = from_union([from_str, from_none], self.plan_content) if self.recommended_action is not None: result["recommendedAction"] = from_union([from_str, from_none], self.recommended_action) + if self.skills is not None: + result["skills"] = from_union([lambda x: from_list(lambda x: to_class(Skill, x), x), from_none], self.skills) + if self.servers is not None: + result["servers"] = from_union([lambda x: from_list(lambda x: to_class(Server, x), x), from_none], self.servers) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(ServerStatus, x), from_none], self.status) + if self.extensions is not None: + result["extensions"] = from_union([lambda x: from_list(lambda x: to_class(Extension, x), x), from_none], self.extensions) return result @@ -2632,7 +2951,9 @@ class SessionEventType(Enum): ASSISTANT_TURN_END = "assistant.turn_end" ASSISTANT_TURN_START = "assistant.turn_start" ASSISTANT_USAGE = "assistant.usage" + COMMANDS_CHANGED = "commands.changed" COMMAND_COMPLETED = "command.completed" + COMMAND_EXECUTE = "command.execute" COMMAND_QUEUED = "command.queued" ELICITATION_COMPLETED = "elicitation.completed" ELICITATION_REQUESTED = "elicitation.requested" @@ -2642,6 +2963,8 @@ class SessionEventType(Enum): EXTERNAL_TOOL_REQUESTED = "external_tool.requested" HOOK_END = "hook.end" HOOK_START = "hook.start" + MCP_OAUTH_COMPLETED = "mcp.oauth_completed" + MCP_OAUTH_REQUIRED = "mcp.oauth_required" PENDING_MESSAGES_MODIFIED = "pending_messages.modified" PERMISSION_COMPLETED = "permission.completed" PERMISSION_REQUESTED = "permission.requested" @@ -2650,14 +2973,18 @@ class SessionEventType(Enum): SESSION_COMPACTION_START = "session.compaction_start" SESSION_CONTEXT_CHANGED = "session.context_changed" SESSION_ERROR = "session.error" + SESSION_EXTENSIONS_LOADED = "session.extensions_loaded" SESSION_HANDOFF = "session.handoff" SESSION_IDLE = "session.idle" SESSION_INFO = "session.info" + SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" + SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" SESSION_MODEL_CHANGE = "session.model_change" SESSION_MODE_CHANGED = "session.mode_changed" SESSION_PLAN_CHANGED = "session.plan_changed" SESSION_RESUME = "session.resume" SESSION_SHUTDOWN = "session.shutdown" + SESSION_SKILLS_LOADED = "session.skills_loaded" SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" SESSION_START = "session.start" SESSION_TASK_COMPLETE = "session.task_complete" @@ -2731,15 +3058,12 @@ class SessionEvent: Current context window usage statistics including token and message counts - Empty payload; the event signals that LLM-powered conversation compaction has begun + Context window breakdown at the start of LLM-powered conversation compaction Conversation compaction results including success status, metrics, and optional error details - Task completion notification with optional summary from the agent - - User message content with optional attachments, source information, and interaction - metadata + Task completion notification with summary from the agent Empty payload; the event signals that the pending message queue has changed @@ -2805,18 +3129,27 @@ class SessionEvent: User input request completion notification signaling UI dismissal - Structured form elicitation request with JSON schema definition for form fields + Elicitation request; may be form-based (structured input) or URL-based (browser + redirect) Elicitation request completion notification signaling UI dismissal + OAuth authentication request for an MCP server + + MCP OAuth request completion notification + External tool invocation request for client-side tool execution External tool completion notification signaling UI dismissal Queued slash command dispatch request for client execution + Registered command dispatch request routed to the owning client + Queued command completion notification signaling UI dismissal + SDK command registration change notification + Plan approval request with plan content and available user actions Plan mode exit completion notification signaling UI dismissal diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 63d3e73221..e82fcc0241 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -147,7 +147,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): - """Test listing agents returns empty when none configured.""" + """Test listing agents returns no custom agents when none configured.""" client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: @@ -157,7 +157,13 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): ) result = await session.rpc.agent.list() - assert result.agents == [] + # The CLI may return built-in/default agents even when no custom agents + # are configured. Verify no custom test agents appear in the list. + custom_names = {"test-agent", "another-agent"} + for agent in result.agents: + assert agent.name not in custom_names, ( + f"Expected no custom agents, but found {agent.name!r}" + ) await session.disconnect() await client.stop() diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index c467761d0d..59abee2981 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -45,6 +45,77 @@ function toGoFieldName(jsonName: string): string { .join(""); } +/** + * Post-process Go enum constants so every constant follows the canonical + * Go `TypeNameValue` convention. quicktype disambiguates collisions with + * whimsical prefixes (Purple, Fluffy, …) that we replace. + */ +function postProcessEnumConstants(code: string): string { + const renames = new Map(); + + // Match constant declarations inside const ( … ) blocks. + const constLineRe = /^\s+(\w+)\s+(\w+)\s*=\s*"([^"]+)"/gm; + let m; + while ((m = constLineRe.exec(code)) !== null) { + const [, constName, typeName, value] = m; + if (constName.startsWith(typeName)) continue; + + // Use the same initialism logic as toPascalCase so "url" → "URL", "mcp" → "MCP", etc. + const valuePascal = value + .split(/[._-]/) + .map((w) => goInitialisms.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); + const desired = typeName + valuePascal; + if (constName !== desired) { + renames.set(constName, desired); + } + } + + // Replace each const block in place, then fix switch-case references + // in marshal/unmarshal functions. This avoids renaming struct fields. + + // Phase 1: Rename inside const ( … ) blocks + code = code.replace(/^(const \([\s\S]*?\n\))/gm, (block) => { + let b = block; + for (const [oldName, newName] of renames) { + b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName); + } + return b; + }); + + // Phase 2: Rename inside func bodies (marshal/unmarshal helpers use case statements) + code = code.replace(/^(func \([\s\S]*?\n\})/gm, (funcBlock) => { + let b = funcBlock; + for (const [oldName, newName] of renames) { + b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName); + } + return b; + }); + + return code; +} + +/** + * Extract a mapping from (structName, jsonFieldName) → goFieldName + * so the wrapper code references the actual quicktype-generated field names. + */ +function extractFieldNames(qtCode: string): Map> { + const result = new Map>(); + const structRe = /^type\s+(\w+)\s+struct\s*\{([^}]*)\}/gm; + let sm; + while ((sm = structRe.exec(qtCode)) !== null) { + const [, structName, body] = sm; + const fields = new Map(); + const fieldRe = /^\s+(\w+)\s+[^`\n]+`json:"([^",]+)/gm; + let fm; + while ((fm = fieldRe.exec(body)) !== null) { + fields.set(fm[2], fm[1]); + } + result.set(structName, fields); + } + return result; +} + async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); @@ -93,7 +164,7 @@ async function generateSessionEvents(schemaPath?: string): Promise { `; - const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + result.lines.join("\n")); + const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + postProcessEnumConstants(result.lines.join("\n"))); console.log(` ✓ ${outPath}`); await formatGoFile(outPath); @@ -154,22 +225,25 @@ async function generateRpc(schemaPath?: string): Promise { rendererOptions: { package: "copilot", "just-types": "true" }, }); - // Build method wrappers - const lines: string[] = []; - lines.push(`// AUTO-GENERATED FILE - DO NOT EDIT`); - lines.push(`// Generated from: api.schema.json`); - lines.push(``); - lines.push(`package rpc`); - lines.push(``); - lines.push(`import (`); - lines.push(`\t"context"`); - lines.push(`\t"encoding/json"`); - lines.push(``); - lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); - lines.push(`)`); - lines.push(``); + // Post-process quicktype output: fix enum constant names + let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n"); + qtCode = postProcessEnumConstants(qtCode); + // Strip trailing whitespace from quicktype output (gofmt requirement) + qtCode = qtCode.replace(/[ \t]+$/gm, ""); - // Add quicktype-generated types (skip package line), annotating experimental types + // Extract actual type names generated by quicktype (may differ from toPascalCase) + const actualTypeNames = new Map(); + const structRe = /^type\s+(\w+)\s+struct\b/gm; + let sm; + while ((sm = structRe.exec(qtCode)) !== null) { + actualTypeNames.set(sm[1].toLowerCase(), sm[1]); + } + const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name; + + // Extract field name mappings (quicktype may rename fields to avoid Go keyword conflicts) + const fieldNames = extractFieldNames(qtCode); + + // Annotate experimental data types const experimentalTypeNames = new Set(); for (const method of allMethods) { if (method.stability !== "experimental") continue; @@ -179,9 +253,6 @@ async function generateRpc(schemaPath?: string): Promise { experimentalTypeNames.add(baseName + "Params"); } } - let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n"); - // Strip trailing whitespace from quicktype output (gofmt requirement) - qtCode = qtCode.replace(/[ \t]+$/gm, ""); for (const typeName of experimentalTypeNames) { qtCode = qtCode.replace( new RegExp(`^(type ${typeName} struct)`, "m"), @@ -190,17 +261,33 @@ async function generateRpc(schemaPath?: string): Promise { } // Remove trailing blank lines from quicktype output before appending qtCode = qtCode.replace(/\n+$/, ""); + + // Build method wrappers + const lines: string[] = []; + lines.push(`// AUTO-GENERATED FILE - DO NOT EDIT`); + lines.push(`// Generated from: api.schema.json`); + lines.push(``); + lines.push(`package rpc`); + lines.push(``); + lines.push(`import (`); + lines.push(`\t"context"`); + lines.push(`\t"encoding/json"`); + lines.push(``); + lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); + lines.push(`)`); + lines.push(``); + lines.push(qtCode); lines.push(``); // Emit ServerRpc if (schema.server) { - emitRpcWrapper(lines, schema.server, false); + emitRpcWrapper(lines, schema.server, false, resolveType, fieldNames); } // Emit SessionRpc if (schema.session) { - emitRpcWrapper(lines, schema.session, true); + emitRpcWrapper(lines, schema.session, true, resolveType, fieldNames); } const outPath = await writeGeneratedFile("go/rpc/generated_rpc.go", lines.join("\n")); @@ -209,7 +296,7 @@ async function generateRpc(schemaPath?: string): Promise { await formatGoFile(outPath); } -function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void { +function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>): void { const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); @@ -235,7 +322,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, apiName, key, value, isSession, groupExperimental); + emitMethod(lines, apiName, key, value, isSession, resolveType, fieldNames, groupExperimental); } } @@ -260,7 +347,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods (server only) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, wrapperName, key, value, isSession, false); + emitMethod(lines, wrapperName, key, value, isSession, resolveType, fieldNames, false); } // Compute key alignment for constructor composite literal (gofmt aligns key: value) @@ -284,15 +371,15 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); } -function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { +function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>, groupExperimental = false): void { const methodName = toPascalCase(name); - const resultType = toPascalCase(method.rpcMethod) + "Result"; + const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result"); const paramProps = method.params?.properties || {}; const requiredParams = new Set(method.params?.required || []); const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId"); const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; - const paramsType = hasParams ? toPascalCase(method.rpcMethod) + "Params" : ""; + const paramsType = hasParams ? resolveType(toPascalCase(method.rpcMethod) + "Params") : ""; if (method.stability === "experimental" && !groupExperimental) { lines.push(`// Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`); @@ -308,7 +395,7 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc if (hasParams) { lines.push(`\tif params != nil {`); for (const pName of nonSessionParams) { - const goField = toGoFieldName(pName); + const goField = fieldNames.get(paramsType)?.get(pName) ?? toGoFieldName(pName); const isOptional = !requiredParams.has(pName); if (isOptional) { // Optional fields are pointers - only add when non-nil and dereference diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index cbbc3df385..0340cf1f1a 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -32,12 +32,37 @@ import { * - Callable from collections.abc instead of typing * - Clean up unused typing imports */ +function replaceBalancedBrackets(code: string, prefix: string, replacer: (inner: string) => string): string { + let result = ""; + let i = 0; + while (i < code.length) { + const idx = code.indexOf(prefix + "[", i); + if (idx === -1) { + result += code.slice(i); + break; + } + result += code.slice(i, idx); + const start = idx + prefix.length + 1; // after '[' + let depth = 1; + let j = start; + while (j < code.length && depth > 0) { + if (code[j] === "[") depth++; + else if (code[j] === "]") depth--; + j++; + } + const inner = code.slice(start, j - 1); + result += replacer(inner); + i = j; + } + return result; +} + function modernizePython(code: string): string { - // Replace Optional[X] with X | None (handles nested brackets) - code = code.replace(/Optional\[([^\[\]]*(?:\[[^\[\]]*\])*[^\[\]]*)\]/g, "$1 | None"); + // Replace Optional[X] with X | None (handles arbitrarily nested brackets) + code = replaceBalancedBrackets(code, "Optional", (inner) => `${inner} | None`); // Replace Union[X, Y] with X | Y - code = code.replace(/Union\[([^\[\]]*(?:\[[^\[\]]*\])*[^\[\]]*)\]/g, (_match, inner: string) => { + code = replaceBalancedBrackets(code, "Union", (inner) => { return inner.split(",").map((s: string) => s.trim()).join(" | "); }); @@ -234,6 +259,16 @@ async function generateRpc(schemaPath?: string): Promise { ); } + // Extract actual class names generated by quicktype (may differ from toPascalCase, + // e.g. quicktype produces "SessionMCPList" not "SessionMcpList") + const actualTypeNames = new Map(); + const classRe = /^class\s+(\w+)\b/gm; + let cm; + while ((cm = classRe.exec(typesCode)) !== null) { + actualTypeNames.set(cm[1].toLowerCase(), cm[1]); + } + const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name; + const lines: string[] = []; lines.push(`""" AUTO-GENERATED FILE - DO NOT EDIT @@ -258,17 +293,17 @@ def _timeout_kwargs(timeout: float | None) -> dict: // Emit RPC wrapper classes if (schema.server) { - emitRpcWrapper(lines, schema.server, false); + emitRpcWrapper(lines, schema.server, false, resolveType); } if (schema.session) { - emitRpcWrapper(lines, schema.session, true); + emitRpcWrapper(lines, schema.session, true, resolveType); } const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", lines.join("\n")); console.log(` ✓ ${outPath}`); } -function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void { +function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string): void { const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); @@ -298,7 +333,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession, groupExperimental); + emitMethod(lines, key, value, isSession, resolveType, groupExperimental); } lines.push(``); } @@ -327,19 +362,19 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession, false); + emitMethod(lines, key, value, isSession, resolveType, false); } lines.push(``); } -function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { +function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false): void { const methodName = toSnakeCase(name); - const resultType = toPascalCase(method.rpcMethod) + "Result"; + const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result"); const paramProps = method.params?.properties || {}; const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId"); const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; - const paramsType = toPascalCase(method.rpcMethod) + "Params"; + const paramsType = resolveType(toPascalCase(method.rpcMethod) + "Params"); // Build signature with typed params + optional timeout const sig = hasParams diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index bf9564d9a2..c8ec038fbf 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.4", + "@github/copilot": "^1.0.10-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", @@ -462,27 +462,27 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz", - "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz", + "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.4", - "@github/copilot-darwin-x64": "1.0.4", - "@github/copilot-linux-arm64": "1.0.4", - "@github/copilot-linux-x64": "1.0.4", - "@github/copilot-win32-arm64": "1.0.4", - "@github/copilot-win32-x64": "1.0.4" + "@github/copilot-darwin-arm64": "1.0.10-0", + "@github/copilot-darwin-x64": "1.0.10-0", + "@github/copilot-linux-arm64": "1.0.10-0", + "@github/copilot-linux-x64": "1.0.10-0", + "@github/copilot-win32-arm64": "1.0.10-0", + "@github/copilot-win32-x64": "1.0.10-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz", + "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==", "cpu": [ "arm64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz", - "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz", + "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz", - "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz", + "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz", - "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz", + "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==", "cpu": [ "x64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz", - "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz", + "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==", "cpu": [ "arm64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz", - "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==", + "version": "1.0.10-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz", + "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 9f336dfd4c..25117cac95 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.4", + "@github/copilot": "^1.0.10-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", diff --git a/test/scenarios/prompts/attachments/README.md b/test/scenarios/prompts/attachments/README.md index d61a26e577..76b76751da 100644 --- a/test/scenarios/prompts/attachments/README.md +++ b/test/scenarios/prompts/attachments/README.md @@ -39,7 +39,7 @@ Demonstrates sending **file attachments** alongside a prompt using the Copilot S |----------|------------------------| | TypeScript | `attachments: [{ type: "blob", data: base64Data, mimeType: "image/png" }]` | | Python | `"attachments": [{"type": "blob", "data": base64_data, "mimeType": "image/png"}]` | -| Go | `Attachments: []copilot.Attachment{{Type: copilot.Blob, Data: &data, MIMEType: &mime}}` | +| Go | `Attachments: []copilot.Attachment{{Type: copilot.AttachmentTypeBlob, Data: &data, MIMEType: &mime}}` | ## Sample Data From 005b780c3b4d320ccbba37d0873d730dfaacc9c5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 20 Mar 2026 08:54:53 -0700 Subject: [PATCH 60/61] Add fine-grained system prompt customization (customize mode) (#816) * Add fine-grained system prompt customization (customize mode) Add a new 'customize' mode for systemMessage configuration, enabling SDK consumers to selectively override individual sections of the CLI system prompt while preserving the rest. This sits between the existing 'append' and 'replace' modes. 9 configurable sections: identity, tone, tool_efficiency, environment_context, code_change_rules, guidelines, safety, tool_instructions, custom_instructions. 4 override actions per section: replace, remove, append, prepend. Unknown section IDs are handled gracefully: content-bearing overrides are appended to additional instructions with a warning, and remove on unknown sections is silently ignored. Types and constants added to all 4 SDK languages (TypeScript, Python, Go, .NET). Documentation updated across all READMEs and getting-started guide. Companion runtime PR: github/copilot-agent-runtime#4751 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback: fix docs, add examples and E2E tests - Fix incorrect package name in nodejs/README.md (@anthropic-ai/sdk -> @github/copilot-sdk) - Add standalone 'System Message Customization' sections with full code examples to Python and Go READMEs (matching TypeScript/.NET) - Add E2E tests for customize mode to Python, Go, and .NET (matching existing Node.js E2E test coverage) - Fix 'end of the prompt' wording in docs to 'additional instructions' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add last_instructions configurable section Expose lastInstructions as a customizable section across all 4 SDKs, addressing review feedback about duplicate tool-efficiency blocks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix lint: prettier formatting, Python import order and line length Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add transform operation for system prompt section customization Adds a new 'transform' action to SectionOverride that enables read-then-write mutation of system prompt sections via callbacks. The SDK intercepts function- valued actions before serialization, stores the callbacks locally, and handles the batched systemMessage.transform JSON-RPC callback from the runtime. Changes across all 4 SDKs (TypeScript, Python, Go, .NET): - Types: SectionTransformFn, SectionOverrideAction (TS/Python), Transform field (Go/.NET), SectionOverrideAction constants (Go) - Client: extractTransformCallbacks helper, transform callback registration, systemMessage.transform RPC handler - Session: transform callback storage and batched dispatch with error handling - E2E tests and shared snapshot YAML files Wire protocol: single batched RPC call with all transform sections, matching the runtime implementation in copilot-agent-runtime PR #5103. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Formatting Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> * Add E2E snapshot for customized systemMessage config test Generate the missing snapshot file that the 'should create a session with customized systemMessage config' test requires across all SDK languages (Node, Python, Go, .NET). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Go fmt blank comment line and Python import ordering - Add blank // comment line between doc example and extractTransformCallbacks function doc comment in go/client.go (required by go fmt) - Fix ruff import sorting in python/copilot/__init__.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python ty type error in session transform handler Use str() to ensure transform callback result is typed as str, fixing the invalid-assignment error from ty type checker at session.py:689. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python E2E test to use keyword args for create_session The create_session() method was refactored to keyword-only params. Update the customized systemMessage test to use keyword arguments instead of a positional dict, and fix send_and_wait() call to pass prompt as a positional string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Assert transform result in system message via HTTP traffic The 'should apply transform modifications' tests previously only verified that the transform callback was invoked, not that the transformed content actually reached the model. Now all 4 SDKs assert that TRANSFORM_MARKER appears in the system message captured from HTTP traffic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Go transform JSON serialization: add json tags for content field The systemMessageTransformRequest and systemMessageTransformResponse used anonymous structs without json tags, causing Content to serialize as uppercase 'Content' instead of lowercase 'content'. The CLI expects lowercase, so transform results were silently ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson --- docs/getting-started.md | 24 ++- dotnet/README.md | 28 +++ dotnet/src/Client.cs | 62 +++++- dotnet/src/SdkProtocolVersion.cs | 5 +- dotnet/src/Session.cs | 71 +++++++ dotnet/src/Types.cs | 118 ++++++++++- dotnet/test/SessionTests.cs | 31 +++ dotnet/test/SystemMessageTransformTests.cs | 140 +++++++++++++ go/README.md | 51 ++++- go/client.go | 64 +++++- go/internal/e2e/session_test.go | 45 +++++ .../e2e/system_message_transform_test.go | 189 ++++++++++++++++++ go/session.go | 80 ++++++-- go/types.go | 57 +++++- nodejs/README.md | 40 +++- nodejs/src/client.ts | 93 ++++++++- nodejs/src/index.ts | 7 +- nodejs/src/session.ts | 44 ++++ nodejs/src/types.ts | 100 ++++++++- nodejs/test/e2e/session.test.ts | 27 +++ .../test/e2e/system_message_transform.test.ts | 125 ++++++++++++ python/README.md | 53 ++++- python/copilot/__init__.py | 18 ++ python/copilot/client.py | 81 +++++++- python/copilot/session.py | 59 ++++++ python/copilot/types.py | 66 +++++- python/e2e/test_session.py | 27 +++ python/e2e/test_system_message_transform.py | 123 ++++++++++++ ..._with_customized_systemmessage_config.yaml | 35 ++++ ...form_modifications_to_section_content.yaml | 33 +++ ...nsform_callbacks_with_section_content.yaml | 54 +++++ ...tic_overrides_and_transforms_together.yaml | 50 +++++ 32 files changed, 1955 insertions(+), 45 deletions(-) create mode 100644 dotnet/test/SystemMessageTransformTests.cs create mode 100644 go/internal/e2e/system_message_transform_test.go create mode 100644 nodejs/test/e2e/system_message_transform.test.ts create mode 100644 python/e2e/test_system_message_transform.py create mode 100644 test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml create mode 100644 test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml create mode 100644 test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml create mode 100644 test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml diff --git a/docs/getting-started.md b/docs/getting-started.md index 6c0aee72e3..9d4189f561 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1235,7 +1235,7 @@ const session = await client.createSession({ ### Customize the System Message -Control the AI's behavior and personality: +Control the AI's behavior and personality by appending instructions: ```typescript const session = await client.createSession({ @@ -1245,6 +1245,28 @@ const session = await client.createSession({ }); ``` +For more fine-grained control, use `mode: "customize"` to override individual sections of the system prompt while preserving the rest: + +```typescript +const session = await client.createSession({ + systemMessage: { + mode: "customize", + sections: { + tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." }, + code_change_rules: { action: "remove" }, + guidelines: { action: "append", content: "\n* Always cite data sources" }, + }, + content: "Focus on financial analysis and reporting.", + }, +}); +``` + +Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. + +Each override supports four actions: `replace`, `remove`, `append`, and `prepend`. Unknown section IDs are handled gracefully — content is appended to additional instructions and a warning is emitted; `remove` on unknown sections is silently ignored. + +See the language-specific SDK READMEs for examples in [TypeScript](../nodejs/README.md), [Python](../python/README.md), [Go](../go/README.md), and [C#](../dotnet/README.md). + --- ## Connecting to an External CLI Server diff --git a/dotnet/README.md b/dotnet/README.md index cb7dbba18e..cab1cf068c 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -509,6 +509,34 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` +#### Customize Mode + +Use `Mode = SystemMessageMode.Customize` to selectively override individual sections of the prompt while preserving the rest: + +```csharp +var session = await client.CreateSessionAsync(new SessionConfig +{ + Model = "gpt-5", + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." }, + [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, + [SystemPromptSections.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" }, + }, + Content = "Focus on financial analysis and reporting." + } +}); +``` + +Available section IDs are defined as constants on `SystemPromptSections`: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `LastInstructions`. + +Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored. + +#### Replace Mode + For full control (removes all guardrails), use `Mode = SystemMessageMode.Replace`: ```csharp diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index a9ad1fccde..99c0eff003 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -365,6 +365,44 @@ private async Task CleanupConnectionAsync(List? errors) } } + private static (SystemMessageConfig? wireConfig, Dictionary>>? callbacks) ExtractTransformCallbacks(SystemMessageConfig? systemMessage) + { + if (systemMessage?.Mode != SystemMessageMode.Customize || systemMessage.Sections == null) + { + return (systemMessage, null); + } + + var callbacks = new Dictionary>>(); + var wireSections = new Dictionary(); + + foreach (var (sectionId, sectionOverride) in systemMessage.Sections) + { + if (sectionOverride.Transform != null) + { + callbacks[sectionId] = sectionOverride.Transform; + wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform }; + } + else + { + wireSections[sectionId] = sectionOverride; + } + } + + if (callbacks.Count == 0) + { + return (systemMessage, null); + } + + var wireConfig = new SystemMessageConfig + { + Mode = systemMessage.Mode, + Content = systemMessage.Content, + Sections = wireSections + }; + + return (wireConfig, callbacks); + } + /// /// Creates a new Copilot session with the specified configuration. /// @@ -409,6 +447,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); + var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); + var sessionId = config.SessionId ?? Guid.NewGuid().ToString(); // Create and register the session before issuing the RPC so that @@ -424,6 +464,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { session.RegisterHooks(config.Hooks); } + if (transformCallbacks != null) + { + session.RegisterTransformCallbacks(transformCallbacks); + } if (config.OnEvent != null) { session.On(config.OnEvent); @@ -440,7 +484,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.ClientName, config.ReasoningEffort, config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, + wireSystemMessage, config.AvailableTools, config.ExcludedTools, config.Provider, @@ -519,6 +563,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); + var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage); + // Create and register the session before issuing the RPC so that // events emitted by the CLI (e.g. session.start) are not dropped. var session = new CopilotSession(sessionId, connection.Rpc, _logger); @@ -532,6 +578,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { session.RegisterHooks(config.Hooks); } + if (transformCallbacks != null) + { + session.RegisterTransformCallbacks(transformCallbacks); + } if (config.OnEvent != null) { session.On(config.OnEvent); @@ -548,7 +598,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Model, config.ReasoningEffort, config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, + wireSystemMessage, config.AvailableTools, config.ExcludedTools, config.Provider, @@ -1222,6 +1272,7 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequestV2); rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest); rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); + rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform); rpc.StartListening(); // Transition state to Disconnected if the JSON-RPC connection drops @@ -1350,6 +1401,12 @@ public async Task OnHooksInvoke(string sessionId, string ho return new HooksInvokeResponse(output); } + public async Task OnSystemMessageTransform(string sessionId, JsonElement sections) + { + var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); + return await session.HandleSystemMessageTransformAsync(sections); + } + // Protocol v2 backward-compatibility adapters public async Task OnToolCallV2(string sessionId, @@ -1685,6 +1742,7 @@ private static LogLevel MapLevel(TraceEventType eventType) [JsonSerializable(typeof(ResumeSessionResponse))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SystemMessageConfig))] + [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(ToolCallResponseV2))] [JsonSerializable(typeof(ToolDefinition))] [JsonSerializable(typeof(ToolResultAIContent))] diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs index f3d8f04c5a..889af460bb 100644 --- a/dotnet/src/SdkProtocolVersion.cs +++ b/dotnet/src/SdkProtocolVersion.cs @@ -16,8 +16,5 @@ internal static class SdkProtocolVersion /// /// Gets the SDK protocol version. /// - public static int GetVersion() - { - return Version; - } + public static int GetVersion() => Version; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 0014ec7f07..675a3e0c0d 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -65,6 +65,8 @@ public sealed partial class CopilotSession : IAsyncDisposable private SessionHooks? _hooks; private readonly SemaphoreSlim _hooksLock = new(1, 1); + private Dictionary>>? _transformCallbacks; + private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1); private SessionRpc? _sessionRpc; private int _isDisposed; @@ -653,6 +655,72 @@ internal void RegisterHooks(SessionHooks hooks) }; } + /// + /// Registers transform callbacks for system message sections. + /// + /// The transform callbacks keyed by section identifier. + internal void RegisterTransformCallbacks(Dictionary>>? callbacks) + { + _transformCallbacksLock.Wait(); + try + { + _transformCallbacks = callbacks; + } + finally + { + _transformCallbacksLock.Release(); + } + } + + /// + /// Handles a systemMessage.transform RPC call from the Copilot CLI. + /// + /// The raw JSON element containing sections to transform. + /// A task that resolves with the transformed sections. + internal async Task HandleSystemMessageTransformAsync(JsonElement sections) + { + Dictionary>>? callbacks; + await _transformCallbacksLock.WaitAsync(); + try + { + callbacks = _transformCallbacks; + } + finally + { + _transformCallbacksLock.Release(); + } + + var parsed = JsonSerializer.Deserialize( + sections.GetRawText(), + SessionJsonContext.Default.DictionaryStringSystemMessageTransformSection) ?? new(); + + var result = new Dictionary(); + foreach (var (sectionId, data) in parsed) + { + Func>? callback = null; + callbacks?.TryGetValue(sectionId, out callback); + + if (callback != null) + { + try + { + var transformed = await callback(data.Content ?? ""); + result[sectionId] = new SystemMessageTransformSection { Content = transformed }; + } + catch + { + result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" }; + } + } + else + { + result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" }; + } + } + + return new SystemMessageTransformRpcResponse { Sections = result }; + } + /// /// Gets the complete list of messages and events in the session. /// @@ -891,5 +959,8 @@ internal record SessionDestroyRequest [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(ErrorOccurredHookInput))] [JsonSerializable(typeof(ErrorOccurredHookOutput))] + [JsonSerializable(typeof(SystemMessageTransformSection))] + [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] + [JsonSerializable(typeof(Dictionary))] internal partial class SessionJsonContext : JsonSerializerContext; } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 84e7feaede..d6530f9c76 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -968,7 +968,86 @@ public enum SystemMessageMode Append, /// Replace the default system message entirely. [JsonStringEnumMemberName("replace")] - Replace + Replace, + /// Override individual sections of the system prompt. + [JsonStringEnumMemberName("customize")] + Customize +} + +/// +/// Specifies the operation to perform on a system prompt section. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SectionOverrideAction +{ + /// Replace the section content entirely. + [JsonStringEnumMemberName("replace")] + Replace, + /// Remove the section from the prompt. + [JsonStringEnumMemberName("remove")] + Remove, + /// Append content after the existing section. + [JsonStringEnumMemberName("append")] + Append, + /// Prepend content before the existing section. + [JsonStringEnumMemberName("prepend")] + Prepend, + /// Transform the section content via a callback. + [JsonStringEnumMemberName("transform")] + Transform +} + +/// +/// Override operation for a single system prompt section. +/// +public class SectionOverride +{ + /// + /// The operation to perform on this section. Ignored when Transform is set. + /// + [JsonPropertyName("action")] + public SectionOverrideAction? Action { get; set; } + + /// + /// Content for the override. Optional for all actions. Ignored for remove. + /// + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// + /// Transform callback. When set, takes precedence over Action. + /// Receives current section content, returns transformed content. + /// Not serialized — the SDK handles this locally. + /// + [JsonIgnore] + public Func>? Transform { get; set; } +} + +/// +/// Known system prompt section identifiers for the "customize" mode. +/// +public static class SystemPromptSections +{ + /// Agent identity preamble and mode statement. + public const string Identity = "identity"; + /// Response style, conciseness rules, output formatting preferences. + public const string Tone = "tone"; + /// Tool usage patterns, parallel calling, batching guidelines. + public const string ToolEfficiency = "tool_efficiency"; + /// CWD, OS, git root, directory listing, available tools. + public const string EnvironmentContext = "environment_context"; + /// Coding rules, linting/testing, ecosystem tools, style. + public const string CodeChangeRules = "code_change_rules"; + /// Tips, behavioral best practices, behavioral guidelines. + public const string Guidelines = "guidelines"; + /// Environment limitations, prohibited actions, security policies. + public const string Safety = "safety"; + /// Per-tool usage instructions. + public const string ToolInstructions = "tool_instructions"; + /// Repository and organization custom instructions. + public const string CustomInstructions = "custom_instructions"; + /// End-of-prompt instructions: parallel tool calling, persistence, task completion. + public const string LastInstructions = "last_instructions"; } /// @@ -977,13 +1056,21 @@ public enum SystemMessageMode public class SystemMessageConfig { /// - /// How the system message is applied (append or replace). + /// How the system message is applied (append, replace, or customize). /// public SystemMessageMode? Mode { get; set; } + /// - /// Content of the system message. + /// Content of the system message. Used by append and replace modes. + /// In customize mode, additional content appended after all sections. /// public string? Content { get; set; } + + /// + /// Section-level overrides for customize mode. + /// Keys are section identifiers (see ). + /// + public Dictionary? Sections { get; set; } } /// @@ -2032,6 +2119,30 @@ public class SetForegroundSessionResponse public string? Error { get; set; } } +/// +/// Content data for a single system prompt section in a transform RPC call. +/// +public class SystemMessageTransformSection +{ + /// + /// The content of the section. + /// + [JsonPropertyName("content")] + public string? Content { get; set; } +} + +/// +/// Response to a systemMessage.transform RPC call. +/// +public class SystemMessageTransformRpcResponse +{ + /// + /// The transformed sections keyed by section identifier. + /// + [JsonPropertyName("sections")] + public Dictionary? Sections { get; set; } +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -2061,6 +2172,7 @@ public class SetForegroundSessionResponse [JsonSerializable(typeof(SessionLifecycleEvent))] [JsonSerializable(typeof(SessionLifecycleEventMetadata))] [JsonSerializable(typeof(SessionListFilter))] +[JsonSerializable(typeof(SectionOverride))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SetForegroundSessionResponse))] [JsonSerializable(typeof(SystemMessageConfig))] diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 30a9135a5c..5aecaccbaf 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -91,6 +91,37 @@ public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config() Assert.Equal(testSystemMessage, GetSystemMessage(traffic[0])); } + [Fact] + public async Task Should_Create_A_Session_With_Customized_SystemMessage_Config() + { + var customTone = "Respond in a warm, professional tone. Be thorough in explanations."; + var appendedContent = "Always mention quarterly earnings."; + var session = await CreateSessionAsync(new SessionConfig + { + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = customTone }, + [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove }, + }, + Content = appendedContent + } + }); + + await session.SendAsync(new MessageOptions { Prompt = "Who are you?" }); + var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session); + Assert.NotNull(assistantMessage); + + var traffic = await Ctx.GetExchangesAsync(); + Assert.NotEmpty(traffic); + var systemMessage = GetSystemMessage(traffic[0]); + Assert.Contains(customTone, systemMessage); + Assert.Contains(appendedContent, systemMessage); + Assert.DoesNotContain("", systemMessage); + } + [Fact] public async Task Should_Create_A_Session_With_AvailableTools() { diff --git a/dotnet/test/SystemMessageTransformTests.cs b/dotnet/test/SystemMessageTransformTests.cs new file mode 100644 index 0000000000..cdddc5a79f --- /dev/null +++ b/dotnet/test/SystemMessageTransformTests.cs @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.SDK.Test.Harness; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.SDK.Test; + +public class SystemMessageTransformTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "system_message_transform", output) +{ + [Fact] + public async Task Should_Invoke_Transform_Callbacks_With_Section_Content() + { + var identityCallbackInvoked = false; + var toneCallbackInvoked = false; + + var session = await CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + ["identity"] = new SectionOverride + { + Transform = async (content) => + { + Assert.False(string.IsNullOrEmpty(content)); + identityCallbackInvoked = true; + return content; + } + }, + ["tone"] = new SectionOverride + { + Transform = async (content) => + { + Assert.False(string.IsNullOrEmpty(content)); + toneCallbackInvoked = true; + return content; + } + } + } + } + }); + + await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "test.txt"), "Hello transform!"); + + await session.SendAsync(new MessageOptions + { + Prompt = "Read the contents of test.txt and tell me what it says" + }); + + await TestHelper.GetFinalAssistantMessageAsync(session); + + Assert.True(identityCallbackInvoked, "Expected identity transform callback to be invoked"); + Assert.True(toneCallbackInvoked, "Expected tone transform callback to be invoked"); + } + + [Fact] + public async Task Should_Apply_Transform_Modifications_To_Section_Content() + { + var session = await CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + ["identity"] = new SectionOverride + { + Transform = async (content) => + { + return content + "\nAlways end your reply with TRANSFORM_MARKER"; + } + } + } + } + }); + + await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "hello.txt"), "Hello!"); + + await session.SendAsync(new MessageOptions + { + Prompt = "Read the contents of hello.txt" + }); + + await TestHelper.GetFinalAssistantMessageAsync(session); + + // Verify the transform result was actually applied to the system message + var traffic = await Ctx.GetExchangesAsync(); + Assert.NotEmpty(traffic); + var systemMessage = GetSystemMessage(traffic[0]); + Assert.Contains("TRANSFORM_MARKER", systemMessage); + } + + [Fact] + public async Task Should_Work_With_Static_Overrides_And_Transforms_Together() + { + var transformCallbackInvoked = false; + + var session = await CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + ["safety"] = new SectionOverride + { + Action = SectionOverrideAction.Remove + }, + ["identity"] = new SectionOverride + { + Transform = async (content) => + { + transformCallbackInvoked = true; + return content; + } + } + } + } + }); + + await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "combo.txt"), "Combo test!"); + + await session.SendAsync(new MessageOptions + { + Prompt = "Read the contents of combo.txt and tell me what it says" + }); + + await TestHelper.GetFinalAssistantMessageAsync(session); + + Assert.True(transformCallbackInvoked, "Expected identity transform callback to be invoked"); + } +} diff --git a/go/README.md b/go/README.md index 8cbb382c32..f29ef9fb73 100644 --- a/go/README.md +++ b/go/README.md @@ -150,7 +150,10 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `ReasoningEffort` (string): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `ListModels()` to check which models support this option. - `SessionID` (string): Custom session ID - `Tools` ([]Tool): Custom tools exposed to the CLI -- `SystemMessage` (\*SystemMessageConfig): System message configuration +- `SystemMessage` (\*SystemMessageConfig): System message configuration. Supports three modes: + - **append** (default): Appends `Content` after the SDK-managed prompt + - **replace**: Replaces the entire prompt with `Content` + - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`) - `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `Streaming` (bool): Enable streaming delta events - `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration @@ -179,6 +182,52 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec - `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart` option +### System Message Customization + +Control the system prompt using `SystemMessage` in session config: + +```go +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SystemMessage: &copilot.SystemMessageConfig{ + Content: "Always check for security vulnerabilities before suggesting changes.", + }, +}) +``` + +The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `Content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `Mode: "replace"` or `Mode: "customize"`. + +#### Customize Mode + +Use `Mode: "customize"` to selectively override individual sections of the prompt while preserving the rest: + +```go +session, err := client.CreateSession(ctx, &copilot.SessionConfig{ + SystemMessage: &copilot.SystemMessageConfig{ + Mode: "customize", + Sections: map[string]copilot.SectionOverride{ + // Replace the tone/style section + copilot.SectionTone: {Action: "replace", Content: "Respond in a warm, professional tone. Be thorough in explanations."}, + // Remove coding-specific rules + copilot.SectionCodeChangeRules: {Action: "remove"}, + // Append to existing guidelines + copilot.SectionGuidelines: {Action: "append", Content: "\n* Always cite data sources"}, + }, + // Additional instructions appended after all sections + Content: "Focus on financial analysis and reporting.", + }, +}) +``` + +Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`. + +Each section override supports four actions: +- **`replace`** — Replace the section content entirely +- **`remove`** — Remove the section from the prompt +- **`append`** — Add content after the existing section +- **`prepend`** — Add content before the existing section + +Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. + ## Image Support The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: diff --git a/go/client.go b/go/client.go index a2431ad395..22be47ec69 100644 --- a/go/client.go +++ b/go/client.go @@ -482,6 +482,37 @@ func (c *Client) ensureConnected(ctx context.Context) error { // }, // }, // }) +// +// extractTransformCallbacks separates transform callbacks from a SystemMessageConfig, +// returning a wire-safe config and a map of callbacks (nil if none). +func extractTransformCallbacks(config *SystemMessageConfig) (*SystemMessageConfig, map[string]SectionTransformFn) { + if config == nil || config.Mode != "customize" || len(config.Sections) == 0 { + return config, nil + } + + callbacks := make(map[string]SectionTransformFn) + wireSections := make(map[string]SectionOverride) + for id, override := range config.Sections { + if override.Transform != nil { + callbacks[id] = override.Transform + wireSections[id] = SectionOverride{Action: "transform"} + } else { + wireSections[id] = override + } + } + + if len(callbacks) == 0 { + return config, nil + } + + wireConfig := &SystemMessageConfig{ + Mode: config.Mode, + Content: config.Content, + Sections: wireSections, + } + return wireConfig, callbacks +} + func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) { if config == nil || config.OnPermissionRequest == nil { return nil, fmt.Errorf("an OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}") @@ -497,7 +528,8 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ReasoningEffort = config.ReasoningEffort req.ConfigDir = config.ConfigDir req.Tools = config.Tools - req.SystemMessage = config.SystemMessage + wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage) + req.SystemMessage = wireSystemMessage req.AvailableTools = config.AvailableTools req.ExcludedTools = config.ExcludedTools req.Provider = config.Provider @@ -548,6 +580,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.Hooks != nil { session.registerHooks(config.Hooks) } + if transformCallbacks != nil { + session.registerTransformCallbacks(transformCallbacks) + } if config.OnEvent != nil { session.On(config.OnEvent) } @@ -616,7 +651,8 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.ClientName = config.ClientName req.Model = config.Model req.ReasoningEffort = config.ReasoningEffort - req.SystemMessage = config.SystemMessage + wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage) + req.SystemMessage = wireSystemMessage req.Tools = config.Tools req.Provider = config.Provider req.AvailableTools = config.AvailableTools @@ -665,6 +701,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.Hooks != nil { session.registerHooks(config.Hooks) } + if transformCallbacks != nil { + session.registerTransformCallbacks(transformCallbacks) + } if config.OnEvent != nil { session.On(config.OnEvent) } @@ -1402,6 +1441,7 @@ func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequestV2)) c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest)) c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke)) + c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform)) } func (c *Client) handleSessionEvent(req sessionEventRequest) { @@ -1468,6 +1508,26 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso return result, nil } +// handleSystemMessageTransform handles a system message transform request from the CLI server. +func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) (systemMessageTransformResponse, *jsonrpc2.Error) { + if req.SessionID == "" { + return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: "invalid system message transform payload"} + } + + c.sessionsMux.Lock() + session, ok := c.sessions[req.SessionID] + c.sessionsMux.Unlock() + if !ok { + return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)} + } + + resp, err := session.handleSystemMessageTransform(req.Sections) + if err != nil { + return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32603, Message: err.Error()} + } + return resp, nil +} + // ======================================================================== // Protocol v2 backward-compatibility adapters // ======================================================================== diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 1eaeacd1ef..7f1817da92 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -184,6 +184,51 @@ func TestSession(t *testing.T) { } }) + t.Run("should create a session with customized systemMessage config", func(t *testing.T) { + ctx.ConfigureForTest(t) + + customTone := "Respond in a warm, professional tone. Be thorough in explanations." + appendedContent := "Always mention quarterly earnings." + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SystemMessage: &copilot.SystemMessageConfig{ + Mode: "customize", + Sections: map[string]copilot.SectionOverride{ + copilot.SectionTone: {Action: "replace", Content: customTone}, + copilot.SectionCodeChangeRules: {Action: "remove"}, + }, + Content: appendedContent, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Who are you?"}) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + // Validate the system message sent to the model + traffic, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("Failed to get exchanges: %v", err) + } + if len(traffic) == 0 { + t.Fatal("Expected at least one exchange") + } + systemMessage := getSystemMessage(traffic[0]) + if !strings.Contains(systemMessage, customTone) { + t.Errorf("Expected system message to contain custom tone, got %q", systemMessage) + } + if !strings.Contains(systemMessage, appendedContent) { + t.Errorf("Expected system message to contain appended content, got %q", systemMessage) + } + if strings.Contains(systemMessage, "") { + t.Error("Expected system message to NOT contain code_change_instructions (it was removed)") + } + }) + t.Run("should create a session with availableTools", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/internal/e2e/system_message_transform_test.go b/go/internal/e2e/system_message_transform_test.go new file mode 100644 index 0000000000..2d62b01cfb --- /dev/null +++ b/go/internal/e2e/system_message_transform_test.go @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package e2e + +import ( + "os" + "path/filepath" + "strings" + "sync" + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +func TestSystemMessageTransform(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("should_invoke_transform_callbacks_with_section_content", func(t *testing.T) { + ctx.ConfigureForTest(t) + + var identityContent string + var toneContent string + var mu sync.Mutex + identityCalled := false + toneCalled := false + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SystemMessage: &copilot.SystemMessageConfig{ + Mode: "customize", + Sections: map[string]copilot.SectionOverride{ + "identity": { + Transform: func(currentContent string) (string, error) { + mu.Lock() + identityCalled = true + identityContent = currentContent + mu.Unlock() + return currentContent, nil + }, + }, + "tone": { + Transform: func(currentContent string) (string, error) { + mu.Lock() + toneCalled = true + toneContent = currentContent + mu.Unlock() + return currentContent, nil + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + testFile := filepath.Join(ctx.WorkDir, "test.txt") + err = os.WriteFile(testFile, []byte("Hello transform!"), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Read the contents of test.txt and tell me what it says", + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + mu.Lock() + defer mu.Unlock() + + if !identityCalled { + t.Error("Expected identity transform callback to be invoked") + } + if !toneCalled { + t.Error("Expected tone transform callback to be invoked") + } + if identityContent == "" { + t.Error("Expected identity transform to receive non-empty content") + } + if toneContent == "" { + t.Error("Expected tone transform to receive non-empty content") + } + }) + + t.Run("should_apply_transform_modifications_to_section_content", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SystemMessage: &copilot.SystemMessageConfig{ + Mode: "customize", + Sections: map[string]copilot.SectionOverride{ + "identity": { + Transform: func(currentContent string) (string, error) { + return currentContent + "\nAlways end your reply with TRANSFORM_MARKER", nil + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + testFile := filepath.Join(ctx.WorkDir, "hello.txt") + err = os.WriteFile(testFile, []byte("Hello!"), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + assistantMessage, err := session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Read the contents of hello.txt", + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + // Verify the transform result was actually applied to the system message + traffic, err := ctx.GetExchanges() + if err != nil { + t.Fatalf("Failed to get exchanges: %v", err) + } + if len(traffic) == 0 { + t.Fatal("Expected at least one exchange") + } + systemMessage := getSystemMessage(traffic[0]) + if !strings.Contains(systemMessage, "TRANSFORM_MARKER") { + t.Errorf("Expected system message to contain TRANSFORM_MARKER, got %q", systemMessage) + } + + _ = assistantMessage + }) + + t.Run("should_work_with_static_overrides_and_transforms_together", func(t *testing.T) { + ctx.ConfigureForTest(t) + + var mu sync.Mutex + transformCalled := false + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SystemMessage: &copilot.SystemMessageConfig{ + Mode: "customize", + Sections: map[string]copilot.SectionOverride{ + "safety": { + Action: copilot.SectionActionRemove, + }, + "identity": { + Transform: func(currentContent string) (string, error) { + mu.Lock() + transformCalled = true + mu.Unlock() + return currentContent, nil + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + testFile := filepath.Join(ctx.WorkDir, "combo.txt") + err = os.WriteFile(testFile, []byte("Combo test!"), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "Read the contents of combo.txt and tell me what it says", + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + mu.Lock() + defer mu.Unlock() + + if !transformCalled { + t.Error("Expected identity transform callback to be invoked") + } + }) +} diff --git a/go/session.go b/go/session.go index 107ac98248..3a94a818ed 100644 --- a/go/session.go +++ b/go/session.go @@ -50,20 +50,22 @@ type sessionHandler struct { // }) type Session struct { // SessionID is the unique identifier for this session. - SessionID string - workspacePath string - client *jsonrpc2.Client - handlers []sessionHandler - nextHandlerID uint64 - handlerMutex sync.RWMutex - toolHandlers map[string]ToolHandler - toolHandlersM sync.RWMutex - permissionHandler PermissionHandlerFunc - permissionMux sync.RWMutex - userInputHandler UserInputHandler - userInputMux sync.RWMutex - hooks *SessionHooks - hooksMux sync.RWMutex + SessionID string + workspacePath string + client *jsonrpc2.Client + handlers []sessionHandler + nextHandlerID uint64 + handlerMutex sync.RWMutex + toolHandlers map[string]ToolHandler + toolHandlersM sync.RWMutex + permissionHandler PermissionHandlerFunc + permissionMux sync.RWMutex + userInputHandler UserInputHandler + userInputMux sync.RWMutex + hooks *SessionHooks + hooksMux sync.RWMutex + transformCallbacks map[string]SectionTransformFn + transformMu sync.Mutex // eventCh serializes user event handler dispatch. dispatchEvent enqueues; // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. @@ -446,6 +448,56 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) ( } } +// registerTransformCallbacks registers transform callbacks for this session. +// +// Transform callbacks are invoked when the CLI requests system message section +// transforms. This method is internal and typically called when creating a session. +func (s *Session) registerTransformCallbacks(callbacks map[string]SectionTransformFn) { + s.transformMu.Lock() + defer s.transformMu.Unlock() + s.transformCallbacks = callbacks +} + +type systemMessageTransformSection struct { + Content string `json:"content"` +} + +type systemMessageTransformRequest struct { + SessionID string `json:"sessionId"` + Sections map[string]systemMessageTransformSection `json:"sections"` +} + +type systemMessageTransformResponse struct { + Sections map[string]systemMessageTransformSection `json:"sections"` +} + +// handleSystemMessageTransform handles a system message transform request from the Copilot CLI. +// This is an internal method called by the SDK when the CLI requests section transforms. +func (s *Session) handleSystemMessageTransform(sections map[string]systemMessageTransformSection) (systemMessageTransformResponse, error) { + s.transformMu.Lock() + callbacks := s.transformCallbacks + s.transformMu.Unlock() + + result := make(map[string]systemMessageTransformSection) + for sectionID, data := range sections { + var callback SectionTransformFn + if callbacks != nil { + callback = callbacks[sectionID] + } + if callback != nil { + transformed, err := callback(data.Content) + if err != nil { + result[sectionID] = systemMessageTransformSection{Content: data.Content} + } else { + result[sectionID] = systemMessageTransformSection{Content: transformed} + } + } else { + result[sectionID] = systemMessageTransformSection{Content: data.Content} + } + } + return systemMessageTransformResponse{Sections: result}, nil +} + // dispatchEvent enqueues an event for delivery to user handlers and fires // broadcast handlers concurrently. // diff --git a/go/types.go b/go/types.go index fd9968e3e3..502d61c1c6 100644 --- a/go/types.go +++ b/go/types.go @@ -111,6 +111,51 @@ func Float64(v float64) *float64 { return &v } +// Known system prompt section identifiers for the "customize" mode. +const ( + SectionIdentity = "identity" + SectionTone = "tone" + SectionToolEfficiency = "tool_efficiency" + SectionEnvironmentContext = "environment_context" + SectionCodeChangeRules = "code_change_rules" + SectionGuidelines = "guidelines" + SectionSafety = "safety" + SectionToolInstructions = "tool_instructions" + SectionCustomInstructions = "custom_instructions" + SectionLastInstructions = "last_instructions" +) + +// SectionOverrideAction represents the action to perform on a system prompt section. +type SectionOverrideAction string + +const ( + // SectionActionReplace replaces section content entirely. + SectionActionReplace SectionOverrideAction = "replace" + // SectionActionRemove removes the section. + SectionActionRemove SectionOverrideAction = "remove" + // SectionActionAppend appends to existing section content. + SectionActionAppend SectionOverrideAction = "append" + // SectionActionPrepend prepends to existing section content. + SectionActionPrepend SectionOverrideAction = "prepend" +) + +// SectionTransformFn is a callback that receives the current content of a system prompt section +// and returns the transformed content. Used with the "transform" action to read-then-write +// modify sections at runtime. +type SectionTransformFn func(currentContent string) (string, error) + +// SectionOverride defines an override operation for a single system prompt section. +type SectionOverride struct { + // Action is the operation to perform: "replace", "remove", "append", "prepend", or "transform". + Action SectionOverrideAction `json:"action,omitempty"` + // Content for the override. Optional for all actions. Ignored for "remove". + Content string `json:"content,omitempty"` + // Transform is a callback invoked when Action is "transform". + // The runtime calls this with the current section content and uses the returned string. + // Excluded from JSON serialization; the SDK registers it as an RPC callback internally. + Transform SectionTransformFn `json:"-"` +} + // SystemMessageAppendConfig is append mode: use CLI foundation with optional appended content. type SystemMessageAppendConfig struct { // Mode is optional, defaults to "append" @@ -129,11 +174,15 @@ type SystemMessageReplaceConfig struct { } // SystemMessageConfig represents system message configuration for session creation. -// Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control. -// In Go, use one struct or the other based on your needs. +// - Append mode (default): SDK foundation + optional custom content +// - Replace mode: Full control, caller provides entire system message +// - Customize mode: Section-level overrides with graceful fallback +// +// In Go, use one struct and set fields appropriate for the desired mode. type SystemMessageConfig struct { - Mode string `json:"mode,omitempty"` - Content string `json:"content,omitempty"` + Mode string `json:"mode,omitempty"` + Content string `json:"content,omitempty"` + Sections map[string]SectionOverride `json:"sections,omitempty"` } // PermissionRequestResultKind represents the kind of a permission request result. diff --git a/nodejs/README.md b/nodejs/README.md index e9d23c529a..cc5d624165 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -473,7 +473,45 @@ const session = await client.createSession({ }); ``` -The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"`. +The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`. + +#### Customize Mode + +Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest: + +```typescript +import { SYSTEM_PROMPT_SECTIONS } from "@github/copilot-sdk"; +import type { SectionOverride, SystemPromptSection } from "@github/copilot-sdk"; + +const session = await client.createSession({ + model: "gpt-5", + systemMessage: { + mode: "customize", + sections: { + // Replace the tone/style section + tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." }, + // Remove coding-specific rules + code_change_rules: { action: "remove" }, + // Append to existing guidelines + guidelines: { action: "append", content: "\n* Always cite data sources" }, + }, + // Additional instructions appended after all sections + content: "Focus on financial analysis and reporting.", + }, +}); +``` + +Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section. + +Each section override supports four actions: +- **`replace`** — Replace the section content entirely +- **`remove`** — Remove the section from the prompt +- **`append`** — Add content after the existing section +- **`prepend`** — Add content before the existing section + +Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. + +#### Replace Mode For full control (removes all guardrails), use `mode: "replace"`: diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 46d932242a..9b8af3dd1c 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -36,6 +36,7 @@ import type { GetStatusResponse, ModelInfo, ResumeSessionConfig, + SectionTransformFn, SessionConfig, SessionContext, SessionEvent, @@ -44,6 +45,7 @@ import type { SessionLifecycleHandler, SessionListFilter, SessionMetadata, + SystemMessageCustomizeConfig, TelemetryConfig, Tool, ToolCallRequestPayload, @@ -82,6 +84,45 @@ function toJsonSchema(parameters: Tool["parameters"]): Record | return parameters; } +/** + * Extract transform callbacks from a system message config and prepare the wire payload. + * Function-valued actions are replaced with `{ action: "transform" }` for serialization, + * and the original callbacks are returned in a separate map. + */ +function extractTransformCallbacks(systemMessage: SessionConfig["systemMessage"]): { + wirePayload: SessionConfig["systemMessage"]; + transformCallbacks: Map | undefined; +} { + if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) { + return { wirePayload: systemMessage, transformCallbacks: undefined }; + } + + const transformCallbacks = new Map(); + const wireSections: Record = {}; + + for (const [sectionId, override] of Object.entries(systemMessage.sections)) { + if (!override) continue; + + if (typeof override.action === "function") { + transformCallbacks.set(sectionId, override.action); + wireSections[sectionId] = { action: "transform" }; + } else { + wireSections[sectionId] = { action: override.action, content: override.content }; + } + } + + if (transformCallbacks.size === 0) { + return { wirePayload: systemMessage, transformCallbacks: undefined }; + } + + const wirePayload: SystemMessageCustomizeConfig = { + ...systemMessage, + sections: wireSections as SystemMessageCustomizeConfig["sections"], + }; + + return { wirePayload, transformCallbacks }; +} + function getNodeExecPath(): string { if (process.versions.bun) { return "node"; @@ -605,6 +646,15 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + + // Extract transform callbacks from system message config before serialization. + const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks( + config.systemMessage + ); + if (transformCallbacks) { + session.registerTransformCallbacks(transformCallbacks); + } + if (config.onEvent) { session.on(config.onEvent); } @@ -624,7 +674,7 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, })), - systemMessage: config.systemMessage, + systemMessage: wireSystemMessage, availableTools: config.availableTools, excludedTools: config.excludedTools, provider: config.provider, @@ -711,6 +761,15 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + + // Extract transform callbacks from system message config before serialization. + const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks( + config.systemMessage + ); + if (transformCallbacks) { + session.registerTransformCallbacks(transformCallbacks); + } + if (config.onEvent) { session.on(config.onEvent); } @@ -723,7 +782,7 @@ export class CopilotClient { clientName: config.clientName, model: config.model, reasoningEffort: config.reasoningEffort, - systemMessage: config.systemMessage, + systemMessage: wireSystemMessage, availableTools: config.availableTools, excludedTools: config.excludedTools, tools: config.tools?.map((tool) => ({ @@ -1477,6 +1536,15 @@ export class CopilotClient { }): Promise<{ output?: unknown }> => await this.handleHooksInvoke(params) ); + this.connection.onRequest( + "systemMessage.transform", + async (params: { + sessionId: string; + sections: Record; + }): Promise<{ sections: Record }> => + await this.handleSystemMessageTransform(params) + ); + this.connection.onClose(() => { this.state = "disconnected"; }); @@ -1588,6 +1656,27 @@ export class CopilotClient { return { output }; } + private async handleSystemMessageTransform(params: { + sessionId: string; + sections: Record; + }): Promise<{ sections: Record }> { + if ( + !params || + typeof params.sessionId !== "string" || + !params.sections || + typeof params.sections !== "object" + ) { + throw new Error("Invalid systemMessage.transform payload"); + } + + const session = this.sessions.get(params.sessionId); + if (!session) { + throw new Error(`Session not found: ${params.sessionId}`); + } + + return await session._handleSystemMessageTransform(params.sections); + } + // ======================================================================== // Protocol v2 backward-compatibility adapters // ======================================================================== diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 214b800508..f3788e168a 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -10,7 +10,7 @@ export { CopilotClient } from "./client.js"; export { CopilotSession, type AssistantMessageEvent } from "./session.js"; -export { defineTool, approveAll } from "./types.js"; +export { defineTool, approveAll, SYSTEM_PROMPT_SECTIONS } from "./types.js"; export type { ConnectionState, CopilotClientOptions, @@ -31,6 +31,9 @@ export type { PermissionRequest, PermissionRequestResult, ResumeSessionConfig, + SectionOverride, + SectionOverrideAction, + SectionTransformFn, SessionConfig, SessionEvent, SessionEventHandler, @@ -44,7 +47,9 @@ export type { SessionMetadata, SystemMessageAppendConfig, SystemMessageConfig, + SystemMessageCustomizeConfig, SystemMessageReplaceConfig, + SystemPromptSection, TelemetryConfig, TraceContext, TraceContextProvider, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 674526764c..122f4ece83 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -17,6 +17,7 @@ import type { PermissionRequest, PermissionRequestResult, ReasoningEffort, + SectionTransformFn, SessionEvent, SessionEventHandler, SessionEventPayload, @@ -70,6 +71,7 @@ export class CopilotSession { private permissionHandler?: PermissionHandler; private userInputHandler?: UserInputHandler; private hooks?: SessionHooks; + private transformCallbacks?: Map; private _rpc: ReturnType | null = null; private traceContextProvider?: TraceContextProvider; @@ -517,6 +519,48 @@ export class CopilotSession { this.hooks = hooks; } + /** + * Registers transform callbacks for system message sections. + * + * @param callbacks - Map of section ID to transform callback, or undefined to clear + * @internal This method is typically called internally when creating a session. + */ + registerTransformCallbacks(callbacks?: Map): void { + this.transformCallbacks = callbacks; + } + + /** + * Handles a systemMessage.transform request from the runtime. + * Dispatches each section to its registered transform callback. + * + * @param sections - Map of section IDs to their current rendered content + * @returns A promise that resolves with the transformed sections + * @internal This method is for internal use by the SDK. + */ + async _handleSystemMessageTransform( + sections: Record + ): Promise<{ sections: Record }> { + const result: Record = {}; + + for (const [sectionId, { content }] of Object.entries(sections)) { + const callback = this.transformCallbacks?.get(sectionId); + if (callback) { + try { + const transformed = await callback(content); + result[sectionId] = { content: transformed }; + } catch (_error) { + // Callback failed — return original content + result[sectionId] = { content }; + } + } else { + // No callback for this section — pass through unchanged + result[sectionId] = { content }; + } + } + + return { sections: result }; + } + /** * Handles a permission request in the v2 protocol format (synchronous RPC). * Used as a back-compat adapter when connected to a v2 server. diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9052bde524..992dbdb9d4 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -272,6 +272,79 @@ export interface ToolCallResponsePayload { result: ToolResult; } +/** + * Known system prompt section identifiers for the "customize" mode. + * Each section corresponds to a distinct part of the system prompt. + */ +export type SystemPromptSection = + | "identity" + | "tone" + | "tool_efficiency" + | "environment_context" + | "code_change_rules" + | "guidelines" + | "safety" + | "tool_instructions" + | "custom_instructions" + | "last_instructions"; + +/** Section metadata for documentation and tooling. */ +export const SYSTEM_PROMPT_SECTIONS: Record = { + identity: { description: "Agent identity preamble and mode statement" }, + tone: { description: "Response style, conciseness rules, output formatting preferences" }, + tool_efficiency: { description: "Tool usage patterns, parallel calling, batching guidelines" }, + environment_context: { description: "CWD, OS, git root, directory listing, available tools" }, + code_change_rules: { description: "Coding rules, linting/testing, ecosystem tools, style" }, + guidelines: { description: "Tips, behavioral best practices, behavioral guidelines" }, + safety: { description: "Environment limitations, prohibited actions, security policies" }, + tool_instructions: { description: "Per-tool usage instructions" }, + custom_instructions: { description: "Repository and organization custom instructions" }, + last_instructions: { + description: + "End-of-prompt instructions: parallel tool calling, persistence, task completion", + }, +}; + +/** + * Transform callback for a single section: receives current content, returns new content. + */ +export type SectionTransformFn = (currentContent: string) => string | Promise; + +/** + * Override action: a string literal for static overrides, or a callback for transforms. + * + * - `"replace"`: Replace section content entirely + * - `"remove"`: Remove the section + * - `"append"`: Append to existing section content + * - `"prepend"`: Prepend to existing section content + * - `function`: Transform callback — receives current section content, returns new content + */ +export type SectionOverrideAction = + | "replace" + | "remove" + | "append" + | "prepend" + | SectionTransformFn; + +/** + * Override operation for a single system prompt section. + */ +export interface SectionOverride { + /** + * The operation to perform on this section. + * Can be a string action or a transform callback function. + */ + action: SectionOverrideAction; + + /** + * Content for the override. Optional for all actions. + * - For replace, omitting content replaces with an empty string. + * - For append/prepend, content is added before/after the existing section. + * - Ignored for the remove action. + */ + content?: string; +} + /** * Append mode: Use CLI foundation with optional appended content (default). */ @@ -298,12 +371,37 @@ export interface SystemMessageReplaceConfig { content: string; } +/** + * Customize mode: Override individual sections of the system prompt. + * Keeps the SDK-managed prompt structure while allowing targeted modifications. + */ +export interface SystemMessageCustomizeConfig { + mode: "customize"; + + /** + * Override specific sections of the system prompt by section ID. + * Unknown section IDs gracefully fall back: content-bearing overrides are appended + * to additional instructions, and "remove" on unknown sections is a silent no-op. + */ + sections?: Partial>; + + /** + * Additional content appended after all sections. + * Equivalent to append mode's content field — provided for convenience. + */ + content?: string; +} + /** * System message configuration for session creation. * - Append mode (default): SDK foundation + optional custom content * - Replace mode: Full control, caller provides entire system message + * - Customize mode: Section-level overrides with graceful fallback */ -export type SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig; +export type SystemMessageConfig = + | SystemMessageAppendConfig + | SystemMessageReplaceConfig + | SystemMessageCustomizeConfig; /** * Permission request types from the server diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 1eb8a175d5..dbcbed8bb7 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -96,6 +96,33 @@ describe("Sessions", async () => { expect(systemMessage).toEqual(testSystemMessage); // Exact match }); + it("should create a session with customized systemMessage config", async () => { + const customTone = "Respond in a warm, professional tone. Be thorough in explanations."; + const appendedContent = "Always mention quarterly earnings."; + const session = await client.createSession({ + onPermissionRequest: approveAll, + systemMessage: { + mode: "customize", + sections: { + tone: { action: "replace", content: customTone }, + code_change_rules: { action: "remove" }, + }, + content: appendedContent, + }, + }); + + const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" }); + expect(assistantMessage?.data.content).toBeDefined(); + + // Validate the system message sent to the model + const traffic = await openAiEndpoint.getExchanges(); + const systemMessage = getSystemMessage(traffic[0]); + expect(systemMessage).toContain(customTone); + expect(systemMessage).toContain(appendedContent); + // The code_change_rules section should have been removed + expect(systemMessage).not.toContain(""); + }); + it("should create a session with availableTools", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, diff --git a/nodejs/test/e2e/system_message_transform.test.ts b/nodejs/test/e2e/system_message_transform.test.ts new file mode 100644 index 0000000000..ef37c39e9a --- /dev/null +++ b/nodejs/test/e2e/system_message_transform.test.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from "fs/promises"; +import { join } from "path"; +import { describe, expect, it } from "vitest"; +import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("System message transform", async () => { + const { copilotClient: client, openAiEndpoint, workDir } = await createSdkTestContext(); + + it("should invoke transform callbacks with section content", async () => { + const transformedSections: Record = {}; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + systemMessage: { + mode: "customize", + sections: { + identity: { + action: (content: string) => { + transformedSections["identity"] = content; + // Pass through unchanged + return content; + }, + }, + tone: { + action: (content: string) => { + transformedSections["tone"] = content; + return content; + }, + }, + }, + }, + }); + + await writeFile(join(workDir, "test.txt"), "Hello transform!"); + + await session.sendAndWait({ + prompt: "Read the contents of test.txt and tell me what it says", + }); + + // Transform callbacks should have been invoked with real section content + expect(Object.keys(transformedSections).length).toBe(2); + expect(transformedSections["identity"]).toBeDefined(); + expect(transformedSections["identity"]!.length).toBeGreaterThan(0); + expect(transformedSections["tone"]).toBeDefined(); + expect(transformedSections["tone"]!.length).toBeGreaterThan(0); + + await session.disconnect(); + }); + + it("should apply transform modifications to section content", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + systemMessage: { + mode: "customize", + sections: { + identity: { + action: (content: string) => { + return content + "\nTRANSFORM_MARKER"; + }, + }, + }, + }, + }); + + await writeFile(join(workDir, "hello.txt"), "Hello!"); + + await session.sendAndWait({ + prompt: "Read the contents of hello.txt", + }); + + // Verify the transform result was actually applied to the system message + const traffic = await openAiEndpoint.getExchanges(); + const systemMessage = getSystemMessage(traffic[0]); + expect(systemMessage).toContain("TRANSFORM_MARKER"); + + await session.disconnect(); + }); + + it("should work with static overrides and transforms together", async () => { + const transformedSections: Record = {}; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + systemMessage: { + mode: "customize", + sections: { + // Static override + safety: { action: "remove" }, + // Transform + identity: { + action: (content: string) => { + transformedSections["identity"] = content; + return content; + }, + }, + }, + }, + }); + + await writeFile(join(workDir, "combo.txt"), "Combo test!"); + + await session.sendAndWait({ + prompt: "Read the contents of combo.txt and tell me what it says", + }); + + // Transform should have been invoked + expect(transformedSections["identity"]).toBeDefined(); + expect(transformedSections["identity"]!.length).toBeGreaterThan(0); + + await session.disconnect(); + }); +}); + +function getSystemMessage(exchange: ParsedHttpExchange): string | undefined { + const systemMessage = exchange.request.messages.find((m) => m.role === "system") as + | { role: "system"; content: string } + | undefined; + return systemMessage?.content; +} diff --git a/python/README.md b/python/README.md index 2394c351ab..139098fa39 100644 --- a/python/README.md +++ b/python/README.md @@ -144,7 +144,10 @@ All parameters are keyword-only: - `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option. - `tools` (list): Custom tools exposed to the CLI. -- `system_message` (dict): System message configuration. +- `system_message` (dict): System message configuration. Supports three modes: + - **append** (default): Appends `content` after the SDK-managed prompt + - **replace**: Replaces the entire prompt with `content` + - **customize**: Selectively override individual sections via `sections` dict (keys: `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"last_instructions"`; values: `SectionOverride` with `action` and optional `content`) - `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`. - `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set. - `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section. @@ -217,6 +220,54 @@ unsubscribe() - `session.foreground` - A session became the foreground session in TUI - `session.background` - A session is no longer the foreground session +### System Message Customization + +Control the system prompt using `system_message` in session config: + +```python +session = await client.create_session( + system_message={ + "content": "Always check for security vulnerabilities before suggesting changes." + } +) +``` + +The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`. + +#### Customize Mode + +Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest: + +```python +from copilot import SYSTEM_PROMPT_SECTIONS + +session = await client.create_session( + system_message={ + "mode": "customize", + "sections": { + # Replace the tone/style section + "tone": {"action": "replace", "content": "Respond in a warm, professional tone. Be thorough in explanations."}, + # Remove coding-specific rules + "code_change_rules": {"action": "remove"}, + # Append to existing guidelines + "guidelines": {"action": "append", "content": "\n* Always cite data sources"}, + }, + # Additional instructions appended after all sections + "content": "Focus on financial analysis and reporting.", + } +) +``` + +Available section IDs: `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"last_instructions"`. Use the `SYSTEM_PROMPT_SECTIONS` dict for descriptions of each section. + +Each section override supports four actions: +- **`replace`** — Replace the section content entirely +- **`remove`** — Remove the section from the prompt +- **`append`** — Add content after the existing section +- **`prepend`** — Add content before the existing section + +Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. + ### Tools Define tools with automatic JSON schema generation using the `@define_tool` decorator and Pydantic models: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index e1fdf92538..6a007afa30 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -8,6 +8,7 @@ from .session import CopilotSession from .tools import define_tool from .types import ( + SYSTEM_PROMPT_SECTIONS, Attachment, AzureProviderOptions, BlobAttachment, @@ -30,6 +31,9 @@ PermissionRequestResult, PingResponse, ProviderConfig, + SectionOverride, + SectionOverrideAction, + SectionTransformFn, SelectionAttachment, SessionContext, SessionEvent, @@ -37,6 +41,11 @@ SessionMetadata, StopError, SubprocessConfig, + SystemMessageAppendConfig, + SystemMessageConfig, + SystemMessageCustomizeConfig, + SystemMessageReplaceConfig, + SystemPromptSection, TelemetryConfig, Tool, ToolHandler, @@ -71,13 +80,22 @@ "PermissionRequestResult", "PingResponse", "ProviderConfig", + "SectionOverride", + "SectionOverrideAction", + "SectionTransformFn", "SelectionAttachment", "SessionContext", "SessionEvent", "SessionListFilter", "SessionMetadata", "StopError", + "SYSTEM_PROMPT_SECTIONS", "SubprocessConfig", + "SystemMessageAppendConfig", + "SystemMessageConfig", + "SystemMessageCustomizeConfig", + "SystemMessageReplaceConfig", + "SystemPromptSection", "TelemetryConfig", "Tool", "ToolHandler", diff --git a/python/copilot/client.py b/python/copilot/client.py index 28050088e6..e9dd98d35d 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -45,6 +45,7 @@ PingResponse, ProviderConfig, ReasoningEffort, + SectionTransformFn, SessionEvent, SessionHooks, SessionLifecycleEvent, @@ -73,6 +74,40 @@ MIN_PROTOCOL_VERSION = 2 +def _extract_transform_callbacks( + system_message: dict | None, +) -> tuple[dict | None, dict[str, SectionTransformFn] | None]: + """Extract function-valued actions from system message config. + + Returns a wire-safe payload (with callable actions replaced by ``"transform"``) + and a dict of transform callbacks keyed by section ID. + """ + if ( + not system_message + or system_message.get("mode") != "customize" + or not system_message.get("sections") + ): + return system_message, None + + callbacks: dict[str, SectionTransformFn] = {} + wire_sections: dict[str, dict] = {} + for section_id, override in system_message["sections"].items(): + if not override: + continue + action = override.get("action") + if callable(action): + callbacks[section_id] = action + wire_sections[section_id] = {"action": "transform"} + else: + wire_sections[section_id] = override + + if not callbacks: + return system_message, None + + wire_payload = {**system_message, "sections": wire_sections} + return wire_payload, callbacks + + def _get_bundled_cli_path() -> str | None: """Get the path to the bundled CLI binary, if available.""" # The binary is bundled in copilot/bin/ within the package @@ -548,8 +583,9 @@ async def create_session( if tool_defs: payload["tools"] = tool_defs - if system_message: - payload["systemMessage"] = system_message + wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message) + if wire_system_message: + payload["systemMessage"] = wire_system_message if available_tools is not None: payload["availableTools"] = available_tools @@ -627,6 +663,8 @@ async def create_session( session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + if transform_callbacks: + session._register_transform_callbacks(transform_callbacks) if on_event: session.on(on_event) with self._sessions_lock: @@ -760,8 +798,9 @@ async def resume_session( if tool_defs: payload["tools"] = tool_defs - if system_message: - payload["systemMessage"] = system_message + wire_system_message, transform_callbacks = _extract_transform_callbacks(system_message) + if wire_system_message: + payload["systemMessage"] = wire_system_message if available_tools is not None: payload["availableTools"] = available_tools @@ -839,6 +878,8 @@ async def resume_session( session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + if transform_callbacks: + session._register_transform_callbacks(transform_callbacks) if on_event: session.on(on_event) with self._sessions_lock: @@ -1485,6 +1526,9 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) + self._client.set_request_handler( + "systemMessage.transform", self._handle_system_message_transform + ) # Start listening for messages loop = asyncio.get_running_loop() @@ -1570,6 +1614,9 @@ def handle_notification(method: str, params: dict): self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) + self._client.set_request_handler( + "systemMessage.transform", self._handle_system_message_transform + ) # Start listening for messages loop = asyncio.get_running_loop() @@ -1630,6 +1677,32 @@ async def _handle_hooks_invoke(self, params: dict) -> dict: output = await session._handle_hooks_invoke(hook_type, input_data) return {"output": output} + async def _handle_system_message_transform(self, params: dict) -> dict: + """ + Handle a systemMessage.transform request from the CLI server. + + Args: + params: The transform parameters from the server. + + Returns: + A dict containing the transformed sections. + + Raises: + ValueError: If the request payload is invalid. + """ + session_id = params.get("sessionId") + sections = params.get("sections") + + if not session_id or not sections: + raise ValueError("invalid systemMessage.transform payload") + + with self._sessions_lock: + session = self._sessions.get(session_id) + if not session: + raise ValueError(f"unknown session {session_id}") + + return await session._handle_system_message_transform(sections) + # ======================================================================== # Protocol v2 backward-compatibility adapters # ======================================================================== diff --git a/python/copilot/session.py b/python/copilot/session.py index 7a8b9f05d6..29421724cf 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -29,6 +29,7 @@ Attachment, PermissionRequest, PermissionRequestResult, + SectionTransformFn, SessionHooks, Tool, ToolHandler, @@ -97,6 +98,8 @@ def __init__(self, session_id: str, client: Any, workspace_path: str | None = No self._user_input_handler_lock = threading.Lock() self._hooks: SessionHooks | None = None self._hooks_lock = threading.Lock() + self._transform_callbacks: dict[str, SectionTransformFn] | None = None + self._transform_callbacks_lock = threading.Lock() self._rpc: SessionRpc | None = None @property @@ -634,6 +637,62 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: # Hook failed, return None return None + def _register_transform_callbacks( + self, callbacks: dict[str, SectionTransformFn] | None + ) -> None: + """ + Register transform callbacks for system message sections. + + Transform callbacks allow modifying individual sections of the system + prompt at runtime. Each callback receives the current section content + and returns the transformed content. + + Note: + This method is internal. Transform callbacks are typically registered + when creating a session via :meth:`CopilotClient.create_session`. + + Args: + callbacks: A dict mapping section IDs to transform functions, + or None to remove all callbacks. + """ + with self._transform_callbacks_lock: + self._transform_callbacks = callbacks + + async def _handle_system_message_transform( + self, sections: dict[str, dict[str, str]] + ) -> dict[str, dict[str, dict[str, str]]]: + """ + Handle a systemMessage.transform request from the runtime. + + Note: + This method is internal and should not be called directly. + + Args: + sections: A dict mapping section IDs to section data dicts + containing a ``"content"`` key. + + Returns: + A dict with a ``"sections"`` key containing the transformed section data. + """ + with self._transform_callbacks_lock: + callbacks = self._transform_callbacks + + result: dict[str, dict[str, str]] = {} + for section_id, section_data in sections.items(): + content = section_data.get("content", "") + callback = callbacks.get(section_id) if callbacks else None + if callback: + try: + transformed = callback(content) + if inspect.isawaitable(transformed): + transformed = await transformed + result[section_id] = {"content": str(transformed)} + except Exception: # pylint: disable=broad-except + result[section_id] = {"content": content} + else: + result[section_id] = {"content": content} + return {"sections": result} + async def get_messages(self) -> list[SessionEvent]: """ Retrieve all events and messages from this session's history. diff --git a/python/copilot/types.py b/python/copilot/types.py index 17be065bcf..ef9a4bce47 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from dataclasses import KW_ONLY, dataclass, field -from typing import Any, Literal, NotRequired, TypedDict +from typing import Any, Literal, NotRequired, Required, TypedDict # Import generated SessionEvent types from .generated.session_events import ( @@ -213,7 +213,52 @@ class Tool: # System message configuration (discriminated union) -# Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control +# Use SystemMessageAppendConfig for default behavior, +# SystemMessageReplaceConfig for full control, +# or SystemMessageCustomizeConfig for section-level overrides. + +# Known system prompt section identifiers for the "customize" mode. +SystemPromptSection = Literal[ + "identity", + "tone", + "tool_efficiency", + "environment_context", + "code_change_rules", + "guidelines", + "safety", + "tool_instructions", + "custom_instructions", + "last_instructions", +] + +SYSTEM_PROMPT_SECTIONS: dict[SystemPromptSection, str] = { + "identity": "Agent identity preamble and mode statement", + "tone": "Response style, conciseness rules, output formatting preferences", + "tool_efficiency": "Tool usage patterns, parallel calling, batching guidelines", + "environment_context": "CWD, OS, git root, directory listing, available tools", + "code_change_rules": "Coding rules, linting/testing, ecosystem tools, style", + "guidelines": "Tips, behavioral best practices, behavioral guidelines", + "safety": "Environment limitations, prohibited actions, security policies", + "tool_instructions": "Per-tool usage instructions", + "custom_instructions": "Repository and organization custom instructions", + "last_instructions": ( + "End-of-prompt instructions: parallel tool calling, persistence, task completion" + ), +} + + +SectionTransformFn = Callable[[str], str | Awaitable[str]] +"""Transform callback: receives current section content, returns new content.""" + +SectionOverrideAction = Literal["replace", "remove", "append", "prepend"] | SectionTransformFn +"""Override action: a string literal for static overrides, or a callback for transforms.""" + + +class SectionOverride(TypedDict, total=False): + """Override operation for a single system prompt section.""" + + action: Required[SectionOverrideAction] + content: NotRequired[str] class SystemMessageAppendConfig(TypedDict, total=False): @@ -235,8 +280,21 @@ class SystemMessageReplaceConfig(TypedDict): content: str -# Union type - use one or the other -SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig +class SystemMessageCustomizeConfig(TypedDict, total=False): + """ + Customize mode: Override individual sections of the system prompt. + Keeps the SDK-managed prompt structure while allowing targeted modifications. + """ + + mode: Required[Literal["customize"]] + sections: NotRequired[dict[SystemPromptSection, SectionOverride]] + content: NotRequired[str] + + +# Union type - use one based on your needs +SystemMessageConfig = ( + SystemMessageAppendConfig | SystemMessageReplaceConfig | SystemMessageCustomizeConfig +) # Permission result types diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index ffb0cd2bc5..04f0b448e7 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -82,6 +82,33 @@ async def test_should_create_a_session_with_replaced_systemMessage_config( system_message = _get_system_message(traffic[0]) assert system_message == test_system_message # Exact match + async def test_should_create_a_session_with_customized_systemMessage_config( + self, ctx: E2ETestContext + ): + custom_tone = "Respond in a warm, professional tone. Be thorough in explanations." + appended_content = "Always mention quarterly earnings." + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + system_message={ + "mode": "customize", + "sections": { + "tone": {"action": "replace", "content": custom_tone}, + "code_change_rules": {"action": "remove"}, + }, + "content": appended_content, + }, + ) + + assistant_message = await session.send_and_wait("Who are you?") + assert assistant_message is not None + + # Validate the system message sent to the model + traffic = await ctx.get_exchanges() + system_message = _get_system_message(traffic[0]) + assert custom_tone in system_message + assert appended_content in system_message + assert "" not in system_message + async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, diff --git a/python/e2e/test_system_message_transform.py b/python/e2e/test_system_message_transform.py new file mode 100644 index 0000000000..9ae1706378 --- /dev/null +++ b/python/e2e/test_system_message_transform.py @@ -0,0 +1,123 @@ +""" +Copyright (c) Microsoft Corporation. + +Tests for system message transform functionality +""" + +import pytest + +from copilot import PermissionHandler + +from .testharness import E2ETestContext +from .testharness.helper import write_file + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +class TestSystemMessageTransform: + async def test_should_invoke_transform_callbacks_with_section_content( + self, ctx: E2ETestContext + ): + """Test that transform callbacks are invoked with the section content""" + identity_contents = [] + tone_contents = [] + + async def identity_transform(content: str) -> str: + identity_contents.append(content) + return content + + async def tone_transform(content: str) -> str: + tone_contents.append(content) + return content + + session = await ctx.client.create_session( + system_message={ + "mode": "customize", + "sections": { + "identity": {"action": identity_transform}, + "tone": {"action": tone_transform}, + }, + }, + on_permission_request=PermissionHandler.approve_all, + ) + + write_file(ctx.work_dir, "test.txt", "Hello transform!") + + await session.send_and_wait("Read the contents of test.txt and tell me what it says") + + # Both transform callbacks should have been invoked + assert len(identity_contents) > 0 + assert len(tone_contents) > 0 + + # Callbacks should have received non-empty content + assert all(len(c) > 0 for c in identity_contents) + assert all(len(c) > 0 for c in tone_contents) + + await session.disconnect() + + async def test_should_apply_transform_modifications_to_section_content( + self, ctx: E2ETestContext + ): + """Test that transform modifications are applied to the section content""" + + async def identity_transform(content: str) -> str: + return content + "\nTRANSFORM_MARKER" + + session = await ctx.client.create_session( + system_message={ + "mode": "customize", + "sections": { + "identity": {"action": identity_transform}, + }, + }, + on_permission_request=PermissionHandler.approve_all, + ) + + write_file(ctx.work_dir, "hello.txt", "Hello!") + + await session.send_and_wait("Read the contents of hello.txt") + + # Verify the transform result was actually applied to the system message + traffic = await ctx.get_exchanges() + system_message = _get_system_message(traffic[0]) + assert "TRANSFORM_MARKER" in system_message + + await session.disconnect() + + async def test_should_work_with_static_overrides_and_transforms_together( + self, ctx: E2ETestContext + ): + """Test that static overrides and transforms work together""" + identity_contents = [] + + async def identity_transform(content: str) -> str: + identity_contents.append(content) + return content + + session = await ctx.client.create_session( + system_message={ + "mode": "customize", + "sections": { + "safety": {"action": "remove"}, + "identity": {"action": identity_transform}, + }, + }, + on_permission_request=PermissionHandler.approve_all, + ) + + write_file(ctx.work_dir, "combo.txt", "Combo test!") + + await session.send_and_wait("Read the contents of combo.txt and tell me what it says") + + # The transform callback should have been invoked + assert len(identity_contents) > 0 + + await session.disconnect() + + +def _get_system_message(exchange: dict) -> str: + messages = exchange.get("request", {}).get("messages", []) + for msg in messages: + if msg.get("role") == "system": + return msg.get("content", "") + return "" diff --git a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml new file mode 100644 index 0000000000..f3ce077a62 --- /dev/null +++ b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml @@ -0,0 +1,35 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Who are you? + - role: assistant + content: >- + I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-4.5 (model ID: + claude-sonnet-4.5). + + + I'm here to help you with software engineering tasks, including: + + - Writing, debugging, and refactoring code + + - Running commands and managing development workflows + + - Exploring codebases and understanding how things work + + - Setting up projects, installing dependencies, and configuring tools + + - Working with Git, testing, and deployment tasks + + - Planning and implementing features + + + I have access to a variety of tools including file operations, shell commands, code search, and specialized + sub-agents for specific tasks. I can work with multiple languages and frameworks, and I'm designed to be + efficient by running tasks in parallel when possible. + + + How can I help you today? diff --git a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml new file mode 100644 index 0000000000..98004f2b0e --- /dev/null +++ b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml @@ -0,0 +1,33 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Read the contents of hello.txt + - role: assistant + content: I'll read the hello.txt file for you. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Reading hello.txt file"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/hello.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: 1. Hello! + - role: assistant + content: |- + The file hello.txt contains: + ``` + Hello! + ``` diff --git a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml new file mode 100644 index 0000000000..631a8eef8b --- /dev/null +++ b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml @@ -0,0 +1,54 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Read the contents of test.txt and tell me what it says + - role: assistant + content: I'll read the test.txt file for you. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Reading test.txt file"}' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/test.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: Read the contents of test.txt and tell me what it says + - role: assistant + content: I'll read the test.txt file for you. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Reading test.txt file"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/test.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: 1. Hello transform! + - role: assistant + content: |- + The file test.txt contains: + ``` + Hello transform! + ``` diff --git a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml new file mode 100644 index 0000000000..9cb5156943 --- /dev/null +++ b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml @@ -0,0 +1,50 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Read the contents of combo.txt and tell me what it says + - role: assistant + content: I'll read the contents of combo.txt for you. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Reading file contents"}' + - role: assistant + tool_calls: + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/combo.txt"}' + - messages: + - role: system + content: ${system} + - role: user + content: Read the contents of combo.txt and tell me what it says + - role: assistant + content: I'll read the contents of combo.txt for you. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Reading file contents"}' + - id: toolcall_1 + type: function + function: + name: view + arguments: '{"path":"${workdir}/combo.txt"}' + - role: tool + tool_call_id: toolcall_0 + content: Intent logged + - role: tool + tool_call_id: toolcall_1 + content: 1. Combo test! + - role: assistant + content: The file combo.txt contains a single line that says "Combo test!" From 1ff9e1b84a06cada43da99919526bcd87d445556 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:42:46 +0000 Subject: [PATCH 61/61] Update @github/copilot to 1.0.10 (#900) - Updated nodejs and test harness dependencies - Re-ran code generators - Formatted generated code Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- dotnet/src/Generated/SessionEvents.cs | 5 ++ go/generated_session_events.go | 3 ++ nodejs/package-lock.json | 56 +++++++++++----------- nodejs/package.json | 2 +- nodejs/samples/package-lock.json | 2 +- nodejs/src/generated/session-events.ts | 4 ++ python/copilot/generated/session_events.py | 9 +++- test/harness/package-lock.json | 56 +++++++++++----------- test/harness/package.json | 2 +- 9 files changed, 79 insertions(+), 60 deletions(-) diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 2821052d0b..d5ef13d530 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1258,6 +1258,11 @@ public partial class SessionHandoffData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("remoteSessionId")] public string? RemoteSessionId { get; set; } + + /// GitHub host URL for the source session (e.g., https://github.com or https://tenant.ghe.com). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("host")] + public string? Host { get; set; } } /// Conversation truncation statistics including token counts and removed content metrics. diff --git a/go/generated_session_events.go b/go/generated_session_events.go index fbdb1597fa..591ff53af5 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -392,6 +392,9 @@ type Data struct { Path *string `json:"path,omitempty"` // ISO 8601 timestamp when the handoff occurred HandoffTime *time.Time `json:"handoffTime,omitempty"` + // GitHub host URL for the source session (e.g., https://github.com or + // https://tenant.ghe.com) + Host *string `json:"host,omitempty"` // Session ID of the remote session being handed off RemoteSessionID *string `json:"remoteSessionId,omitempty"` // Repository context for the handed-off session diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index fd56aa84b5..52a84fc9d2 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.10-0", + "@github/copilot": "^1.0.10", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz", - "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10.tgz", + "integrity": "sha512-RpHYMXYpyAgQLYQ3MB8ubV8zMn/zDatwaNmdxcC8ws7jqM+Ojy7Dz4KFKzyT0rCrWoUCAEBXsXoPbP0LY0FgLw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.10-0", - "@github/copilot-darwin-x64": "1.0.10-0", - "@github/copilot-linux-arm64": "1.0.10-0", - "@github/copilot-linux-x64": "1.0.10-0", - "@github/copilot-win32-arm64": "1.0.10-0", - "@github/copilot-win32-x64": "1.0.10-0" + "@github/copilot-darwin-arm64": "1.0.10", + "@github/copilot-darwin-x64": "1.0.10", + "@github/copilot-linux-arm64": "1.0.10", + "@github/copilot-linux-x64": "1.0.10", + "@github/copilot-win32-arm64": "1.0.10", + "@github/copilot-win32-x64": "1.0.10" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz", - "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10.tgz", + "integrity": "sha512-MNlzwkTQ9iUgHQ+2Z25D0KgYZDEl4riEa1Z4/UCNpHXmmBiIY8xVRbXZTNMB69cnagjQ5Z8D2QM2BjI0kqeFPg==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz", - "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10.tgz", + "integrity": "sha512-zAQBCbEue/n4xHBzE9T03iuupVXvLtu24MDMeXXtIC0d4O+/WV6j1zVJrp9Snwr0MBWYH+wUrV74peDDdd1VOQ==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz", - "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10.tgz", + "integrity": "sha512-7mJ3uLe7ITyRi2feM1rMLQ5d0bmUGTUwV1ZxKZwSzWCYmuMn05pg4fhIUdxZZZMkLbOl3kG/1J7BxMCTdS2w7A==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz", - "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10.tgz", + "integrity": "sha512-66NPaxroRScNCs6TZGX3h1RSKtzew0tcHBkj4J1AHkgYLjNHMdjjBwokGtKeMxzYOCAMBbmJkUDdNGkqsKIKUA==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz", - "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10.tgz", + "integrity": "sha512-WC5M+M75sxLn4lvZ1wPA1Lrs/vXFisPXJPCKbKOMKqzwMLX/IbuybTV4dZDIyGEN591YmOdRIylUF0tVwO8Zmw==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz", - "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10.tgz", + "integrity": "sha512-tUfIwyamd0zpm9DVTtbjIWF6j3zrA5A5IkkiuRgsy0HRJPQpeAV7ZYaHEZteHrynaULpl1Gn/Dq0IB4hYc4QtQ==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 7d1822a9cc..7bde33b80d 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.10-0", + "@github/copilot": "^1.0.10", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 77daced15e..a05be5360a 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.10-0", + "@github/copilot": "^1.0.10", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 9ad6d3c024..3453f01917 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -562,6 +562,10 @@ export type SessionEvent = * Session ID of the remote session being handed off */ remoteSessionId?: string; + /** + * GitHub host URL for the source session (e.g., https://github.com or https://tenant.ghe.com) + */ + host?: string; }; } | { diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index f3970b8155..9701a4d9f3 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -1943,6 +1943,10 @@ class Data: handoff_time: datetime | None = None """ISO 8601 timestamp when the handoff occurred""" + host: str | None = None + """GitHub host URL for the source session (e.g., https://github.com or + https://tenant.ghe.com) + """ remote_session_id: str | None = None """Session ID of the remote session being handed off""" @@ -2503,6 +2507,7 @@ def from_dict(obj: Any) -> 'Data': operation = from_union([Operation, from_none], obj.get("operation")) path = from_union([from_str, from_none], obj.get("path")) handoff_time = from_union([from_datetime, from_none], obj.get("handoffTime")) + host = from_union([from_str, from_none], obj.get("host")) remote_session_id = from_union([from_str, from_none], obj.get("remoteSessionId")) repository = from_union([RepositoryClass.from_dict, from_str, from_none], obj.get("repository")) source_type = from_union([SourceType, from_none], obj.get("sourceType")) @@ -2627,7 +2632,7 @@ def from_dict(obj: Any) -> 'Data': servers = from_union([lambda x: from_list(Server.from_dict, x), from_none], obj.get("servers")) status = from_union([ServerStatus, from_none], obj.get("status")) extensions = from_union([lambda x: from_list(Extension.from_dict, x), from_none], obj.get("extensions")) - return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, servers, status, extensions) + return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, host, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, servers, status, extensions) def to_dict(self) -> dict: result: dict = {} @@ -2689,6 +2694,8 @@ def to_dict(self) -> dict: result["path"] = from_union([from_str, from_none], self.path) if self.handoff_time is not None: result["handoffTime"] = from_union([lambda x: x.isoformat(), from_none], self.handoff_time) + if self.host is not None: + result["host"] = from_union([from_str, from_none], self.host) if self.remote_session_id is not None: result["remoteSessionId"] = from_union([from_str, from_none], self.remote_session_id) if self.repository is not None: diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index c8ec038fbf..a9503d7df6 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.10-0", + "@github/copilot": "^1.0.10", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", @@ -462,27 +462,27 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz", - "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10.tgz", + "integrity": "sha512-RpHYMXYpyAgQLYQ3MB8ubV8zMn/zDatwaNmdxcC8ws7jqM+Ojy7Dz4KFKzyT0rCrWoUCAEBXsXoPbP0LY0FgLw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.10-0", - "@github/copilot-darwin-x64": "1.0.10-0", - "@github/copilot-linux-arm64": "1.0.10-0", - "@github/copilot-linux-x64": "1.0.10-0", - "@github/copilot-win32-arm64": "1.0.10-0", - "@github/copilot-win32-x64": "1.0.10-0" + "@github/copilot-darwin-arm64": "1.0.10", + "@github/copilot-darwin-x64": "1.0.10", + "@github/copilot-linux-arm64": "1.0.10", + "@github/copilot-linux-x64": "1.0.10", + "@github/copilot-win32-arm64": "1.0.10", + "@github/copilot-win32-x64": "1.0.10" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz", - "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10.tgz", + "integrity": "sha512-MNlzwkTQ9iUgHQ+2Z25D0KgYZDEl4riEa1Z4/UCNpHXmmBiIY8xVRbXZTNMB69cnagjQ5Z8D2QM2BjI0kqeFPg==", "cpu": [ "arm64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz", - "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10.tgz", + "integrity": "sha512-zAQBCbEue/n4xHBzE9T03iuupVXvLtu24MDMeXXtIC0d4O+/WV6j1zVJrp9Snwr0MBWYH+wUrV74peDDdd1VOQ==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz", - "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10.tgz", + "integrity": "sha512-7mJ3uLe7ITyRi2feM1rMLQ5d0bmUGTUwV1ZxKZwSzWCYmuMn05pg4fhIUdxZZZMkLbOl3kG/1J7BxMCTdS2w7A==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz", - "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10.tgz", + "integrity": "sha512-66NPaxroRScNCs6TZGX3h1RSKtzew0tcHBkj4J1AHkgYLjNHMdjjBwokGtKeMxzYOCAMBbmJkUDdNGkqsKIKUA==", "cpu": [ "x64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz", - "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10.tgz", + "integrity": "sha512-WC5M+M75sxLn4lvZ1wPA1Lrs/vXFisPXJPCKbKOMKqzwMLX/IbuybTV4dZDIyGEN591YmOdRIylUF0tVwO8Zmw==", "cpu": [ "arm64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.10-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz", - "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10.tgz", + "integrity": "sha512-tUfIwyamd0zpm9DVTtbjIWF6j3zrA5A5IkkiuRgsy0HRJPQpeAV7ZYaHEZteHrynaULpl1Gn/Dq0IB4hYc4QtQ==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 25117cac95..3155d3ef30 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.10-0", + "@github/copilot": "^1.0.10", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0",