diff --git a/docs/compatibility.md b/docs/compatibility.md index bfd17915..e113a22f 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -174,15 +174,85 @@ 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 Version | Min Protocol | Max Protocol | Notes | +|-------------|-------------|--------------|-------| +| 0.1.x | 2 | 2 | Exact match required | +| 0.2.x+ | 2 | 3 | Range-based negotiation | -The SDK and CLI must have compatible protocol versions. The SDK will log warnings if versions are mismatched. +The SDK and CLI negotiate a compatible protocol version at startup. The SDK advertises a supported range (`minVersion`–`version`) and the CLI reports its version via `ping`. If the CLI's version falls within the SDK's range, the connection succeeds; otherwise, the SDK throws an error. + +You can check versions at runtime: + +```typescript +const status = await client.ping(); +console.log("Server protocol version:", status.protocolVersion); +``` + +## Protocol v3: Broadcast Events and Multi-Client Sessions + +Protocol v3 changes how the SDK handles tool calls and permission requests internally. **No user-facing API changes are required** — existing code continues to work. + +### What Changed + +| Aspect | Protocol v2 | Protocol v3 | +|--------|-------------|-------------| +| **Tool calls** | CLI sends RPC request directly to the SDK | CLI broadcasts `external_tool.requested` event to all connected clients | +| **Permission requests** | CLI sends RPC request directly to the SDK | CLI broadcasts `permission.requested` event to all connected clients | +| **Multi-client** | One SDK client per CLI server | Multiple SDK clients can share a CLI server and session | + +### How It Works + +In v3, the CLI broadcasts tool and permission events to every connected client. Each client checks whether it has a matching handler: + +- If the client has a handler for the requested tool, it executes the tool and sends the result back via `session.tools.handlePendingToolCall`. +- If the client doesn't have the handler, it responds with an "unsupported" result. +- Permission requests follow the same pattern via `session.permissions.handlePendingPermissionRequest`. + +The SDK handles all of this automatically — you register tools and permission handlers the same way as before: + +```typescript +import { CopilotClient, defineTool } from "@github/copilot-sdk"; + +const myTool = defineTool("my_tool", { + description: "A custom tool", + parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + handler: async (args: { input: string }) => { + return { result: args.input.toUpperCase() }; + }, +}); + +// Works identically on both v2 and v3 +const session = await client.createSession({ + tools: [myTool], + onPermissionRequest: approveAll, +}); +``` + +### Multi-Client Sessions + +With v3, multiple SDK clients can connect to the same CLI server (via `cliUrl`) and share sessions. Each client can register different tools, and the broadcast model routes tool calls to the client that has the matching handler. + +See the [Multi-Client Session Sharing](./guides/session-persistence.md#multi-client-session-sharing) section in the Session Persistence guide for details and code samples. + +## Upgrading from v2 to v3 + +Upgrading is straightforward — no code changes required: + +1. **Update the SDK package** to the latest version +2. **Update the CLI** to a version that supports protocol v3 +3. **That's it** — the SDK auto-negotiates the protocol version + +The SDK remains backward-compatible with v2 CLI servers. If the CLI only supports v2, the SDK operates in v2 mode automatically. Multi-client session features are only available when both the SDK and CLI use v3. + +| Step | TypeScript | Python | Go | .NET | +|------|-----------|--------|-----|------| +| Update SDK | `npm install @github/copilot-sdk@latest` | `pip install --upgrade copilot-sdk` | `go get github.com/github/copilot-sdk/go@latest` | Update `PackageReference` version | +| Update CLI | `npm install @github/copilot@latest` | Bundled with SDK | External install | Bundled with SDK | ## See Also - [Getting Started Guide](./getting-started.md) +- [Session Persistence & Multi-Client](./guides/session-persistence.md) - [Hooks Documentation](./hooks/overview.md) - [MCP Servers Guide](./mcp/overview.md) - [Debugging Guide](./debugging.md) diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index e2b736c1..2660294c 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -502,12 +502,13 @@ const session = await client.createSession({ |------------|-------------|------------| | **BYOK re-authentication** | API keys aren't persisted | Store keys in your secret manager; provide on resume | | **Writable storage** | `~/.copilot/session-state/` must be writable | Mount persistent volume in containers | -| **No session locking** | Concurrent access to same session is undefined | Implement application-level locking or queue | | **Tool state not persisted** | In-memory tool state is lost | Design tools to be stateless or persist their own state | ### Handling Concurrent Access -The SDK doesn't provide built-in session locking. If multiple clients might access the same session: +With protocol v3, the SDK supports **multi-client session sharing** — multiple SDK clients can connect to the same CLI server and operate on the same session simultaneously. The CLI broadcasts tool and permission events to all connected clients, and each client handles the events for tools it has registered. + +If your use case requires **strict serialization** (e.g., only one client sends prompts at a time), you can still use application-level locking: ```typescript // Option 1: Application-level locking with Redis @@ -540,12 +541,213 @@ await withSessionLock("user-123-task-456", async () => { }); ``` +For multi-client session sharing without locking, see the next section. + +## Multi-Client Session Sharing + +Protocol v3 enables multiple SDK clients to share a session via broadcast events. This is useful for architectures where different services each provide different tools, or where a session needs to be accessible from multiple processes. + +```mermaid +flowchart TB + subgraph clients["SDK Clients"] + A["Client A
(tool: search_docs)"] + B["Client B
(tool: run_tests)"] + end + + subgraph server["CLI Server"] + CLI["Copilot CLI
cliUrl: localhost:3000"] + S["Session: task-123"] + end + + A -->|cliUrl| CLI + B -->|cliUrl| CLI + CLI --> S + S -->|broadcast: external_tool.requested| A + S -->|broadcast: external_tool.requested| B +``` + +### How It Works + +1. Start a CLI server (or use an existing one accessible via `cliUrl`) +2. Client A connects and creates a session, registering its tools +3. Client B connects and resumes the same session, registering different tools +4. When the model calls a tool, the CLI broadcasts the request to all clients +5. Each client checks if it has the requested tool and responds accordingly + +### TypeScript + +```typescript +import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk"; + +// Client A: provides search capabilities +const searchDocs = defineTool("search_docs", { + description: "Search the documentation", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] }, + handler: async (args: { query: string }) => { + return { results: [`Result for: ${args.query}`] }; + }, +}); + +const clientA = new CopilotClient({ cliUrl: "localhost:3000" }); +const sessionA = await clientA.createSession({ + sessionId: "shared-task-123", + tools: [searchDocs], + onPermissionRequest: approveAll, +}); + +// Client B: provides test capabilities (different process or service) +const runTests = defineTool("run_tests", { + description: "Run the test suite", + parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }, + handler: async (args: { path: string }) => { + return { passed: true, path: args.path }; + }, +}); + +const clientB = new CopilotClient({ cliUrl: "localhost:3000" }); +const sessionB = await clientB.resumeSession("shared-task-123", { + tools: [runTests], + onPermissionRequest: approveAll, +}); +``` + +### Python + +```python +from copilot import CopilotClient, PermissionHandler +from copilot.tools import define_tool +from pydantic import BaseModel, Field + +# Client A: provides search capabilities +class SearchParams(BaseModel): + query: str = Field(description="Search query") + +@define_tool(description="Search the documentation") +async def search_docs(params: SearchParams) -> dict: + return {"results": [f"Result for: {params.query}"]} + +client_a = CopilotClient(cli_url="localhost:3000") +await client_a.start() +session_a = await client_a.create_session({ + "session_id": "shared-task-123", + "tools": [search_docs], + "on_permission_request": PermissionHandler.approve_all, +}) + +# Client B: provides test capabilities (different process) +class TestParams(BaseModel): + path: str = Field(description="Test path") + +@define_tool(description="Run the test suite") +async def run_tests(params: TestParams) -> dict: + return {"passed": True, "path": params.path} + +client_b = CopilotClient(cli_url="localhost:3000") +await client_b.start() +session_b = await client_b.resume_session("shared-task-123", { + "tools": [run_tests], + "on_permission_request": PermissionHandler.approve_all, +}) +``` + +### Go + + +```go +ctx := context.Background() + +// Client A: provides search capabilities +searchDocs := copilot.DefineTool( + "search_docs", + "Search the documentation", + func(params struct { + Query string `json:"query" jsonschema:"Search query"` + }, inv copilot.ToolInvocation) (map[string]any, error) { + return map[string]any{"results": []string{fmt.Sprintf("Result for: %s", params.Query)}}, nil + }, +) + +clientA := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"}) +sessionA, _ := clientA.CreateSession(ctx, &copilot.SessionConfig{ + SessionID: "shared-task-123", + Tools: []copilot.Tool{searchDocs}, + OnPermissionRequest: copilot.ApproveAll, +}) + +// Client B: provides test capabilities (different process) +runTests := copilot.DefineTool( + "run_tests", + "Run the test suite", + func(params struct { + Path string `json:"path" jsonschema:"Test path"` + }, inv copilot.ToolInvocation) (map[string]any, error) { + return map[string]any{"passed": true, "path": params.Path}, nil + }, +) + +clientB := copilot.NewClient(&copilot.ClientOptions{CLIUrl: "localhost:3000"}) +sessionB, _ := clientB.ResumeSession(ctx, "shared-task-123", &copilot.ResumeSessionConfig{ + Tools: []copilot.Tool{runTests}, + OnPermissionRequest: copilot.ApproveAll, +}) +``` + +### C# (.NET) + + +```csharp +using GitHub.Copilot.SDK; +using Microsoft.Extensions.AI; +using System.ComponentModel; + +// Client A: provides search capabilities +var searchDocs = AIFunctionFactory.Create( + ([Description("Search query")] string query) => + new { results = new[] { $"Result for: {query}" } }, + "search_docs", + "Search the documentation" +); + +var clientA = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" }); +var sessionA = await clientA.CreateSessionAsync(new SessionConfig +{ + SessionId = "shared-task-123", + Tools = [searchDocs], + OnPermissionRequest = PermissionHandler.ApproveAll, +}); + +// Client B: provides test capabilities (different process) +var runTests = AIFunctionFactory.Create( + ([Description("Test path")] string path) => + new { passed = true, path }, + "run_tests", + "Run the test suite" +); + +var clientB = new CopilotClient(new CopilotClientOptions { CliUrl = "localhost:3000" }); +var sessionB = await clientB.ResumeSessionAsync("shared-task-123", new ResumeSessionConfig +{ + Tools = [runTests], + OnPermissionRequest = PermissionHandler.ApproveAll, +}); +``` + +### Best Practices for Multi-Client Sessions + +| Practice | Description | +|----------|-------------| +| **Distribute tools uniquely** | Each tool should be registered on exactly one client. If multiple clients register the same tool, only one response will be used. | +| **Always provide `onPermissionRequest`** | Each client that might receive permission broadcasts should have a handler. | +| **Use meaningful session IDs** | Shared sessions need predictable IDs so all clients can find them. | +| **Handle disconnections gracefully** | If a client disconnects, its tools become unavailable. Design your system so remaining clients can still operate. | + ## Summary | Feature | How to Use | |---------|------------| | **Create resumable session** | Provide your own `sessionId` | | **Resume session** | `client.resumeSession(sessionId)` | +| **Multi-client sharing** | Multiple clients connect via `cliUrl`, each registers its own tools | | **BYOK resume** | Re-provide `provider` config | | **List sessions** | `client.listSessions(filter?)` | | **Disconnect from active session** | `session.disconnect()` — releases in-memory resources; session data on disk is preserved for resumption | @@ -554,6 +756,6 @@ await withSessionLock("user-123-task-456", async () => { ## Next Steps +- [Compatibility Guide](../compatibility.md) - SDK vs CLI feature comparison, protocol v3 details - [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 diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index a340cd63..a0330003 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -67,6 +67,8 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable private readonly Dictionary>> _typedLifecycleHandlers = []; private readonly object _lifecycleHandlersLock = new(); private ServerRpc? _rpc; + private JsonRpc? _jsonRpc; + private int _negotiatedProtocolVersion; /// /// Gets the typed RPC client for server-scoped methods (no session required). @@ -318,6 +320,8 @@ private async Task CleanupConnectionAsync(List? errors) // Clear RPC and models cache _rpc = null; + _jsonRpc = null; + _negotiatedProtocolVersion = 0; _modelsCache = null; if (ctx.NetworkStream is not null) @@ -915,27 +919,31 @@ private Task EnsureConnectedAsync(CancellationToken cancellationToke return (Task)StartAsync(cancellationToken); } - private static async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) + private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken) { - var expectedVersion = SdkProtocolVersion.GetVersion(); + var maxVersion = SdkProtocolVersion.GetVersion(); + var minVersion = SdkProtocolVersion.GetMinVersion(); var pingResponse = await InvokeRpcAsync( connection.Rpc, "ping", [new PingRequest()], connection.StderrBuffer, cancellationToken); if (!pingResponse.ProtocolVersion.HasValue) { throw new InvalidOperationException( - $"SDK protocol version mismatch: SDK expects version {expectedVersion}, " + + $"SDK protocol version mismatch: SDK supports versions {minVersion}-{maxVersion}, " + $"but server does not report a protocol version. " + $"Please update your server to ensure compatibility."); } - if (pingResponse.ProtocolVersion.Value != expectedVersion) + var serverVersion = pingResponse.ProtocolVersion.Value; + if (serverVersion < minVersion || serverVersion > maxVersion) { throw new InvalidOperationException( - $"SDK protocol version mismatch: SDK expects version {expectedVersion}, " + - $"but server reports version {pingResponse.ProtocolVersion.Value}. " + + $"SDK protocol version mismatch: SDK supports versions {minVersion}-{maxVersion}, " + + $"but server reports version {serverVersion}. " + $"Please update your SDK or server to ensure compatibility."); } + + _negotiatedProtocolVersion = serverVersion; } private static async Task<(Process Process, int? DetectedLocalhostTcpPort, StringBuilder StderrBuffer)> StartCliServerAsync(CopilotClientOptions options, ILogger logger, CancellationToken cancellationToken) @@ -1135,6 +1143,7 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke); rpc.StartListening(); + _jsonRpc = rpc; _rpc = new ServerRpc(rpc); return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer); @@ -1173,6 +1182,145 @@ private static JsonSerializerOptions CreateSerializerOptions() return _sessions.TryGetValue(sessionId, out var session) ? session : null; } + private async Task HandleExternalToolRequestedAsync(string sessionId, JsonElement eventJson) + { + string? requestId = null; + try + { + if (!eventJson.TryGetProperty("data", out var data)) return; + + requestId = data.TryGetProperty("requestId", out var rid) ? rid.GetString() : null; + var toolCallId = data.TryGetProperty("toolCallId", out var tcid) ? tcid.GetString() : null; + var toolName = data.TryGetProperty("toolName", out var tn) ? tn.GetString() : null; + + if (requestId == null || toolName == null) return; + + var session = GetSession(sessionId); + if (session == null) return; + + ToolResultObject resultObj; + if (session.GetTool(toolName) is not { } tool) + { + resultObj = new ToolResultObject + { + TextResultForLlm = $"Tool '{toolName}' is not supported.", + ResultType = "failure", + Error = $"tool '{toolName}' not supported" + }; + } + else + { + try + { + var arguments = data.TryGetProperty("arguments", out var args) ? (object?)args : null; + + var invocation = new ToolInvocation + { + SessionId = sessionId, + ToolCallId = toolCallId ?? "", + ToolName = toolName, + Arguments = arguments + }; + + var aiFunctionArgs = new AIFunctionArguments + { + Context = new Dictionary + { + [typeof(ToolInvocation)] = invocation + } + }; + + if (arguments is JsonElement { ValueKind: JsonValueKind.Object } incomingJsonArgs) + { + foreach (var prop in incomingJsonArgs.EnumerateObject()) + { + aiFunctionArgs[prop.Name] = prop.Value; + } + } + + var result = await tool.InvokeAsync(aiFunctionArgs); + + resultObj = result is ToolResultAIContent trac ? trac.Result : new ToolResultObject + { + ResultType = "success", + TextResultForLlm = result is JsonElement { ValueKind: JsonValueKind.String } je + ? je.GetString()! + : JsonSerializer.Serialize(result, tool.JsonSerializerOptions.GetTypeInfo(typeof(object))), + }; + } + catch (Exception ex) + { + resultObj = new ToolResultObject + { + TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available.", + ResultType = "failure", + Error = ex.Message + }; + } + } + + if (_jsonRpc is { } rpc) + { + await InvokeRpcAsync(rpc, "session.tools.handlePendingToolCall", + [new HandlePendingToolCallRequest(sessionId, requestId, Result: resultObj)], CancellationToken.None); + } + } + catch (Exception) + { + try + { + if (_jsonRpc is { } rpc && requestId != null) + { + await InvokeRpcAsync(rpc, "session.tools.handlePendingToolCall", + [new HandlePendingToolCallRequest(sessionId, requestId, Error: "Internal error handling tool call")], + CancellationToken.None); + } + } + catch { /* Connection may be closed */ } + } + } + + private async Task HandlePermissionRequestedEventAsync(string sessionId, JsonElement eventJson) + { + string? requestId = null; + try + { + if (!eventJson.TryGetProperty("data", out var data)) return; + + requestId = data.TryGetProperty("requestId", out var rid) ? rid.GetString() : null; + if (requestId == null) return; + + var session = GetSession(sessionId); + if (session == null) return; + + if (!data.TryGetProperty("permissionRequest", out var permissionRequest)) return; + + var result = await session.HandlePermissionRequestAsync(permissionRequest); + + if (_jsonRpc is { } rpc) + { + await InvokeRpcAsync(rpc, "session.permissions.handlePendingPermissionRequest", + [new HandlePendingPermissionRequestRequest(sessionId, requestId, result)], CancellationToken.None); + } + } + catch (Exception) + { + try + { + if (_jsonRpc is { } rpc && requestId != null) + { + var deniedResult = new PermissionRequestResult + { + Kind = PermissionRequestResultKind.DeniedCouldNotRequestFromUser + }; + await InvokeRpcAsync(rpc, "session.permissions.handlePendingPermissionRequest", + [new HandlePendingPermissionRequestRequest(sessionId, requestId, deniedResult)], CancellationToken.None); + } + } + catch { /* Connection may be closed */ } + } + } + /// /// Disposes the synchronously. /// @@ -1202,8 +1350,28 @@ private class RpcHandler(CopilotClient client) { public void OnSessionEvent(string sessionId, JsonElement? @event) { + if (@event == null) return; + + // Extract event type for v3 broadcast handling + string? eventType = null; + if (@event.Value.TryGetProperty("type", out var typeProp)) + { + eventType = typeProp.GetString(); + } + + // external_tool.requested is not in the typed schema; intercept on v3, always skip deserialization + if (eventType == "external_tool.requested") + { + if (client._negotiatedProtocolVersion >= 3) + { + _ = Task.Run(() => client.HandleExternalToolRequestedAsync(sessionId, @event.Value)); + } + return; + } + + // Normal typed event dispatch var session = client.GetSession(sessionId); - if (session != null && @event != null) + if (session != null) { var evt = SessionEvent.FromJson(@event.Value.GetRawText()); if (evt != null) @@ -1211,6 +1379,12 @@ public void OnSessionEvent(string sessionId, JsonElement? @event) session.DispatchEvent(evt); } } + + // v3: permission.requested - handle via RPC callback in addition to event dispatch + if (client._negotiatedProtocolVersion >= 3 && eventType == "permission.requested") + { + _ = Task.Run(() => client.HandlePermissionRequestedEventAsync(sessionId, @event.Value)); + } } public void OnSessionLifecycle(string type, string sessionId, JsonElement? metadata) @@ -1486,6 +1660,17 @@ internal record UserInputRequestResponse( internal record HooksInvokeResponse( object? Output); + internal record HandlePendingToolCallRequest( + string SessionId, + string RequestId, + ToolResultObject? Result = null, + string? Error = null); + + internal record HandlePendingPermissionRequestRequest( + string SessionId, + string RequestId, + PermissionRequestResult Result); + /// Trace source that forwards all logs to the ILogger. internal sealed class LoggerTraceSource : TraceSource { @@ -1575,6 +1760,8 @@ private static LogLevel MapLevel(TraceEventType eventType) [JsonSerializable(typeof(DeleteSessionRequest))] [JsonSerializable(typeof(DeleteSessionResponse))] [JsonSerializable(typeof(GetLastSessionIdResponse))] + [JsonSerializable(typeof(HandlePendingPermissionRequestRequest))] + [JsonSerializable(typeof(HandlePendingToolCallRequest))] [JsonSerializable(typeof(HooksInvokeResponse))] [JsonSerializable(typeof(ListSessionsRequest))] [JsonSerializable(typeof(ListSessionsResponse))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 4c4bac0f..a5dc3409 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -250,13 +250,17 @@ internal class SessionModeSetRequest public class SessionPlanReadResult { - /// Whether plan.md exists in the workspace + /// Whether the plan file exists in the workspace [JsonPropertyName("exists")] public bool Exists { get; set; } - /// The content of plan.md, 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 + [JsonPropertyName("path")] + public string? Path { get; set; } } internal class SessionPlanReadRequest @@ -468,6 +472,45 @@ internal class SessionCompactionCompactRequest public string SessionId { get; set; } = string.Empty; } +public class SessionToolsHandlePendingToolCallResult +{ + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +internal class SessionToolsHandlePendingToolCallRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + [JsonPropertyName("result")] + public object? Result { get; set; } + + [JsonPropertyName("error")] + public string? Error { get; set; } +} + +public class SessionPermissionsHandlePendingPermissionRequestResult +{ + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +internal class SessionPermissionsHandlePendingPermissionRequestRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + [JsonPropertyName("result")] + public object Result { get; set; } = null!; +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode { @@ -572,36 +615,42 @@ internal SessionRpc(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; - Model = new ModelApi(rpc, sessionId); - Mode = new ModeApi(rpc, sessionId); - Plan = new PlanApi(rpc, sessionId); - Workspace = new WorkspaceApi(rpc, sessionId); - Fleet = new FleetApi(rpc, sessionId); - Agent = new AgentApi(rpc, sessionId); - Compaction = new CompactionApi(rpc, sessionId); + Model = new SessionModelApi(rpc, sessionId); + Mode = new SessionModeApi(rpc, sessionId); + Plan = new SessionPlanApi(rpc, sessionId); + Workspace = new SessionWorkspaceApi(rpc, sessionId); + Fleet = new SessionFleetApi(rpc, sessionId); + Agent = new SessionAgentApi(rpc, sessionId); + Compaction = new SessionCompactionApi(rpc, sessionId); + Tools = new SessionToolsApi(rpc, sessionId); + Permissions = new SessionPermissionsApi(rpc, sessionId); } - public ModelApi Model { get; } + public SessionModelApi Model { get; } + + public SessionModeApi Mode { get; } - public ModeApi Mode { get; } + public SessionPlanApi Plan { get; } - public PlanApi Plan { get; } + public SessionWorkspaceApi Workspace { get; } - public WorkspaceApi Workspace { get; } + public SessionFleetApi Fleet { get; } - public FleetApi Fleet { get; } + public SessionAgentApi Agent { get; } - public AgentApi Agent { get; } + public SessionCompactionApi Compaction { get; } - public CompactionApi Compaction { get; } + public SessionToolsApi Tools { get; } + + public SessionPermissionsApi Permissions { get; } } -public class ModelApi +public class SessionModelApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal ModelApi(JsonRpc rpc, string sessionId) + internal SessionModelApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -622,12 +671,12 @@ public async Task SwitchToAsync(string modelId, Canc } } -public class ModeApi +public class SessionModeApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal ModeApi(JsonRpc rpc, string sessionId) + internal SessionModeApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -648,12 +697,12 @@ public async Task SetAsync(SessionModeGetResultMode mode, } } -public class PlanApi +public class SessionPlanApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal PlanApi(JsonRpc rpc, string sessionId) + internal SessionPlanApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -681,12 +730,12 @@ public async Task DeleteAsync(CancellationToken cancell } } -public class WorkspaceApi +public class SessionWorkspaceApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal WorkspaceApi(JsonRpc rpc, string sessionId) + internal SessionWorkspaceApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -714,12 +763,12 @@ public async Task CreateFileAsync(string path, } } -public class FleetApi +public class SessionFleetApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal FleetApi(JsonRpc rpc, string sessionId) + internal SessionFleetApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -733,12 +782,12 @@ public async Task StartAsync(string? prompt, Cancellati } } -public class AgentApi +public class SessionAgentApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal AgentApi(JsonRpc rpc, string sessionId) + internal SessionAgentApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -773,12 +822,12 @@ public async Task DeselectAsync(CancellationToken ca } } -public class CompactionApi +public class SessionCompactionApi { private readonly JsonRpc _rpc; private readonly string _sessionId; - internal CompactionApi(JsonRpc rpc, string sessionId) + internal SessionCompactionApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; @@ -792,6 +841,44 @@ public async Task CompactAsync(CancellationToken } } +public class SessionToolsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal SessionToolsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.tools.handlePendingToolCall". + public async Task HandlePendingToolCallAsync(string requestId, object? result, string? error, 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); + } +} + +public class SessionPermissionsApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal SessionPermissionsApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.permissions.handlePendingPermissionRequest". + public async Task HandlePendingPermissionRequestAsync(string requestId, object result, CancellationToken cancellationToken = default) + { + var request = new SessionPermissionsHandlePendingPermissionRequestRequest { SessionId = _sessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.permissions.handlePendingPermissionRequest", [request], cancellationToken); + } +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -830,12 +917,16 @@ public async Task CompactAsync(CancellationToken [JsonSerializable(typeof(SessionModelGetCurrentResult))] [JsonSerializable(typeof(SessionModelSwitchToRequest))] [JsonSerializable(typeof(SessionModelSwitchToResult))] +[JsonSerializable(typeof(SessionPermissionsHandlePendingPermissionRequestRequest))] +[JsonSerializable(typeof(SessionPermissionsHandlePendingPermissionRequestResult))] [JsonSerializable(typeof(SessionPlanDeleteRequest))] [JsonSerializable(typeof(SessionPlanDeleteResult))] [JsonSerializable(typeof(SessionPlanReadRequest))] [JsonSerializable(typeof(SessionPlanReadResult))] [JsonSerializable(typeof(SessionPlanUpdateRequest))] [JsonSerializable(typeof(SessionPlanUpdateResult))] +[JsonSerializable(typeof(SessionToolsHandlePendingToolCallRequest))] +[JsonSerializable(typeof(SessionToolsHandlePendingToolCallResult))] [JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))] [JsonSerializable(typeof(SessionWorkspaceCreateFileResult))] [JsonSerializable(typeof(SessionWorkspaceListFilesRequest))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 73e8d67b..c497038c 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -29,8 +29,14 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(AssistantTurnEndEvent), "assistant.turn_end")] [JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")] [JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")] +[JsonDerivedType(typeof(CommandCompletedEvent), "command.completed")] +[JsonDerivedType(typeof(CommandQueuedEvent), "command.queued")] [JsonDerivedType(typeof(ElicitationCompletedEvent), "elicitation.completed")] [JsonDerivedType(typeof(ElicitationRequestedEvent), "elicitation.requested")] +[JsonDerivedType(typeof(ExitPlanModeCompletedEvent), "exit_plan_mode.completed")] +[JsonDerivedType(typeof(ExitPlanModeRequestedEvent), "exit_plan_mode.requested")] +[JsonDerivedType(typeof(ExternalToolCompletedEvent), "external_tool.completed")] +[JsonDerivedType(typeof(ExternalToolRequestedEvent), "external_tool.requested")] [JsonDerivedType(typeof(HookEndEvent), "hook.end")] [JsonDerivedType(typeof(HookStartEvent), "hook.start")] [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] @@ -723,6 +729,78 @@ public partial class ElicitationCompletedEvent : SessionEvent public required ElicitationCompletedData Data { get; set; } } +/// +/// Event: external_tool.requested +/// +public partial class ExternalToolRequestedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "external_tool.requested"; + + [JsonPropertyName("data")] + public required ExternalToolRequestedData Data { get; set; } +} + +/// +/// Event: external_tool.completed +/// +public partial class ExternalToolCompletedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "external_tool.completed"; + + [JsonPropertyName("data")] + public required ExternalToolCompletedData Data { get; set; } +} + +/// +/// Event: command.queued +/// +public partial class CommandQueuedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "command.queued"; + + [JsonPropertyName("data")] + public required CommandQueuedData Data { get; set; } +} + +/// +/// Event: command.completed +/// +public partial class CommandCompletedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "command.completed"; + + [JsonPropertyName("data")] + public required CommandCompletedData Data { get; set; } +} + +/// +/// Event: exit_plan_mode.requested +/// +public partial class ExitPlanModeRequestedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "exit_plan_mode.requested"; + + [JsonPropertyName("data")] + public required ExitPlanModeRequestedData Data { get; set; } +} + +/// +/// Event: exit_plan_mode.completed +/// +public partial class ExitPlanModeCompletedEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "exit_plan_mode.completed"; + + [JsonPropertyName("data")] + public required ExitPlanModeCompletedData Data { get; set; } +} + public partial class SessionStartData { [JsonPropertyName("sessionId")] @@ -785,6 +863,9 @@ public partial class SessionErrorData public partial class SessionIdleData { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("backgroundTasks")] + public SessionIdleDataBackgroundTasks? BackgroundTasks { get; set; } } public partial class SessionTitleChangedData @@ -1124,6 +1205,10 @@ public partial class AssistantMessageData [JsonPropertyName("phase")] public string? Phase { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputTokens")] + public double? OutputTokens { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] public string? InteractionId { get; set; } @@ -1450,6 +1535,9 @@ public partial class PermissionCompletedData { [JsonPropertyName("requestId")] public required string RequestId { get; set; } + + [JsonPropertyName("result")] + public required PermissionCompletedDataResult Result { get; set; } } public partial class UserInputRequestedData @@ -1497,6 +1585,70 @@ public partial class ElicitationCompletedData public required string RequestId { get; set; } } +public partial class ExternalToolRequestedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } + + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arguments")] + public object? Arguments { get; set; } +} + +public partial class ExternalToolCompletedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + +public partial class CommandQueuedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + [JsonPropertyName("command")] + public required string Command { get; set; } +} + +public partial class CommandCompletedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + +public partial class ExitPlanModeRequestedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + [JsonPropertyName("summary")] + public required string Summary { get; set; } + + [JsonPropertyName("planContent")] + public required string PlanContent { get; set; } + + [JsonPropertyName("actions")] + public required string[] Actions { get; set; } + + [JsonPropertyName("recommendedAction")] + public required string RecommendedAction { get; set; } +} + +public partial class ExitPlanModeCompletedData +{ + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + public partial class SessionStartDataContext { [JsonPropertyName("cwd")] @@ -1533,6 +1685,38 @@ public partial class SessionResumeDataContext public string? Branch { get; set; } } +public partial class SessionIdleDataBackgroundTasksAgentsItem +{ + [JsonPropertyName("agentId")] + public required string AgentId { get; set; } + + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public partial class SessionIdleDataBackgroundTasksShellsItem +{ + [JsonPropertyName("shellId")] + public required string ShellId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } +} + +public partial class SessionIdleDataBackgroundTasks +{ + [JsonPropertyName("agents")] + public required SessionIdleDataBackgroundTasksAgentsItem[] Agents { get; set; } + + [JsonPropertyName("shells")] + public required SessionIdleDataBackgroundTasksShellsItem[] Shells { get; set; } +} + public partial class SessionHandoffDataRepository { [JsonPropertyName("owner")] @@ -1911,6 +2095,12 @@ public partial class SystemMessageDataMetadata public Dictionary? Variables { get; set; } } +public partial class PermissionCompletedDataResult +{ + [JsonPropertyName("kind")] + public required PermissionCompletedDataResultKind Kind { get; set; } +} + public partial class ElicitationRequestedDataRequestedSchema { [JsonPropertyName("type")] @@ -2013,6 +2203,21 @@ public enum SystemMessageDataRole Developer, } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum PermissionCompletedDataResultKind +{ + [JsonStringEnumMemberName("approved")] + Approved, + [JsonStringEnumMemberName("denied-by-rules")] + DeniedByRules, + [JsonStringEnumMemberName("denied-no-approval-rule-and-could-not-request-from-user")] + DeniedNoApprovalRuleAndCouldNotRequestFromUser, + [JsonStringEnumMemberName("denied-interactively-by-user")] + DeniedInteractivelyByUser, + [JsonStringEnumMemberName("denied-by-content-exclusion-policy")] + DeniedByContentExclusionPolicy, +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -2041,11 +2246,23 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(AssistantUsageDataCopilotUsage))] [JsonSerializable(typeof(AssistantUsageDataCopilotUsageTokenDetailsItem))] [JsonSerializable(typeof(AssistantUsageEvent))] +[JsonSerializable(typeof(CommandCompletedData))] +[JsonSerializable(typeof(CommandCompletedEvent))] +[JsonSerializable(typeof(CommandQueuedData))] +[JsonSerializable(typeof(CommandQueuedEvent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] [JsonSerializable(typeof(ElicitationRequestedData))] [JsonSerializable(typeof(ElicitationRequestedDataRequestedSchema))] [JsonSerializable(typeof(ElicitationRequestedEvent))] +[JsonSerializable(typeof(ExitPlanModeCompletedData))] +[JsonSerializable(typeof(ExitPlanModeCompletedEvent))] +[JsonSerializable(typeof(ExitPlanModeRequestedData))] +[JsonSerializable(typeof(ExitPlanModeRequestedEvent))] +[JsonSerializable(typeof(ExternalToolCompletedData))] +[JsonSerializable(typeof(ExternalToolCompletedEvent))] +[JsonSerializable(typeof(ExternalToolRequestedData))] +[JsonSerializable(typeof(ExternalToolRequestedEvent))] [JsonSerializable(typeof(HookEndData))] [JsonSerializable(typeof(HookEndDataError))] [JsonSerializable(typeof(HookEndEvent))] @@ -2054,6 +2271,7 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(PendingMessagesModifiedData))] [JsonSerializable(typeof(PendingMessagesModifiedEvent))] [JsonSerializable(typeof(PermissionCompletedData))] +[JsonSerializable(typeof(PermissionCompletedDataResult))] [JsonSerializable(typeof(PermissionCompletedEvent))] [JsonSerializable(typeof(PermissionRequestedData))] [JsonSerializable(typeof(PermissionRequestedEvent))] @@ -2071,6 +2289,9 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(SessionHandoffDataRepository))] [JsonSerializable(typeof(SessionHandoffEvent))] [JsonSerializable(typeof(SessionIdleData))] +[JsonSerializable(typeof(SessionIdleDataBackgroundTasks))] +[JsonSerializable(typeof(SessionIdleDataBackgroundTasksAgentsItem))] +[JsonSerializable(typeof(SessionIdleDataBackgroundTasksShellsItem))] [JsonSerializable(typeof(SessionIdleEvent))] [JsonSerializable(typeof(SessionInfoData))] [JsonSerializable(typeof(SessionInfoEvent))] diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs index b4c2a367..a460201b 100644 --- a/dotnet/src/SdkProtocolVersion.cs +++ b/dotnet/src/SdkProtocolVersion.cs @@ -9,15 +9,23 @@ namespace GitHub.Copilot.SDK; internal static class SdkProtocolVersion { /// - /// The SDK protocol version. + /// The maximum SDK protocol version supported. /// - private const int Version = 2; + private const int Version = 3; /// - /// Gets the SDK protocol version. + /// The minimum SDK protocol version supported. + /// Servers reporting a version in [Min, Max] are considered compatible. /// - public static int GetVersion() - { - return Version; - } + private const int MinVersion = 2; + + /// + /// Gets the SDK protocol version (maximum supported). + /// + public static int GetVersion() => Version; + + /// + /// Gets the minimum SDK protocol version supported. + /// + public static int GetMinVersion() => MinVersion; } diff --git a/go/client.go b/go/client.go index 2801ef12..e044dfed 100644 --- a/go/client.go +++ b/go/client.go @@ -69,28 +69,29 @@ import ( // } // 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 - modelsCache []ModelInfo - modelsCacheMux sync.Mutex - lifecycleHandlers []SessionLifecycleHandler - typedLifecycleHandlers map[SessionLifecycleEventType][]SessionLifecycleHandler - lifecycleHandlersMux sync.Mutex - startStopMux sync.RWMutex // protects process and state during start/[force]stop - processDone chan struct{} - processErrorPtr *error - osProcess atomic.Pointer[os.Process] + options ClientOptions + process *exec.Cmd + client *jsonrpc2.Client + actualPort int + actualHost string + state ConnectionState + sessions map[string]*Session + sessionsMux sync.Mutex + isExternalServer bool + negotiatedProtocolVersion int + 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 + modelsCache []ModelInfo + modelsCacheMux sync.Mutex + lifecycleHandlers []SessionLifecycleHandler + typedLifecycleHandlers map[SessionLifecycleEventType][]SessionLifecycleHandler + lifecycleHandlersMux sync.Mutex + startStopMux sync.RWMutex // protects process and state during start/[force]stop + processDone chan struct{} + processErrorPtr *error + osProcess atomic.Pointer[os.Process] // RPC provides typed server-scoped RPC methods. // This field is nil until the client is connected via Start(). @@ -1062,22 +1063,25 @@ func (c *Client) ListModels(ctx context.Context) ([]ModelInfo, error) { return models, nil } -// verifyProtocolVersion verifies that the server's protocol version matches the SDK's expected version +// verifyProtocolVersion verifies that the server's protocol version is within the SDK's supported range func (c *Client) verifyProtocolVersion(ctx context.Context) error { - expectedVersion := GetSdkProtocolVersion() + maxVersion := GetSdkProtocolVersion() + minVersion := GetMinSdkProtocolVersion() pingResult, err := c.Ping(ctx, "") if err != nil { return err } if pingResult.ProtocolVersion == nil { - return fmt.Errorf("SDK protocol version mismatch: SDK expects version %d, but server does not report a protocol version. Please update your server to ensure compatibility", expectedVersion) + return fmt.Errorf("SDK protocol version mismatch: SDK supports versions %d-%d, but server does not report a protocol version. Please update your server to ensure compatibility", minVersion, maxVersion) } - if *pingResult.ProtocolVersion != expectedVersion { - return fmt.Errorf("SDK protocol version mismatch: SDK expects version %d, but server reports version %d. Please update your SDK or server to ensure compatibility", expectedVersion, *pingResult.ProtocolVersion) + serverVersion := *pingResult.ProtocolVersion + if serverVersion < minVersion || serverVersion > maxVersion { + return fmt.Errorf("SDK protocol version mismatch: SDK supports versions %d-%d, but server reports version %d. Please update your SDK or server to ensure compatibility", minVersion, maxVersion, serverVersion) } + c.negotiatedProtocolVersion = serverVersion return nil } @@ -1311,6 +1315,11 @@ func (c *Client) handleSessionEvent(req sessionEventRequest) { if ok { session.dispatchEvent(req.Event) } + + // Protocol v3: handle broadcast tool/permission events + if c.negotiatedProtocolVersion >= 3 { + c.handleBroadcastEvent(req.SessionID, req.Event) + } } // handleToolCallRequest handles a tool call request from the CLI server. @@ -1460,3 +1469,118 @@ func buildUnsupportedToolResult(toolName string) ToolResult { ToolTelemetry: map[string]any{}, } } + +// handleBroadcastEvent dispatches v3 broadcast events (external_tool.requested, permission.requested). +func (c *Client) handleBroadcastEvent(sessionID string, event SessionEvent) { + switch event.Type { + case "external_tool.requested": + go c.handleExternalToolRequestedEvent(sessionID, event) + case "permission.requested": + go c.handlePermissionRequestedEvent(sessionID, event) + } +} + +// handleExternalToolRequestedEvent handles v3 external_tool.requested broadcast events. +func (c *Client) handleExternalToolRequestedEvent(sessionID string, event SessionEvent) { + data := event.Data + if data.RequestID == nil || data.ToolName == nil { + return + } + requestID := *data.RequestID + toolName := *data.ToolName + toolCallID := "" + if data.ToolCallID != nil { + toolCallID = *data.ToolCallID + } + + c.sessionsMux.Lock() + session, ok := c.sessions[sessionID] + c.sessionsMux.Unlock() + if !ok { + return + } + + handler, found := session.getToolHandler(toolName) + + var result ToolResult + if !found { + result = buildUnsupportedToolResult(toolName) + } else { + result = c.executeToolCall(sessionID, toolCallID, toolName, data.Arguments, handler) + } + + type handlePendingToolCallParams struct { + SessionID string `json:"sessionId"` + RequestID string `json:"requestId"` + Result ToolResult `json:"result"` + } + _, err := c.client.Request("session.tools.handlePendingToolCall", handlePendingToolCallParams{ + SessionID: sessionID, + RequestID: requestID, + Result: result, + }) + if err != nil { + // Send error response as a fallback + type handlePendingToolCallErrorParams struct { + SessionID string `json:"sessionId"` + RequestID string `json:"requestId"` + Error string `json:"error"` + } + _, _ = c.client.Request("session.tools.handlePendingToolCall", handlePendingToolCallErrorParams{ + SessionID: sessionID, + RequestID: requestID, + Error: err.Error(), + }) + } +} + +// handlePermissionRequestedEvent handles v3 permission.requested broadcast events. +func (c *Client) handlePermissionRequestedEvent(sessionID string, event SessionEvent) { + data := event.Data + if data.RequestID == nil || data.PermissionRequest == nil { + return + } + requestID := *data.RequestID + + c.sessionsMux.Lock() + session, ok := c.sessions[sessionID] + c.sessionsMux.Unlock() + if !ok { + return + } + + type handlePendingPermissionParams struct { + SessionID string `json:"sessionId"` + RequestID string `json:"requestId"` + Result PermissionRequestResult `json:"result"` + } + + result, err := session.handlePermissionRequest(*data.PermissionRequest) + if err != nil { + // Send denial on error + _, _ = c.client.Request("session.permissions.handlePendingPermissionRequest", handlePendingPermissionParams{ + SessionID: sessionID, + RequestID: requestID, + Result: PermissionRequestResult{ + Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, + }, + }) + return + } + + _, rpcErr := c.client.Request("session.permissions.handlePendingPermissionRequest", handlePendingPermissionParams{ + SessionID: sessionID, + RequestID: requestID, + Result: result, + }) + if rpcErr != nil { + // Send denial as fallback + _, _ = c.client.Request("session.permissions.handlePendingPermissionRequest", handlePendingPermissionParams{ + SessionID: sessionID, + RequestID: requestID, + Result: PermissionRequestResult{ + Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, + }, + }) + } +} diff --git a/go/generated_session_events.go b/go/generated_session_events.go index dba38d1e..86f5066f 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -26,337 +26,813 @@ func (r *SessionEvent) Marshal() ([]byte, error) { } type SessionEvent struct { - Data Data `json:"data"` - Ephemeral *bool `json:"ephemeral,omitempty"` - ID string `json:"id"` - ParentID *string `json:"parentId"` + // Payload indicating the agent is idle; includes any background tasks still in flight + // + // Empty payload; the event signals that LLM-powered conversation compaction has begun + // + // Empty payload; the event signals that the pending message queue has changed + // + // Empty payload; the event signals that the custom agent was deselected, returning to the + // default agent + 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"` + // Unique event identifier (UUID v4), generated when the event is emitted + ID string `json:"id"` + // ID of the chronologically preceding event in the session, forming a linked chain. Null + // for the first event. + ParentID *string `json:"parentId"` + // ISO 8601 timestamp when the event was created Timestamp time.Time `json:"timestamp"` Type SessionEventType `json:"type"` } +// Payload indicating the agent is idle; includes any background tasks still in flight +// +// Empty payload; the event signals that LLM-powered conversation compaction has begun +// +// Empty payload; the event signals that the pending message queue has changed +// +// Empty payload; the event signals that the custom agent was deselected, returning to the +// default agent type Data struct { - Context *ContextUnion `json:"context"` - CopilotVersion *string `json:"copilotVersion,omitempty"` - Producer *string `json:"producer,omitempty"` - SelectedModel *string `json:"selectedModel,omitempty"` - SessionID *string `json:"sessionId,omitempty"` - StartTime *time.Time `json:"startTime,omitempty"` - Version *float64 `json:"version,omitempty"` - EventCount *float64 `json:"eventCount,omitempty"` - ResumeTime *time.Time `json:"resumeTime,omitempty"` - ErrorType *string `json:"errorType,omitempty"` - Message *string `json:"message,omitempty"` - ProviderCallID *string `json:"providerCallId,omitempty"` - Stack *string `json:"stack,omitempty"` - StatusCode *int64 `json:"statusCode,omitempty"` - Title *string `json:"title,omitempty"` - InfoType *string `json:"infoType,omitempty"` - WarningType *string `json:"warningType,omitempty"` - NewModel *string `json:"newModel,omitempty"` - PreviousModel *string `json:"previousModel,omitempty"` - NewMode *string `json:"newMode,omitempty"` - PreviousMode *string `json:"previousMode,omitempty"` - Operation *Operation `json:"operation,omitempty"` - // Relative path within the workspace files directory - Path *string `json:"path,omitempty"` - HandoffTime *time.Time `json:"handoffTime,omitempty"` - RemoteSessionID *string `json:"remoteSessionId,omitempty"` - Repository *RepositoryUnion `json:"repository"` - SourceType *SourceType `json:"sourceType,omitempty"` - Summary *string `json:"summary,omitempty"` - MessagesRemovedDuringTruncation *float64 `json:"messagesRemovedDuringTruncation,omitempty"` - PerformedBy *string `json:"performedBy,omitempty"` - PostTruncationMessagesLength *float64 `json:"postTruncationMessagesLength,omitempty"` - PostTruncationTokensInMessages *float64 `json:"postTruncationTokensInMessages,omitempty"` - PreTruncationMessagesLength *float64 `json:"preTruncationMessagesLength,omitempty"` - PreTruncationTokensInMessages *float64 `json:"preTruncationTokensInMessages,omitempty"` - TokenLimit *float64 `json:"tokenLimit,omitempty"` - TokensRemovedDuringTruncation *float64 `json:"tokensRemovedDuringTruncation,omitempty"` - EventsRemoved *float64 `json:"eventsRemoved,omitempty"` - UpToEventID *string `json:"upToEventId,omitempty"` - CodeChanges *CodeChanges `json:"codeChanges,omitempty"` - CurrentModel *string `json:"currentModel,omitempty"` - ErrorReason *string `json:"errorReason,omitempty"` - ModelMetrics map[string]ModelMetric `json:"modelMetrics,omitempty"` - SessionStartTime *float64 `json:"sessionStartTime,omitempty"` - ShutdownType *ShutdownType `json:"shutdownType,omitempty"` - TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"` - TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` - Branch *string `json:"branch,omitempty"` - Cwd *string `json:"cwd,omitempty"` - GitRoot *string `json:"gitRoot,omitempty"` - CurrentTokens *float64 `json:"currentTokens,omitempty"` - MessagesLength *float64 `json:"messagesLength,omitempty"` - CheckpointNumber *float64 `json:"checkpointNumber,omitempty"` - CheckpointPath *string `json:"checkpointPath,omitempty"` - CompactionTokensUsed *CompactionTokensUsed `json:"compactionTokensUsed,omitempty"` - Error *ErrorUnion `json:"error"` - MessagesRemoved *float64 `json:"messagesRemoved,omitempty"` - PostCompactionTokens *float64 `json:"postCompactionTokens,omitempty"` - PreCompactionMessagesLength *float64 `json:"preCompactionMessagesLength,omitempty"` - PreCompactionTokens *float64 `json:"preCompactionTokens,omitempty"` - RequestID *string `json:"requestId,omitempty"` - Success *bool `json:"success,omitempty"` - SummaryContent *string `json:"summaryContent,omitempty"` - TokensRemoved *float64 `json:"tokensRemoved,omitempty"` - AgentMode *AgentMode `json:"agentMode,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` - Content *string `json:"content,omitempty"` - InteractionID *string `json:"interactionId,omitempty"` - Source *string `json:"source,omitempty"` - TransformedContent *string `json:"transformedContent,omitempty"` - TurnID *string `json:"turnId,omitempty"` - Intent *string `json:"intent,omitempty"` - ReasoningID *string `json:"reasoningId,omitempty"` - DeltaContent *string `json:"deltaContent,omitempty"` - TotalResponseSizeBytes *float64 `json:"totalResponseSizeBytes,omitempty"` - EncryptedContent *string `json:"encryptedContent,omitempty"` - MessageID *string `json:"messageId,omitempty"` - ParentToolCallID *string `json:"parentToolCallId,omitempty"` - Phase *string `json:"phase,omitempty"` - ReasoningOpaque *string `json:"reasoningOpaque,omitempty"` - ReasoningText *string `json:"reasoningText,omitempty"` - ToolRequests []ToolRequest `json:"toolRequests,omitempty"` - APICallID *string `json:"apiCallId,omitempty"` - CacheReadTokens *float64 `json:"cacheReadTokens,omitempty"` - CacheWriteTokens *float64 `json:"cacheWriteTokens,omitempty"` - CopilotUsage *CopilotUsage `json:"copilotUsage,omitempty"` - Cost *float64 `json:"cost,omitempty"` - Duration *float64 `json:"duration,omitempty"` - Initiator *string `json:"initiator,omitempty"` - InputTokens *float64 `json:"inputTokens,omitempty"` - Model *string `json:"model,omitempty"` - OutputTokens *float64 `json:"outputTokens,omitempty"` - QuotaSnapshots map[string]QuotaSnapshot `json:"quotaSnapshots,omitempty"` - Reason *string `json:"reason,omitempty"` - Arguments interface{} `json:"arguments"` - ToolCallID *string `json:"toolCallId,omitempty"` - ToolName *string `json:"toolName,omitempty"` - MCPServerName *string `json:"mcpServerName,omitempty"` - MCPToolName *string `json:"mcpToolName,omitempty"` - PartialOutput *string `json:"partialOutput,omitempty"` - ProgressMessage *string `json:"progressMessage,omitempty"` - IsUserRequested *bool `json:"isUserRequested,omitempty"` - Result *Result `json:"result,omitempty"` - ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"` - AllowedTools []string `json:"allowedTools,omitempty"` - Name *string `json:"name,omitempty"` - PluginName *string `json:"pluginName,omitempty"` - PluginVersion *string `json:"pluginVersion,omitempty"` - AgentDescription *string `json:"agentDescription,omitempty"` - AgentDisplayName *string `json:"agentDisplayName,omitempty"` - AgentName *string `json:"agentName,omitempty"` - Tools []string `json:"tools"` - HookInvocationID *string `json:"hookInvocationId,omitempty"` - HookType *string `json:"hookType,omitempty"` - Input interface{} `json:"input"` - Output interface{} `json:"output"` - Metadata *Metadata `json:"metadata,omitempty"` - Role *Role `json:"role,omitempty"` - PermissionRequest *PermissionRequest `json:"permissionRequest,omitempty"` - AllowFreeform *bool `json:"allowFreeform,omitempty"` - Choices []string `json:"choices,omitempty"` - Question *string `json:"question,omitempty"` - Mode *Mode `json:"mode,omitempty"` - RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"` + // Working directory and git context at session start + // + // Updated working directory and git context at resume time + // + // Additional context information for the handoff + Context *ContextUnion `json:"context"` + // Version string of the Copilot application + CopilotVersion *string `json:"copilotVersion,omitempty"` + // Identifier of the software producing the events (e.g., "copilot-agent") + Producer *string `json:"producer,omitempty"` + // Model selected at session creation time, if any + SelectedModel *string `json:"selectedModel,omitempty"` + // Unique identifier for the session + // + // Session ID that this external tool request belongs to + SessionID *string `json:"sessionId,omitempty"` + // ISO 8601 timestamp when the session was created + StartTime *time.Time `json:"startTime,omitempty"` + // Schema version number for the session event format + Version *float64 `json:"version,omitempty"` + // Total number of persisted events in the session at the time of resume + EventCount *float64 `json:"eventCount,omitempty"` + // ISO 8601 timestamp when the session was resumed + ResumeTime *time.Time `json:"resumeTime,omitempty"` + // Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", + // "query") + ErrorType *string `json:"errorType,omitempty"` + // Human-readable error message + // + // Human-readable informational message for display in the timeline + // + // Human-readable warning message for display in the timeline + // + // Message describing what information is needed from the user + Message *string `json:"message,omitempty"` + // GitHub request tracing ID (x-github-request-id header) for correlating with server-side + // logs + // + // GitHub request tracing ID (x-github-request-id header) for server-side log correlation + ProviderCallID *string `json:"providerCallId,omitempty"` + // Error stack trace, when available + Stack *string `json:"stack,omitempty"` + // HTTP status code from the upstream request, if applicable + StatusCode *int64 `json:"statusCode,omitempty"` + // Background tasks still running when the agent became idle + BackgroundTasks *BackgroundTasks `json:"backgroundTasks,omitempty"` + // The new display title for the session + Title *string `json:"title,omitempty"` + // Category of informational message (e.g., "notification", "timing", "context_window", + // "mcp", "snapshot", "configuration", "authentication", "model") + InfoType *string `json:"infoType,omitempty"` + // Category of warning (e.g., "subscription", "policy", "mcp") + WarningType *string `json:"warningType,omitempty"` + // Newly selected model identifier + NewModel *string `json:"newModel,omitempty"` + // Model that was previously selected, if any + PreviousModel *string `json:"previousModel,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") + PreviousMode *string `json:"previousMode,omitempty"` + // The type of operation performed on the plan file + // + // Whether the file was newly created or updated + Operation *Operation `json:"operation,omitempty"` + // Relative path within the session workspace files directory + // + // File path to the SKILL.md definition + Path *string `json:"path,omitempty"` + // ISO 8601 timestamp when the handoff occurred + HandoffTime *time.Time `json:"handoffTime,omitempty"` + // Session ID of the remote session being handed off + 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 *RepositoryUnion `json:"repository"` + // Origin type of the session being handed off + 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 plan that was created + Summary *string `json:"summary,omitempty"` + // Number of messages removed by truncation + MessagesRemovedDuringTruncation *float64 `json:"messagesRemovedDuringTruncation,omitempty"` + // Identifier of the component that performed truncation (e.g., "BasicTruncator") + PerformedBy *string `json:"performedBy,omitempty"` + // Number of conversation messages after truncation + PostTruncationMessagesLength *float64 `json:"postTruncationMessagesLength,omitempty"` + // Total tokens in conversation messages after truncation + PostTruncationTokensInMessages *float64 `json:"postTruncationTokensInMessages,omitempty"` + // Number of conversation messages before truncation + PreTruncationMessagesLength *float64 `json:"preTruncationMessagesLength,omitempty"` + // Total tokens in conversation messages before truncation + PreTruncationTokensInMessages *float64 `json:"preTruncationTokensInMessages,omitempty"` + // Maximum token count for the model's context window + TokenLimit *float64 `json:"tokenLimit,omitempty"` + // Number of tokens removed by truncation + TokensRemovedDuringTruncation *float64 `json:"tokensRemovedDuringTruncation,omitempty"` + // Number of events that were removed by the rewind + EventsRemoved *float64 `json:"eventsRemoved,omitempty"` + // Event ID that was rewound to; all events after this one were removed + UpToEventID *string `json:"upToEventId,omitempty"` + // Aggregate code change metrics for the session + CodeChanges *CodeChanges `json:"codeChanges,omitempty"` + // Model that was selected at the time of shutdown + CurrentModel *string `json:"currentModel,omitempty"` + // Error description when shutdownType is "error" + ErrorReason *string `json:"errorReason,omitempty"` + // Per-model usage breakdown, keyed by model identifier + ModelMetrics map[string]ModelMetric `json:"modelMetrics,omitempty"` + // Unix timestamp (milliseconds) when the session started + SessionStartTime *float64 `json:"sessionStartTime,omitempty"` + // Whether the session ended normally ("routine") or due to a crash/fatal error ("error") + ShutdownType *ShutdownType `json:"shutdownType,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 + TotalPremiumRequests *float64 `json:"totalPremiumRequests,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"` + // Current number of tokens in the context window + CurrentTokens *float64 `json:"currentTokens,omitempty"` + // Current number of messages in the conversation + MessagesLength *float64 `json:"messagesLength,omitempty"` + // Checkpoint snapshot number created for recovery + CheckpointNumber *float64 `json:"checkpointNumber,omitempty"` + // File path where the checkpoint was stored + CheckpointPath *string `json:"checkpointPath,omitempty"` + // Token usage breakdown for the compaction LLM call + CompactionTokensUsed *CompactionTokensUsed `json:"compactionTokensUsed,omitempty"` + // Error message if compaction failed + // + // Error details when the tool execution failed + // + // Error message describing why the sub-agent failed + // + // Error details when the hook failed + Error *ErrorUnion `json:"error"` + // Number of messages removed during compaction + MessagesRemoved *float64 `json:"messagesRemoved,omitempty"` + // Total tokens in conversation after compaction + PostCompactionTokens *float64 `json:"postCompactionTokens,omitempty"` + // Number of messages before compaction + PreCompactionMessagesLength *float64 `json:"preCompactionMessagesLength,omitempty"` + // Total tokens in conversation before compaction + PreCompactionTokens *float64 `json:"preCompactionTokens,omitempty"` + // GitHub request tracing ID (x-github-request-id header) for the compaction LLM call + // + // Unique identifier for this permission request; used to respond via + // session.respondToPermission() + // + // Request ID of the resolved permission request; clients should dismiss any UI for this + // request + // + // Unique identifier for this input request; used to respond via + // session.respondToUserInput() + // + // Request ID of the resolved user input request; clients should dismiss any UI for this + // request + // + // Unique identifier for this elicitation request; used to respond via + // session.respondToElicitation() + // + // Request ID of the resolved elicitation request; clients should dismiss any UI for this + // 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 + // request + // + // Unique identifier for this request; used to respond via session.respondToQueuedCommand() + // + // Request ID of the resolved command request; clients should dismiss any UI for this + // request + // + // Unique identifier for this request; used to respond via session.respondToExitPlanMode() + // + // Request ID of the resolved exit plan mode request; clients should dismiss any UI for this + // request + RequestID *string `json:"requestId,omitempty"` + // Whether compaction completed successfully + // + // Whether the tool execution completed successfully + // + // Whether the hook completed successfully + Success *bool `json:"success,omitempty"` + // LLM-generated summary of the compacted conversation history + SummaryContent *string `json:"summaryContent,omitempty"` + // Number of tokens removed during compaction + TokensRemoved *float64 `json:"tokensRemoved,omitempty"` + // The agent mode that was active when this message was sent + AgentMode *AgentMode `json:"agentMode,omitempty"` + // Files, selections, or GitHub references attached to the message + Attachments []Attachment `json:"attachments,omitempty"` + // The user's message text as displayed in the timeline + // + // The complete extended thinking text from the model + // + // The assistant's text response content + // + // Full content of the skill file, injected into the conversation for the model + // + // The system or developer prompt text + Content *string `json:"content,omitempty"` + // CAPI interaction ID for correlating this user message with its turn + // + // CAPI interaction ID for correlating this turn with upstream telemetry + // + // CAPI interaction ID for correlating this message with upstream telemetry + // + // 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"` + // Transformed version of the message sent to the model, with XML wrapping, timestamps, and + // other augmentations for prompt caching + TransformedContent *string `json:"transformedContent,omitempty"` + // Identifier for this turn within the agentic loop, typically a stringified turn number + // + // Identifier of the turn that has ended, matching the corresponding assistant.turn_start + // event + TurnID *string `json:"turnId,omitempty"` + // Short description of what the agent is currently doing or planning to do + Intent *string `json:"intent,omitempty"` + // Unique identifier for this reasoning block + // + // Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning + // event + ReasoningID *string `json:"reasoningId,omitempty"` + // Incremental text chunk to append to the reasoning content + // + // Incremental text chunk to append to the message content + DeltaContent *string `json:"deltaContent,omitempty"` + // Cumulative total bytes received from the streaming response so far + TotalResponseSizeBytes *float64 `json:"totalResponseSizeBytes,omitempty"` + // Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. + EncryptedContent *string `json:"encryptedContent,omitempty"` + // Unique identifier for this assistant message + // + // Message ID this delta belongs to, matching the corresponding assistant.message event + MessageID *string `json:"messageId,omitempty"` + // Actual output token count from the API response (completion_tokens), used for accurate + // token accounting + // + // Number of output tokens produced + OutputTokens *float64 `json:"outputTokens,omitempty"` + // Tool call ID of the parent tool invocation when this event originates from a sub-agent + // + // Parent tool call ID when this usage originates from a sub-agent + ParentToolCallID *string `json:"parentToolCallId,omitempty"` + // Generation phase for phased-output models (e.g., thinking vs. response phases) + Phase *string `json:"phase,omitempty"` + // Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped + // on resume. + ReasoningOpaque *string `json:"reasoningOpaque,omitempty"` + // Readable reasoning text from the model's extended thinking + ReasoningText *string `json:"reasoningText,omitempty"` + // Tool invocations requested by the assistant in this message + ToolRequests []ToolRequest `json:"toolRequests,omitempty"` + // Completion ID from the model provider (e.g., chatcmpl-abc123) + APICallID *string `json:"apiCallId,omitempty"` + // Number of tokens read from prompt cache + CacheReadTokens *float64 `json:"cacheReadTokens,omitempty"` + // Number of tokens written to prompt cache + CacheWriteTokens *float64 `json:"cacheWriteTokens,omitempty"` + // Per-request cost and usage data from the CAPI copilot_usage response field + CopilotUsage *CopilotUsage `json:"copilotUsage,omitempty"` + // Model multiplier cost for billing purposes + Cost *float64 `json:"cost,omitempty"` + // Duration of the API call in milliseconds + Duration *float64 `json:"duration,omitempty"` + // What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls + Initiator *string `json:"initiator,omitempty"` + // Number of input tokens consumed + InputTokens *float64 `json:"inputTokens,omitempty"` + // Model identifier used for this API call + // + // Model identifier that generated this tool call + Model *string `json:"model,omitempty"` + // Per-quota resource usage snapshots, keyed by quota identifier + QuotaSnapshots map[string]QuotaSnapshot `json:"quotaSnapshots,omitempty"` + // Reason the current turn was aborted (e.g., "user initiated") + Reason *string `json:"reason,omitempty"` + // Arguments for the tool invocation + // + // Arguments passed to the tool + // + // Arguments to pass to the external tool + Arguments interface{} `json:"arguments"` + // Unique identifier for this tool call + // + // Tool call ID this partial result belongs to + // + // Tool call ID this progress notification belongs to + // + // Unique identifier for the completed tool call + // + // Tool call ID of the parent tool invocation that spawned this sub-agent + // + // Tool call ID assigned to this external tool invocation + ToolCallID *string `json:"toolCallId,omitempty"` + // Name of the tool the user wants to invoke + // + // Name of the tool being executed + // + // Name of the external tool to invoke + ToolName *string `json:"toolName,omitempty"` + // Name of the MCP server hosting this tool, when the tool is an MCP tool + MCPServerName *string `json:"mcpServerName,omitempty"` + // Original tool name on the MCP server, when the tool is an MCP tool + MCPToolName *string `json:"mcpToolName,omitempty"` + // Incremental output chunk from the running tool + PartialOutput *string `json:"partialOutput,omitempty"` + // Human-readable progress status message (e.g., from an MCP server) + ProgressMessage *string `json:"progressMessage,omitempty"` + // Whether this tool call was explicitly requested by the user rather than the assistant + IsUserRequested *bool `json:"isUserRequested,omitempty"` + // Tool execution result on success + // + // The result of the permission request + Result *Result `json:"result,omitempty"` + // Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) + ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"` + // Tool names that should be auto-approved when this skill is active + AllowedTools []string `json:"allowedTools,omitempty"` + // Name of the invoked skill + // + // Optional name identifier for the message source + Name *string `json:"name,omitempty"` + // Name of the plugin this skill originated from, when applicable + PluginName *string `json:"pluginName,omitempty"` + // Version of the plugin this skill originated from, when applicable + PluginVersion *string `json:"pluginVersion,omitempty"` + // Description of what the sub-agent does + AgentDescription *string `json:"agentDescription,omitempty"` + // Human-readable display name of the sub-agent + // + // Human-readable display name of the selected custom agent + AgentDisplayName *string `json:"agentDisplayName,omitempty"` + // Internal name of the sub-agent + // + // Internal name of the selected custom agent + AgentName *string `json:"agentName,omitempty"` + // List of tool names available to this agent, or null for all tools + Tools []string `json:"tools"` + // Unique identifier for this hook invocation + // + // Identifier matching the corresponding hook.start event + HookInvocationID *string `json:"hookInvocationId,omitempty"` + // Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + // + // Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + HookType *string `json:"hookType,omitempty"` + // Input data passed to the hook + Input interface{} `json:"input"` + // Output data produced by the hook + Output interface{} `json:"output"` + // Metadata about the prompt template and its construction + Metadata *Metadata `json:"metadata,omitempty"` + // Message role: "system" for system prompts, "developer" for developer-injected instructions + Role *Role `json:"role,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 + AllowFreeform *bool `json:"allowFreeform,omitempty"` + // Predefined choices for the user to select from, if applicable + 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. + Mode *Mode `json:"mode,omitempty"` + // JSON Schema describing the form fields to present to the user + RequestedSchema *RequestedSchema `json:"requestedSchema,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) + 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"` } type Attachment struct { - DisplayName *string `json:"displayName,omitempty"` - LineRange *LineRange `json:"lineRange,omitempty"` - Path *string `json:"path,omitempty"` - Type AttachmentType `json:"type"` - FilePath *string `json:"filePath,omitempty"` - Selection *SelectionClass `json:"selection,omitempty"` - Text *string `json:"text,omitempty"` - Number *float64 `json:"number,omitempty"` - ReferenceType *ReferenceType `json:"referenceType,omitempty"` - State *string `json:"state,omitempty"` - Title *string `json:"title,omitempty"` - URL *string `json:"url,omitempty"` + // User-facing display name for the attachment + // + // User-facing display name for the selection + 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 + Path *string `json:"path,omitempty"` + // Attachment type discriminator + Type AttachmentType `json:"type"` + // Absolute path to the file containing the selection + FilePath *string `json:"filePath,omitempty"` + // Position range of the selection within the file + Selection *SelectionClass `json:"selection,omitempty"` + // The selected text content + Text *string `json:"text,omitempty"` + // Issue, pull request, or discussion number + Number *float64 `json:"number,omitempty"` + // Type of GitHub reference + ReferenceType *ReferenceType `json:"referenceType,omitempty"` + // Current state of the referenced item (e.g., open, closed, merged) + State *string `json:"state,omitempty"` + // Title of the referenced item + Title *string `json:"title,omitempty"` + // URL to the referenced item on GitHub + URL *string `json:"url,omitempty"` } +// Optional line range to scope the attachment to a specific section of the file type LineRange struct { - End float64 `json:"end"` + // End line number (1-based, inclusive) + End float64 `json:"end"` + // Start line number (1-based) Start float64 `json:"start"` } +// Position range of the selection within the file type SelectionClass struct { End End `json:"end"` Start Start `json:"start"` } type End struct { + // End character offset within the line (0-based) Character float64 `json:"character"` - Line float64 `json:"line"` + // End line number (0-based) + Line float64 `json:"line"` } type Start struct { + // Start character offset within the line (0-based) Character float64 `json:"character"` - Line float64 `json:"line"` + // Start line number (0-based) + Line float64 `json:"line"` +} + +// Background tasks still running when the agent became idle +type BackgroundTasks struct { + // Currently running background agents + Agents []Agent `json:"agents"` + // Currently running background shell commands + Shells []Shell `json:"shells"` } +type Agent struct { + // Unique identifier of the background agent + AgentID string `json:"agentId"` + // Type of the background agent + AgentType string `json:"agentType"` + // Human-readable description of the agent task + Description *string `json:"description,omitempty"` +} + +type Shell struct { + // Human-readable description of the shell command + Description *string `json:"description,omitempty"` + // Unique identifier of the background shell + ShellID string `json:"shellId"` +} + +// Aggregate code change metrics for the session type CodeChanges struct { + // List of file paths that were modified during the session FilesModified []string `json:"filesModified"` - LinesAdded float64 `json:"linesAdded"` - LinesRemoved float64 `json:"linesRemoved"` + // Total number of lines added during the session + LinesAdded float64 `json:"linesAdded"` + // Total number of lines removed during the session + LinesRemoved float64 `json:"linesRemoved"` } +// Token usage breakdown for the compaction LLM call type CompactionTokensUsed struct { + // Cached input tokens reused in the compaction LLM call CachedInput float64 `json:"cachedInput"` - Input float64 `json:"input"` - Output float64 `json:"output"` + // Input tokens consumed by the compaction LLM call + Input float64 `json:"input"` + // Output tokens produced by the compaction LLM call + Output float64 `json:"output"` } +// Working directory and git context at session start +// +// Updated working directory and git context at resume time type ContextClass struct { - Branch *string `json:"branch,omitempty"` - Cwd string `json:"cwd"` - GitRoot *string `json:"gitRoot,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 Repository *string `json:"repository,omitempty"` } +// Per-request cost and usage data from the CAPI copilot_usage response field type CopilotUsage struct { + // Itemized token usage breakdown TokenDetails []TokenDetail `json:"tokenDetails"` - TotalNanoAiu float64 `json:"totalNanoAiu"` + // Total cost in nano-AIU (AI Units) for this request + TotalNanoAiu float64 `json:"totalNanoAiu"` } type TokenDetail struct { - BatchSize float64 `json:"batchSize"` + // Number of tokens in this billing batch + BatchSize float64 `json:"batchSize"` + // Cost per batch of tokens CostPerBatch float64 `json:"costPerBatch"` - TokenCount float64 `json:"tokenCount"` - TokenType string `json:"tokenType"` + // Total token count for this entry + TokenCount float64 `json:"tokenCount"` + // Token category (e.g., "input", "output") + TokenType string `json:"tokenType"` } +// Error details when the tool execution failed +// +// Error details when the hook failed type ErrorClass struct { - Code *string `json:"code,omitempty"` - Message string `json:"message"` - Stack *string `json:"stack,omitempty"` + // Machine-readable error code + Code *string `json:"code,omitempty"` + // Human-readable error message + Message string `json:"message"` + // Error stack trace, when available + Stack *string `json:"stack,omitempty"` } +// Metadata about the prompt template and its construction type Metadata struct { - PromptVersion *string `json:"promptVersion,omitempty"` - Variables map[string]interface{} `json:"variables,omitempty"` + // Version identifier of the prompt template used + PromptVersion *string `json:"promptVersion,omitempty"` + // Template variables used when constructing the prompt + Variables map[string]interface{} `json:"variables,omitempty"` } type ModelMetric struct { + // Request count and cost metrics Requests Requests `json:"requests"` - Usage Usage `json:"usage"` + // Token usage breakdown + Usage Usage `json:"usage"` } +// Request count and cost metrics type Requests struct { - Cost float64 `json:"cost"` + // Cumulative cost multiplier for requests to this model + Cost float64 `json:"cost"` + // Total number of API requests made to this model Count float64 `json:"count"` } +// Token usage breakdown type Usage struct { - CacheReadTokens float64 `json:"cacheReadTokens"` + // Total tokens read from prompt cache across all requests + CacheReadTokens float64 `json:"cacheReadTokens"` + // Total tokens written to prompt cache across all requests CacheWriteTokens float64 `json:"cacheWriteTokens"` - InputTokens float64 `json:"inputTokens"` - OutputTokens float64 `json:"outputTokens"` + // Total input tokens consumed across all requests to this model + InputTokens float64 `json:"inputTokens"` + // Total output tokens produced across all requests to this model + OutputTokens float64 `json:"outputTokens"` } +// Details of the permission being requested type PermissionRequest struct { - CanOfferSessionApproval *bool `json:"canOfferSessionApproval,omitempty"` - Commands []Command `json:"commands,omitempty"` - FullCommandText *string `json:"fullCommandText,omitempty"` - HasWriteFileRedirection *bool `json:"hasWriteFileRedirection,omitempty"` - Intention *string `json:"intention,omitempty"` - Kind Kind `json:"kind"` - PossiblePaths []string `json:"possiblePaths,omitempty"` - PossibleUrls []PossibleURL `json:"possibleUrls,omitempty"` - ToolCallID *string `json:"toolCallId,omitempty"` - Warning *string `json:"warning,omitempty"` - Diff *string `json:"diff,omitempty"` - FileName *string `json:"fileName,omitempty"` - NewFileContents *string `json:"newFileContents,omitempty"` - Path *string `json:"path,omitempty"` - Args interface{} `json:"args"` - ReadOnly *bool `json:"readOnly,omitempty"` - ServerName *string `json:"serverName,omitempty"` - ToolName *string `json:"toolName,omitempty"` - ToolTitle *string `json:"toolTitle,omitempty"` - URL *string `json:"url,omitempty"` - Citations *string `json:"citations,omitempty"` - Fact *string `json:"fact,omitempty"` - Subject *string `json:"subject,omitempty"` - ToolDescription *string `json:"toolDescription,omitempty"` + // 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"` + // The complete shell command text to be executed + FullCommandText *string `json:"fullCommandText,omitempty"` + // Whether the command includes a file write redirection (e.g., > or >>) + HasWriteFileRedirection *bool `json:"hasWriteFileRedirection,omitempty"` + // Human-readable description of what the command intends to do + // + // Human-readable description of the intended file change + // + // Human-readable description of why the file is being read + // + // Human-readable description of why the URL is being accessed + Intention *string `json:"intention,omitempty"` + // Permission kind discriminator + Kind PermissionRequestKind `json:"kind"` + // File paths that may be read or written by the command + PossiblePaths []string `json:"possiblePaths,omitempty"` + // URLs that may be accessed by the command + PossibleUrls []PossibleURL `json:"possibleUrls,omitempty"` + // Tool call ID that triggered this permission request + ToolCallID *string `json:"toolCallId,omitempty"` + // Optional warning message about risks of running this command + Warning *string `json:"warning,omitempty"` + // Unified diff showing the proposed changes + Diff *string `json:"diff,omitempty"` + // Path of the file being written to + FileName *string `json:"fileName,omitempty"` + // Complete new file contents for newly created files + NewFileContents *string `json:"newFileContents,omitempty"` + // Path of the file or directory being read + Path *string `json:"path,omitempty"` + // Arguments to pass to the MCP tool + // + // Arguments to pass to the custom tool + Args interface{} `json:"args"` + // Whether this MCP tool is read-only (no side effects) + ReadOnly *bool `json:"readOnly,omitempty"` + // Name of the MCP server providing the tool + ServerName *string `json:"serverName,omitempty"` + // Internal name of the MCP tool + // + // Name of the custom tool + ToolName *string `json:"toolName,omitempty"` + // Human-readable title of the MCP tool + ToolTitle *string `json:"toolTitle,omitempty"` + // URL to be fetched + URL *string `json:"url,omitempty"` + // Source references for the stored fact + Citations *string `json:"citations,omitempty"` + // The fact or convention being stored + Fact *string `json:"fact,omitempty"` + // Topic or subject of the memory being stored + Subject *string `json:"subject,omitempty"` + // Description of what the custom tool does + ToolDescription *string `json:"toolDescription,omitempty"` } type Command struct { + // Command identifier (e.g., executable name) Identifier string `json:"identifier"` - ReadOnly bool `json:"readOnly"` + // Whether this command is read-only (no side effects) + ReadOnly bool `json:"readOnly"` } type PossibleURL struct { + // URL that may be accessed by the command URL string `json:"url"` } type QuotaSnapshot struct { - EntitlementRequests float64 `json:"entitlementRequests"` - IsUnlimitedEntitlement bool `json:"isUnlimitedEntitlement"` - Overage float64 `json:"overage"` - OverageAllowedWithExhaustedQuota bool `json:"overageAllowedWithExhaustedQuota"` - RemainingPercentage float64 `json:"remainingPercentage"` - ResetDate *time.Time `json:"resetDate,omitempty"` - UsageAllowedWithExhaustedQuota bool `json:"usageAllowedWithExhaustedQuota"` - UsedRequests float64 `json:"usedRequests"` + // Total requests allowed by the entitlement + EntitlementRequests float64 `json:"entitlementRequests"` + // Whether the user has an unlimited usage entitlement + IsUnlimitedEntitlement bool `json:"isUnlimitedEntitlement"` + // Number of requests over the entitlement limit + Overage float64 `json:"overage"` + // Whether overage is allowed when quota is exhausted + OverageAllowedWithExhaustedQuota bool `json:"overageAllowedWithExhaustedQuota"` + // Percentage of quota remaining (0.0 to 1.0) + RemainingPercentage float64 `json:"remainingPercentage"` + // Date when the quota resets + ResetDate *time.Time `json:"resetDate,omitempty"` + // Whether usage is still permitted after quota exhaustion + UsageAllowedWithExhaustedQuota bool `json:"usageAllowedWithExhaustedQuota"` + // Number of requests already consumed + UsedRequests float64 `json:"usedRequests"` } +// Repository context for the handed-off session type RepositoryClass struct { + // Git branch name, if applicable Branch *string `json:"branch,omitempty"` - Name string `json:"name"` - Owner string `json:"owner"` + // Repository name + Name string `json:"name"` + // Repository owner (user or organization) + Owner string `json:"owner"` } +// JSON Schema describing the form fields to present to the user type RequestedSchema struct { + // Form field definitions, keyed by field name Properties map[string]interface{} `json:"properties"` - Required []string `json:"required,omitempty"` - Type RequestedSchemaType `json:"type"` + // List of required field names + Required []string `json:"required,omitempty"` + Type RequestedSchemaType `json:"type"` } +// Tool execution result on success +// +// The result of the permission request type Result struct { - Content string `json:"content"` - Contents []Content `json:"contents,omitempty"` - DetailedContent *string `json:"detailedContent,omitempty"` + // Concise tool result text sent to the LLM for chat completion, potentially truncated for + // token efficiency + Content *string `json:"content,omitempty"` + // Structured content blocks (text, images, audio, resources) returned by the tool in their + // native format + Contents []Content `json:"contents,omitempty"` + // Full detailed tool result for UI/timeline display, preserving complete content such as + // diffs. Falls back to content when absent. + DetailedContent *string `json:"detailedContent,omitempty"` + // The outcome of the permission request + Kind *ResultKind `json:"kind,omitempty"` } type Content struct { - Text *string `json:"text,omitempty"` - Type ContentType `json:"type"` - Cwd *string `json:"cwd,omitempty"` - ExitCode *float64 `json:"exitCode,omitempty"` - Data *string `json:"data,omitempty"` - MIMEType *string `json:"mimeType,omitempty"` - Description *string `json:"description,omitempty"` - Icons []Icon `json:"icons,omitempty"` - Name *string `json:"name,omitempty"` - Size *float64 `json:"size,omitempty"` - Title *string `json:"title,omitempty"` - URI *string `json:"uri,omitempty"` - Resource *ResourceClass `json:"resource,omitempty"` + // The text content + // + // Terminal/shell output text + Text *string `json:"text,omitempty"` + // Content block type discriminator + Type ContentType `json:"type"` + // Working directory where the command was executed + Cwd *string `json:"cwd,omitempty"` + // Process exit code, if the command has completed + ExitCode *float64 `json:"exitCode,omitempty"` + // Base64-encoded image data + // + // Base64-encoded audio data + Data *string `json:"data,omitempty"` + // MIME type of the image (e.g., image/png, image/jpeg) + // + // MIME type of the audio (e.g., audio/wav, audio/mpeg) + // + // MIME type of the resource content + MIMEType *string `json:"mimeType,omitempty"` + // Human-readable description of the resource + Description *string `json:"description,omitempty"` + // Icons associated with this resource + Icons []Icon `json:"icons,omitempty"` + // Resource name identifier + Name *string `json:"name,omitempty"` + // Size of the resource in bytes + Size *float64 `json:"size,omitempty"` + // Human-readable display title for the resource + Title *string `json:"title,omitempty"` + // URI identifying the resource + URI *string `json:"uri,omitempty"` + // The embedded resource contents, either text or base64-encoded binary + Resource *ResourceClass `json:"resource,omitempty"` } type Icon struct { - MIMEType *string `json:"mimeType,omitempty"` - Sizes []string `json:"sizes,omitempty"` - Src string `json:"src"` - Theme *Theme `json:"theme,omitempty"` + // MIME type of the icon image + MIMEType *string `json:"mimeType,omitempty"` + // Available icon sizes (e.g., ['16x16', '32x32']) + Sizes []string `json:"sizes,omitempty"` + // URL or path to the icon image + Src string `json:"src"` + // Theme variant this icon is intended for + Theme *Theme `json:"theme,omitempty"` } +// The embedded resource contents, either text or base64-encoded binary type ResourceClass struct { + // MIME type of the text content + // + // MIME type of the blob content MIMEType *string `json:"mimeType,omitempty"` - Text *string `json:"text,omitempty"` - URI string `json:"uri"` - Blob *string `json:"blob,omitempty"` + // Text content of the resource + Text *string `json:"text,omitempty"` + // URI identifying the resource + URI string `json:"uri"` + // Base64-encoded binary content of the resource + Blob *string `json:"blob,omitempty"` } type ToolRequest struct { - Arguments interface{} `json:"arguments"` - Name string `json:"name"` - ToolCallID string `json:"toolCallId"` - Type *ToolRequestType `json:"type,omitempty"` + // Arguments to pass to the tool, format depends on the tool + Arguments interface{} `json:"arguments"` + // Name of the tool being invoked + Name string `json:"name"` + // Unique identifier for this tool call + ToolCallID string `json:"toolCallId"` + // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool + // calls. Defaults to "function" when absent. + Type *ToolRequestType `json:"type,omitempty"` } +// The agent mode that was active when this message was sent type AgentMode string const ( @@ -366,6 +842,7 @@ const ( Plan AgentMode = "plan" ) +// Type of GitHub reference type ReferenceType string const ( @@ -389,6 +866,9 @@ const ( Form Mode = "form" ) +// The type of operation performed on the plan file +// +// Whether the file was newly created or updated type Operation string const ( @@ -397,16 +877,16 @@ const ( Update Operation = "update" ) -type Kind string +type PermissionRequestKind string const ( - CustomTool Kind = "custom-tool" - KindShell Kind = "shell" - MCP Kind = "mcp" - Memory Kind = "memory" - Read Kind = "read" - URL Kind = "url" - Write Kind = "write" + CustomTool PermissionRequestKind = "custom-tool" + KindShell PermissionRequestKind = "shell" + MCP PermissionRequestKind = "mcp" + Memory PermissionRequestKind = "memory" + Read PermissionRequestKind = "read" + URL PermissionRequestKind = "url" + Write PermissionRequestKind = "write" ) type RequestedSchemaType string @@ -415,6 +895,7 @@ const ( Object RequestedSchemaType = "object" ) +// Theme variant this icon is intended for type Theme string const ( @@ -433,6 +914,18 @@ const ( Text 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" +) + +// Message role: "system" for system prompts, "developer" for developer-injected instructions type Role string const ( @@ -440,6 +933,7 @@ const ( System Role = "system" ) +// Whether the session ended normally ("routine") or due to a crash/fatal error ("error") type ShutdownType string const ( @@ -447,6 +941,7 @@ const ( Routine ShutdownType = "routine" ) +// Origin type of the session being handed off type SourceType string const ( @@ -454,6 +949,8 @@ const ( Remote SourceType = "remote" ) +// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool +// calls. Defaults to "function" when absent. type ToolRequestType string const ( @@ -474,8 +971,14 @@ const ( 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" diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 858a8032..c295bd28 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -148,17 +148,19 @@ type SessionModeSetParams struct { } type SessionPlanReadResult struct { - // The content of plan.md, or null if it does not exist + // The content of the plan file, or null if it does not exist Content *string `json:"content"` - // Whether plan.md exists in the workspace + // Whether the plan file exists in the workspace Exists bool `json:"exists"` + // Absolute file path of the plan file, or null if workspace is not enabled + Path *string `json:"path"` } type SessionPlanUpdateResult struct { } type SessionPlanUpdateParams struct { - // The new content for plan.md + // The new content for the plan file Content string `json:"content"` } @@ -260,6 +262,40 @@ type SessionCompactionCompactResult struct { TokensRemoved float64 `json:"tokensRemoved"` } +type SessionToolsHandlePendingToolCallResult struct { + Success bool `json:"success"` +} + +type SessionToolsHandlePendingToolCallParams struct { + Error *string `json:"error,omitempty"` + RequestID string `json:"requestId"` + Result *ResultUnion `json:"result"` +} + +type ResultResult struct { + Error *string `json:"error,omitempty"` + ResultType *string `json:"resultType,omitempty"` + TextResultForLlm string `json:"textResultForLlm"` + ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"` +} + +type SessionPermissionsHandlePendingPermissionRequestResult struct { + Success bool `json:"success"` +} + +type SessionPermissionsHandlePendingPermissionRequestParams struct { + RequestID string `json:"requestId"` + Result SessionPermissionsHandlePendingPermissionRequestParamsResult `json:"result"` +} + +type SessionPermissionsHandlePendingPermissionRequestParamsResult struct { + Kind Kind `json:"kind"` + Rules []interface{} `json:"rules,omitempty"` + Feedback *string `json:"feedback,omitempty"` + Message *string `json:"message,omitempty"` + Path *string `json:"path,omitempty"` +} + // The current agent mode. // // The agent mode after switching. @@ -273,6 +309,21 @@ const ( Plan Mode = "plan" ) +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" +) + +type ResultUnion struct { + ResultResult *ResultResult + String *string +} + type ModelsRpcApi struct{ client *jsonrpc2.Client } func (a *ModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error) { @@ -343,12 +394,12 @@ func NewServerRpc(client *jsonrpc2.Client) *ServerRpc { } } -type ModelRpcApi struct { +type SessionModelRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *ModelRpcApi) GetCurrent(ctx context.Context) (*SessionModelGetCurrentResult, error) { +func (a *SessionModelRpcApi) GetCurrent(ctx context.Context) (*SessionModelGetCurrentResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.model.getCurrent", req) if err != nil { @@ -361,7 +412,7 @@ func (a *ModelRpcApi) GetCurrent(ctx context.Context) (*SessionModelGetCurrentRe return &result, nil } -func (a *ModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchToParams) (*SessionModelSwitchToResult, error) { +func (a *SessionModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchToParams) (*SessionModelSwitchToResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["modelId"] = params.ModelID @@ -377,12 +428,12 @@ func (a *ModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchTo return &result, nil } -type ModeRpcApi struct { +type SessionModeRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *ModeRpcApi) Get(ctx context.Context) (*SessionModeGetResult, error) { +func (a *SessionModeRpcApi) Get(ctx context.Context) (*SessionModeGetResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.mode.get", req) if err != nil { @@ -395,7 +446,7 @@ func (a *ModeRpcApi) Get(ctx context.Context) (*SessionModeGetResult, error) { return &result, nil } -func (a *ModeRpcApi) Set(ctx context.Context, params *SessionModeSetParams) (*SessionModeSetResult, error) { +func (a *SessionModeRpcApi) Set(ctx context.Context, params *SessionModeSetParams) (*SessionModeSetResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["mode"] = params.Mode @@ -411,12 +462,12 @@ func (a *ModeRpcApi) Set(ctx context.Context, params *SessionModeSetParams) (*Se return &result, nil } -type PlanRpcApi struct { +type SessionPlanRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *PlanRpcApi) Read(ctx context.Context) (*SessionPlanReadResult, error) { +func (a *SessionPlanRpcApi) Read(ctx context.Context) (*SessionPlanReadResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.plan.read", req) if err != nil { @@ -429,7 +480,7 @@ func (a *PlanRpcApi) Read(ctx context.Context) (*SessionPlanReadResult, error) { return &result, nil } -func (a *PlanRpcApi) Update(ctx context.Context, params *SessionPlanUpdateParams) (*SessionPlanUpdateResult, error) { +func (a *SessionPlanRpcApi) Update(ctx context.Context, params *SessionPlanUpdateParams) (*SessionPlanUpdateResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["content"] = params.Content @@ -445,7 +496,7 @@ func (a *PlanRpcApi) Update(ctx context.Context, params *SessionPlanUpdateParams return &result, nil } -func (a *PlanRpcApi) Delete(ctx context.Context) (*SessionPlanDeleteResult, error) { +func (a *SessionPlanRpcApi) Delete(ctx context.Context) (*SessionPlanDeleteResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.plan.delete", req) if err != nil { @@ -458,12 +509,12 @@ func (a *PlanRpcApi) Delete(ctx context.Context) (*SessionPlanDeleteResult, erro return &result, nil } -type WorkspaceRpcApi struct { +type SessionWorkspaceRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *WorkspaceRpcApi) ListFiles(ctx context.Context) (*SessionWorkspaceListFilesResult, error) { +func (a *SessionWorkspaceRpcApi) ListFiles(ctx context.Context) (*SessionWorkspaceListFilesResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.workspace.listFiles", req) if err != nil { @@ -476,7 +527,7 @@ func (a *WorkspaceRpcApi) ListFiles(ctx context.Context) (*SessionWorkspaceListF return &result, nil } -func (a *WorkspaceRpcApi) ReadFile(ctx context.Context, params *SessionWorkspaceReadFileParams) (*SessionWorkspaceReadFileResult, error) { +func (a *SessionWorkspaceRpcApi) ReadFile(ctx context.Context, params *SessionWorkspaceReadFileParams) (*SessionWorkspaceReadFileResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["path"] = params.Path @@ -492,7 +543,7 @@ func (a *WorkspaceRpcApi) ReadFile(ctx context.Context, params *SessionWorkspace return &result, nil } -func (a *WorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspaceCreateFileParams) (*SessionWorkspaceCreateFileResult, error) { +func (a *SessionWorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspaceCreateFileParams) (*SessionWorkspaceCreateFileResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["path"] = params.Path @@ -509,12 +560,12 @@ func (a *WorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspa return &result, nil } -type FleetRpcApi struct { +type SessionFleetRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams) (*SessionFleetStartResult, error) { +func (a *SessionFleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams) (*SessionFleetStartResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { if params.Prompt != nil { @@ -532,12 +583,12 @@ func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams return &result, nil } -type AgentRpcApi struct { +type SessionAgentRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *AgentRpcApi) List(ctx context.Context) (*SessionAgentListResult, error) { +func (a *SessionAgentRpcApi) List(ctx context.Context) (*SessionAgentListResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.agent.list", req) if err != nil { @@ -550,7 +601,7 @@ func (a *AgentRpcApi) List(ctx context.Context) (*SessionAgentListResult, error) return &result, nil } -func (a *AgentRpcApi) GetCurrent(ctx context.Context) (*SessionAgentGetCurrentResult, error) { +func (a *SessionAgentRpcApi) GetCurrent(ctx context.Context) (*SessionAgentGetCurrentResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.agent.getCurrent", req) if err != nil { @@ -563,7 +614,7 @@ func (a *AgentRpcApi) GetCurrent(ctx context.Context) (*SessionAgentGetCurrentRe return &result, nil } -func (a *AgentRpcApi) Select(ctx context.Context, params *SessionAgentSelectParams) (*SessionAgentSelectResult, error) { +func (a *SessionAgentRpcApi) Select(ctx context.Context, params *SessionAgentSelectParams) (*SessionAgentSelectResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["name"] = params.Name @@ -579,7 +630,7 @@ func (a *AgentRpcApi) Select(ctx context.Context, params *SessionAgentSelectPara return &result, nil } -func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult, error) { +func (a *SessionAgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.agent.deselect", req) if err != nil { @@ -592,12 +643,12 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult return &result, nil } -type CompactionRpcApi struct { +type SessionCompactionRpcApi struct { client *jsonrpc2.Client sessionID string } -func (a *CompactionRpcApi) Compact(ctx context.Context) (*SessionCompactionCompactResult, error) { +func (a *SessionCompactionRpcApi) Compact(ctx context.Context) (*SessionCompactionCompactResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.compaction.compact", req) if err != nil { @@ -610,27 +661,80 @@ func (a *CompactionRpcApi) Compact(ctx context.Context) (*SessionCompactionCompa return &result, nil } +type SessionToolsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *SessionToolsRpcApi) HandlePendingToolCall(ctx context.Context, params *SessionToolsHandlePendingToolCallParams) (*SessionToolsHandlePendingToolCallResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + if params.Result != nil { + req["result"] = *params.Result + } + if params.Error != nil { + req["error"] = *params.Error + } + } + raw, err := a.client.Request("session.tools.handlePendingToolCall", req) + if err != nil { + return nil, err + } + var result SessionToolsHandlePendingToolCallResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +type SessionPermissionsRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *SessionPermissionsRpcApi) HandlePendingPermissionRequest(ctx context.Context, params *SessionPermissionsHandlePendingPermissionRequestParams) (*SessionPermissionsHandlePendingPermissionRequestResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request("session.permissions.handlePendingPermissionRequest", req) + if err != nil { + return nil, err + } + var result SessionPermissionsHandlePendingPermissionRequestResult + 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 - sessionID string - Model *ModelRpcApi - Mode *ModeRpcApi - Plan *PlanRpcApi - Workspace *WorkspaceRpcApi - Fleet *FleetRpcApi - Agent *AgentRpcApi - Compaction *CompactionRpcApi + client *jsonrpc2.Client + sessionID string + Model *SessionModelRpcApi + Mode *SessionModeRpcApi + Plan *SessionPlanRpcApi + Workspace *SessionWorkspaceRpcApi + Fleet *SessionFleetRpcApi + Agent *SessionAgentRpcApi + Compaction *SessionCompactionRpcApi + Tools *SessionToolsRpcApi + Permissions *SessionPermissionsRpcApi } func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { return &SessionRpc{client: client, sessionID: sessionID, - Model: &ModelRpcApi{client: client, sessionID: sessionID}, - Mode: &ModeRpcApi{client: client, sessionID: sessionID}, - Plan: &PlanRpcApi{client: client, sessionID: sessionID}, - Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID}, - Fleet: &FleetRpcApi{client: client, sessionID: sessionID}, - Agent: &AgentRpcApi{client: client, sessionID: sessionID}, - Compaction: &CompactionRpcApi{client: client, sessionID: sessionID}, + Model: &SessionModelRpcApi{client: client, sessionID: sessionID}, + Mode: &SessionModeRpcApi{client: client, sessionID: sessionID}, + Plan: &SessionPlanRpcApi{client: client, sessionID: sessionID}, + Workspace: &SessionWorkspaceRpcApi{client: client, sessionID: sessionID}, + Fleet: &SessionFleetRpcApi{client: client, sessionID: sessionID}, + Agent: &SessionAgentRpcApi{client: client, sessionID: sessionID}, + Compaction: &SessionCompactionRpcApi{client: client, sessionID: sessionID}, + Tools: &SessionToolsRpcApi{client: client, sessionID: sessionID}, + Permissions: &SessionPermissionsRpcApi{client: client, sessionID: sessionID}, } } diff --git a/go/sdk_protocol_version.go b/go/sdk_protocol_version.go index 52b1ebe0..4e6caafe 100644 --- a/go/sdk_protocol_version.go +++ b/go/sdk_protocol_version.go @@ -2,11 +2,20 @@ package copilot -// SdkProtocolVersion is the SDK protocol version. +// SdkProtocolVersion is the maximum SDK protocol version supported. // This must match the version expected by the copilot-agent-runtime server. -const SdkProtocolVersion = 2 +const SdkProtocolVersion = 3 -// GetSdkProtocolVersion returns the SDK protocol version. +// MinSdkProtocolVersion is the minimum SDK protocol version supported. +// Servers reporting a version in [Min, Max] are considered compatible. +const MinSdkProtocolVersion = 2 + +// GetSdkProtocolVersion returns the SDK protocol version (maximum supported). func GetSdkProtocolVersion() int { return SdkProtocolVersion } + +// GetMinSdkProtocolVersion returns the minimum SDK protocol version supported. +func GetMinSdkProtocolVersion() int { + return MinSdkProtocolVersion +} diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index fc3d4e3b..5ce2ff94 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.421", + "@github/copilot": "^1.0.1", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "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.2", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.2.tgz", + "integrity": "sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww==", "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.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" } }, "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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw==", "cpu": [ "arm64" ], @@ -695,9 +695,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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.2.tgz", + "integrity": "sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg==", "cpu": [ "x64" ], @@ -711,9 +711,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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.2.tgz", + "integrity": "sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q==", "cpu": [ "arm64" ], @@ -727,9 +727,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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.2.tgz", + "integrity": "sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw==", "cpu": [ "x64" ], @@ -743,9 +743,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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.2.tgz", + "integrity": "sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA==", "cpu": [ "arm64" ], @@ -759,9 +759,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.2", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.2.tgz", + "integrity": "sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index ef89556a..fc7d5a38 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -40,7 +40,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.421", + "@github/copilot": "^1.0.1", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/scripts/update-protocol-version.ts b/nodejs/scripts/update-protocol-version.ts index 46f6189e..49b2b3bd 100644 --- a/nodejs/scripts/update-protocol-version.ts +++ b/nodejs/scripts/update-protocol-version.ts @@ -24,8 +24,9 @@ const __dirname = path.dirname(__filename); const rootDir = path.join(__dirname, "..", ".."); const version = versionFile.version; +const minVersion = (versionFile as Record).minVersion as number | undefined ?? version; -console.log(`Generating SDK protocol version constants for version ${version}...`); +console.log(`Generating SDK protocol version constants for version ${version} (min: ${minVersion})...`); // Generate TypeScript const tsCode = `/*--------------------------------------------------------------------------------------------- @@ -35,18 +36,32 @@ const tsCode = `/*-------------------------------------------------------------- // Code generated by update-protocol-version.ts. DO NOT EDIT. /** - * The SDK protocol version. + * The maximum SDK protocol version supported. * This must match the version expected by the copilot-agent-runtime server. */ export const SDK_PROTOCOL_VERSION = ${version}; /** - * Gets the SDK protocol version. + * The minimum SDK protocol version supported. + * Servers reporting a version in [MIN, MAX] are considered compatible. + */ +export const MIN_SDK_PROTOCOL_VERSION = ${minVersion}; + +/** + * Gets the SDK protocol version (maximum supported). * @returns The protocol version number */ export function getSdkProtocolVersion(): number { return SDK_PROTOCOL_VERSION; } + +/** + * Gets the minimum SDK protocol version supported. + * @returns The minimum protocol version number + */ +export function getMinSdkProtocolVersion(): number { + return MIN_SDK_PROTOCOL_VERSION; +} `; fs.writeFileSync(path.join(rootDir, "nodejs", "src", "sdkProtocolVersion.ts"), tsCode); console.log(" ✓ nodejs/src/sdkProtocolVersion.ts"); @@ -56,14 +71,23 @@ const goCode = `// Code generated by update-protocol-version.ts. DO NOT EDIT. package copilot -// SdkProtocolVersion is the SDK protocol version. +// SdkProtocolVersion is the maximum SDK protocol version supported. // This must match the version expected by the copilot-agent-runtime server. const SdkProtocolVersion = ${version} -// GetSdkProtocolVersion returns the SDK protocol version. +// MinSdkProtocolVersion is the minimum SDK protocol version supported. +// Servers reporting a version in [Min, Max] are considered compatible. +const MinSdkProtocolVersion = ${minVersion} + +// GetSdkProtocolVersion returns the SDK protocol version (maximum supported). func GetSdkProtocolVersion() int { return SdkProtocolVersion } + +// GetMinSdkProtocolVersion returns the minimum SDK protocol version supported. +func GetMinSdkProtocolVersion() int { + return MinSdkProtocolVersion +} `; fs.writeFileSync(path.join(rootDir, "go", "sdk_protocol_version.go"), goCode); console.log(" ✓ go/sdk_protocol_version.go"); @@ -78,16 +102,30 @@ This must match the version expected by the copilot-agent-runtime server. """ SDK_PROTOCOL_VERSION = ${version} +"""The maximum SDK protocol version supported.""" + +MIN_SDK_PROTOCOL_VERSION = ${minVersion} +"""The minimum SDK protocol version supported.""" def get_sdk_protocol_version() -> int: """ - Gets the SDK protocol version. + Gets the SDK protocol version (maximum supported). Returns: The protocol version number """ return SDK_PROTOCOL_VERSION + + +def get_min_sdk_protocol_version() -> int: + """ + Gets the minimum SDK protocol version supported. + + Returns: + The minimum protocol version number + """ + return MIN_SDK_PROTOCOL_VERSION `; fs.writeFileSync(path.join(rootDir, "python", "copilot", "sdk_protocol_version.py"), pythonCode); console.log(" ✓ python/copilot/sdk_protocol_version.py"); @@ -104,14 +142,25 @@ namespace GitHub.Copilot.SDK; internal static class SdkProtocolVersion { /// - /// The SDK protocol version. + /// The maximum SDK protocol version supported. /// private const int Version = ${version}; /// - /// Gets the SDK protocol version. + /// The minimum SDK protocol version supported. + /// Servers reporting a version in [Min, Max] are considered compatible. + /// + private const int MinVersion = ${minVersion}; + + /// + /// Gets the SDK protocol version (maximum supported). /// public static int GetVersion() => Version; + + /// + /// Gets the minimum SDK protocol version supported. + /// + public static int GetMinVersion() => MinVersion; } `; fs.writeFileSync(path.join(rootDir, "dotnet", "src", "SdkProtocolVersion.cs"), csharpCode); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 7e441a7d..e9226199 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -23,7 +23,7 @@ import { StreamMessageWriter, } from "vscode-jsonrpc/node.js"; import { createServerRpc } from "./generated/rpc.js"; -import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; +import { getSdkProtocolVersion, getMinSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; import type { ConnectionState, @@ -145,6 +145,7 @@ export class CopilotClient { }; private isExternalServer: boolean = false; private forceStopping: boolean = false; + private negotiatedProtocolVersion: number = 0; private modelsCache: ModelInfo[] | null = null; private modelsCacheLock: Promise = Promise.resolve(); private sessionLifecycleHandlers: Set = new Set(); @@ -775,7 +776,8 @@ export class CopilotClient { * Verify that the server's protocol version matches the SDK's expected version */ private async verifyProtocolVersion(): Promise { - const expectedVersion = getSdkProtocolVersion(); + const maxVersion = getSdkProtocolVersion(); + const minVersion = getMinSdkProtocolVersion(); // Race ping against process exit to detect early CLI failures let pingResult: Awaited>; @@ -789,17 +791,19 @@ export class CopilotClient { if (serverVersion === undefined) { throw new Error( - `SDK protocol version mismatch: SDK expects version ${expectedVersion}, but server does not report a protocol version. ` + + `SDK protocol version mismatch: SDK supports versions ${minVersion}-${maxVersion}, but server does not report a protocol version. ` + `Please update your server to ensure compatibility.` ); } - if (serverVersion !== expectedVersion) { + if (serverVersion < minVersion || serverVersion > maxVersion) { throw new Error( - `SDK protocol version mismatch: SDK expects version ${expectedVersion}, but server reports version ${serverVersion}. ` + + `SDK protocol version mismatch: SDK supports versions ${minVersion}-${maxVersion}, but server reports version ${serverVersion}. ` + `Please update your SDK or server to ensure compatibility.` ); } + + this.negotiatedProtocolVersion = serverVersion; } /** @@ -1340,9 +1344,129 @@ export class CopilotClient { return; } - const session = this.sessions.get((notification as { sessionId: string }).sessionId); + const sessionId = (notification as { sessionId: string }).sessionId; + const event = (notification as { event: SessionEvent }).event; + const session = this.sessions.get(sessionId); + if (session) { - session._dispatchEvent((notification as { event: SessionEvent }).event); + session._dispatchEvent(event); + } + + // Protocol v3: handle broadcast tool/permission events + if (this.negotiatedProtocolVersion >= 3) { + this.handleBroadcastEvent(sessionId, event); + } + } + + private handleBroadcastEvent(sessionId: string, event: SessionEvent): void { + const eventType = (event as unknown as { type: string }).type; + if (eventType === "external_tool.requested") { + void this.handleExternalToolRequested(sessionId, event); + } else if (eventType === "permission.requested") { + void this.handlePermissionRequested(sessionId, event); + } + } + + private async handleExternalToolRequested( + sessionId: string, + event: SessionEvent + ): Promise { + const data = ( + event as unknown as { + data: { + requestId: string; + sessionId: string; + toolCallId: string; + toolName: string; + arguments: unknown; + }; + } + ).data; + if (!data || !data.requestId || !data.toolName) { + return; + } + + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + const handler = session.getToolHandler(data.toolName); + try { + let result: ToolResult; + if (!handler) { + result = this.buildUnsupportedToolResult(data.toolName); + } else { + const response = await this.executeToolCall(handler, { + sessionId, + toolCallId: data.toolCallId, + toolName: data.toolName, + arguments: data.arguments, + }); + result = response.result; + } + + if (!this.connection) return; + await this.connection.sendRequest("session.tools.handlePendingToolCall", { + sessionId, + requestId: data.requestId, + result, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + try { + if (!this.connection) return; + await this.connection.sendRequest("session.tools.handlePendingToolCall", { + sessionId, + requestId: data.requestId, + error: message, + }); + } catch { + // Connection may be closed + } + } + } + + private async handlePermissionRequested(sessionId: string, event: SessionEvent): Promise { + const data = ( + event as unknown as { data: { requestId: string; permissionRequest: unknown } } + ).data; + if (!data || !data.requestId || !data.permissionRequest) { + return; + } + + const session = this.sessions.get(sessionId); + if (!session) { + return; + } + + try { + const result = await session._handlePermissionRequest(data.permissionRequest); + if (!this.connection) return; + await this.connection.sendRequest( + "session.permissions.handlePendingPermissionRequest", + { + sessionId, + requestId: data.requestId, + result, + } + ); + } catch { + try { + if (!this.connection) return; + await this.connection.sendRequest( + "session.permissions.handlePendingPermissionRequest", + { + sessionId, + requestId: data.requestId, + result: { + kind: "denied-no-approval-rule-and-could-not-request-from-user", + }, + } + ); + } catch { + // Connection may be closed + } } } diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index af6d2778..c230348e 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -209,13 +209,17 @@ export interface SessionModeSetParams { export interface SessionPlanReadResult { /** - * Whether plan.md exists in the workspace + * Whether the plan file exists in the workspace */ exists: boolean; /** - * The content of plan.md, or null if it does not exist + * The content of the plan file, or null if it does not exist */ content: string | null; + /** + * Absolute file path of the plan file, or null if workspace is not enabled + */ + path: string | null; } export interface SessionPlanReadParams { @@ -233,7 +237,7 @@ export interface SessionPlanUpdateParams { */ sessionId: string; /** - * The new content for plan.md + * The new content for the plan file */ content: string; } @@ -430,6 +434,61 @@ export interface SessionCompactionCompactParams { sessionId: string; } +export interface SessionToolsHandlePendingToolCallResult { + success: boolean; +} + +export interface SessionToolsHandlePendingToolCallParams { + /** + * Target session identifier + */ + sessionId: string; + requestId: string; + result?: + | string + | { + textResultForLlm: string; + resultType?: string; + error?: string; + toolTelemetry?: { + [k: string]: unknown; + }; + }; + error?: string; +} + +export interface SessionPermissionsHandlePendingPermissionRequestResult { + success: boolean; +} + +export interface SessionPermissionsHandlePendingPermissionRequestParams { + /** + * Target session identifier + */ + sessionId: string; + requestId: string; + result: + | { + kind: "approved"; + } + | { + kind: "denied-by-rules"; + rules: unknown[]; + } + | { + kind: "denied-no-approval-rule-and-could-not-request-from-user"; + } + | { + kind: "denied-interactively-by-user"; + feedback?: string; + } + | { + kind: "denied-by-content-exclusion-policy"; + path: string; + message: string; + }; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -499,5 +558,13 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin compact: async (): Promise => connection.sendRequest("session.compaction.compact", { sessionId }), }, + tools: { + handlePendingToolCall: async (params: Omit): Promise => + connection.sendRequest("session.tools.handlePendingToolCall", { 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 4b0e4c0b..cf87e102 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -5,747 +5,2147 @@ 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: "session.start"; data: { + /** + * Unique identifier for the session + */ sessionId: string; + /** + * Schema version number for the session event format + */ version: number; + /** + * Identifier of the software producing the events (e.g., "copilot-agent") + */ producer: string; + /** + * Version string of the Copilot application + */ copilotVersion: string; + /** + * ISO 8601 timestamp when the session was created + */ startTime: string; + /** + * Model selected at session creation time, if any + */ selectedModel?: string; + /** + * Working directory and git context at session start + */ context?: { + /** + * Current working directory path + */ cwd: string; + /** + * Root directory of the git repository, resolved via git rev-parse + */ gitRoot?: string; + /** + * Repository identifier in "owner/name" format, derived from the git remote URL + */ repository?: string; + /** + * Current git branch name + */ branch?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.resume"; data: { + /** + * ISO 8601 timestamp when the session was resumed + */ resumeTime: string; + /** + * Total number of persisted events in the session at the time of resume + */ eventCount: number; + /** + * Updated working directory and git context at resume time + */ context?: { + /** + * Current working directory path + */ cwd: string; + /** + * Root directory of the git repository, resolved via git rev-parse + */ gitRoot?: string; + /** + * Repository identifier in "owner/name" format, derived from the git remote URL + */ repository?: string; + /** + * Current git branch name + */ branch?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.error"; data: { + /** + * Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", "query") + */ errorType: string; + /** + * Human-readable error message + */ message: string; + /** + * Error stack trace, when available + */ stack?: string; + /** + * HTTP status code from the upstream request, if applicable + */ statusCode?: number; + /** + * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs + */ providerCallId?: 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.idle"; - data: {}; + /** + * Payload indicating the agent is idle; includes any background tasks still in flight + */ + data: { + /** + * Background tasks still running when the agent became idle + */ + backgroundTasks?: { + /** + * Currently running background agents + */ + agents: { + /** + * Unique identifier of the background agent + */ + agentId: string; + /** + * Type of the background agent + */ + agentType: string; + /** + * Human-readable description of the agent task + */ + description?: string; + }[]; + /** + * Currently running background shell commands + */ + shells: { + /** + * Unique identifier of the background shell + */ + shellId: string; + /** + * Human-readable description of the shell command + */ + description?: 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.title_changed"; data: { + /** + * The new display title for the session + */ title: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.info"; data: { + /** + * Category of informational message (e.g., "notification", "timing", "context_window", "mcp", "snapshot", "configuration", "authentication", "model") + */ infoType: string; + /** + * Human-readable informational message for display in the timeline + */ message: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.warning"; data: { + /** + * Category of warning (e.g., "subscription", "policy", "mcp") + */ warningType: string; + /** + * Human-readable warning message for display in the timeline + */ message: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.model_change"; data: { + /** + * Model that was previously selected, if any + */ previousModel?: string; + /** + * Newly selected model identifier + */ newModel: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.mode_changed"; data: { + /** + * Agent mode before the change (e.g., "interactive", "plan", "autopilot") + */ previousMode: string; + /** + * Agent mode after the change (e.g., "interactive", "plan", "autopilot") + */ newMode: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.plan_changed"; data: { + /** + * The type of operation performed on the plan file + */ operation: "create" | "update" | "delete"; }; } | { + /** + * 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: "session.workspace_file_changed"; data: { /** - * Relative path within the workspace files directory + * Relative path within the session workspace files directory */ path: string; + /** + * Whether the file was newly created or updated + */ operation: "create" | "update"; }; } | { + /** + * 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: "session.handoff"; data: { + /** + * ISO 8601 timestamp when the handoff occurred + */ handoffTime: string; + /** + * Origin type of the session being handed off + */ sourceType: "remote" | "local"; + /** + * Repository context for the handed-off session + */ repository?: { + /** + * Repository owner (user or organization) + */ owner: string; + /** + * Repository name + */ name: string; + /** + * Git branch name, if applicable + */ branch?: string; }; + /** + * Additional context information for the handoff + */ context?: string; + /** + * Summary of the work done in the source session + */ summary?: string; + /** + * Session ID of the remote session being handed off + */ remoteSessionId?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.truncation"; data: { + /** + * Maximum token count for the model's context window + */ tokenLimit: number; + /** + * Total tokens in conversation messages before truncation + */ preTruncationTokensInMessages: number; + /** + * Number of conversation messages before truncation + */ preTruncationMessagesLength: number; + /** + * Total tokens in conversation messages after truncation + */ postTruncationTokensInMessages: number; + /** + * Number of conversation messages after truncation + */ postTruncationMessagesLength: number; + /** + * Number of tokens removed by truncation + */ tokensRemovedDuringTruncation: number; + /** + * Number of messages removed by truncation + */ messagesRemovedDuringTruncation: number; + /** + * Identifier of the component that performed truncation (e.g., "BasicTruncator") + */ performedBy: 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.snapshot_rewind"; data: { + /** + * Event ID that was rewound to; all events after this one were removed + */ upToEventId: string; + /** + * Number of events that were removed by the rewind + */ eventsRemoved: number; }; } | { + /** + * 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; type: "session.shutdown"; data: { + /** + * Whether the session ended normally ("routine") or due to a crash/fatal error ("error") + */ shutdownType: "routine" | "error"; + /** + * Error description when shutdownType is "error" + */ errorReason?: string; + /** + * Total number of premium API requests used during the session + */ totalPremiumRequests: number; + /** + * Cumulative time spent in API calls during the session, in milliseconds + */ totalApiDurationMs: number; + /** + * Unix timestamp (milliseconds) when the session started + */ sessionStartTime: number; + /** + * Aggregate code change metrics for the session + */ codeChanges: { + /** + * Total number of lines added during the session + */ linesAdded: number; + /** + * Total number of lines removed during the session + */ linesRemoved: number; + /** + * List of file paths that were modified during the session + */ filesModified: string[]; }; + /** + * Per-model usage breakdown, keyed by model identifier + */ modelMetrics: { [k: string]: { + /** + * Request count and cost metrics + */ requests: { + /** + * Total number of API requests made to this model + */ count: number; + /** + * Cumulative cost multiplier for requests to this model + */ cost: number; }; + /** + * Token usage breakdown + */ usage: { + /** + * Total input tokens consumed across all requests to this model + */ inputTokens: number; + /** + * Total output tokens produced across all requests to this model + */ outputTokens: number; + /** + * Total tokens read from prompt cache across all requests + */ cacheReadTokens: number; + /** + * Total tokens written to prompt cache across all requests + */ cacheWriteTokens: number; }; }; }; + /** + * Model that was selected at the time of shutdown + */ currentModel?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.context_changed"; data: { + /** + * Current working directory path + */ cwd: string; + /** + * Root directory of the git repository, resolved via git rev-parse + */ gitRoot?: string; + /** + * Repository identifier in "owner/name" format, derived from the git remote URL + */ repository?: string; + /** + * Current git branch name + */ branch?: 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.usage_info"; data: { + /** + * Maximum token count for the model's context window + */ tokenLimit: number; + /** + * Current number of tokens in the context window + */ currentTokens: number; + /** + * Current number of messages in the conversation + */ messagesLength: number; }; } | { + /** + * 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: "session.compaction_start"; + /** + * Empty payload; the event signals that LLM-powered conversation compaction has begun + */ 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.compaction_complete"; data: { + /** + * Whether compaction completed successfully + */ success: boolean; + /** + * Error message if compaction failed + */ error?: string; + /** + * Total tokens in conversation before compaction + */ preCompactionTokens?: number; + /** + * Total tokens in conversation after compaction + */ postCompactionTokens?: number; + /** + * Number of messages before compaction + */ preCompactionMessagesLength?: number; + /** + * Number of messages removed during compaction + */ messagesRemoved?: number; + /** + * Number of tokens removed during compaction + */ tokensRemoved?: number; + /** + * LLM-generated summary of the compacted conversation history + */ summaryContent?: string; + /** + * Checkpoint snapshot number created for recovery + */ checkpointNumber?: number; + /** + * File path where the checkpoint was stored + */ checkpointPath?: string; + /** + * Token usage breakdown for the compaction LLM call + */ compactionTokensUsed?: { + /** + * Input tokens consumed by the compaction LLM call + */ input: number; + /** + * Output tokens produced by the compaction LLM call + */ output: number; + /** + * Cached input tokens reused in the compaction LLM call + */ cachedInput: number; }; + /** + * GitHub request tracing ID (x-github-request-id header) for the compaction LLM call + */ 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "session.task_complete"; data: { + /** + * Optional summary of the completed task, provided by the agent + */ summary?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "user.message"; data: { + /** + * The user's message text as displayed in the timeline + */ content: string; + /** + * Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching + */ transformedContent?: string; + /** + * Files, selections, or GitHub references attached to the message + */ attachments?: ( | { type: "file"; + /** + * Absolute file or 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; }; } | { type: "directory"; + /** + * Absolute file or 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; }; } | { + /** + * Attachment type discriminator + */ type: "selection"; + /** + * Absolute path to the file containing the selection + */ filePath: string; + /** + * User-facing display name for the selection + */ displayName: string; + /** + * The selected text content + */ text: string; + /** + * Position range of the selection within the file + */ selection: { start: { + /** + * Start line number (0-based) + */ line: number; + /** + * Start character offset within the line (0-based) + */ character: number; }; end: { + /** + * End line number (0-based) + */ line: number; + /** + * End character offset within the line (0-based) + */ character: number; }; }; } | { + /** + * Attachment type discriminator + */ type: "github_reference"; + /** + * Issue, pull request, or discussion number + */ number: number; + /** + * Title of the referenced item + */ title: string; + /** + * Type of GitHub reference + */ referenceType: "issue" | "pr" | "discussion"; + /** + * Current state of the referenced item (e.g., open, closed, merged) + */ state: string; + /** + * URL to the referenced item on GitHub + */ url: string; } )[]; + /** + * 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; + /** + * The agent mode that was active when this message was sent + */ agentMode?: "interactive" | "plan" | "autopilot" | "shell"; + /** + * CAPI interaction ID for correlating this user message with its turn + */ interactionId?: 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: "pending_messages.modified"; + /** + * Empty payload; the event signals that the pending message queue has 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "assistant.turn_start"; data: { + /** + * Identifier for this turn within the agentic loop, typically a stringified turn number + */ turnId: string; + /** + * CAPI interaction ID for correlating this turn with upstream telemetry + */ interactionId?: 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: "assistant.intent"; data: { + /** + * Short description of what the agent is currently doing or planning to do + */ intent: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "assistant.reasoning"; data: { + /** + * Unique identifier for this reasoning block + */ reasoningId: string; + /** + * The complete extended thinking text from the model + */ content: 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: "assistant.reasoning_delta"; data: { + /** + * Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning event + */ reasoningId: string; + /** + * Incremental text chunk to append to the reasoning content + */ deltaContent: 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: "assistant.streaming_delta"; data: { + /** + * Cumulative total bytes received from the streaming response so far + */ totalResponseSizeBytes: number; }; } | { + /** + * 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: "assistant.message"; data: { + /** + * Unique identifier for this assistant message + */ messageId: string; + /** + * The assistant's text response content + */ content: string; + /** + * Tool invocations requested by the assistant in this message + */ toolRequests?: { + /** + * Unique identifier for this tool call + */ toolCallId: string; + /** + * Name of the tool being invoked + */ name: string; - arguments?: unknown; + /** + * Arguments to pass to the tool, format depends on the tool + */ + arguments?: { + [k: string]: unknown; + }; + /** + * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. + */ type?: "function" | "custom"; }[]; + /** + * Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped on resume. + */ reasoningOpaque?: string; + /** + * Readable reasoning text from the model's extended thinking + */ reasoningText?: string; + /** + * Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume. + */ encryptedContent?: string; + /** + * Generation phase for phased-output models (e.g., thinking vs. response phases) + */ phase?: string; + /** + * Actual output token count from the API response (completion_tokens), used for accurate token accounting + */ + outputTokens?: number; + /** + * CAPI interaction ID for correlating this message with upstream telemetry + */ interactionId?: string; + /** + * Tool call ID of the parent tool invocation when this event originates from a sub-agent + */ parentToolCallId?: 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: "assistant.message_delta"; data: { + /** + * Message ID this delta belongs to, matching the corresponding assistant.message event + */ messageId: string; + /** + * Incremental text chunk to append to the message content + */ deltaContent: string; + /** + * Tool call ID of the parent tool invocation when this event originates from a sub-agent + */ parentToolCallId?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "assistant.turn_end"; data: { + /** + * Identifier of the turn that has ended, matching the corresponding assistant.turn_start event + */ turnId: 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: "assistant.usage"; data: { + /** + * Model identifier used for this API call + */ model: string; + /** + * Number of input tokens consumed + */ inputTokens?: number; + /** + * Number of output tokens produced + */ outputTokens?: number; + /** + * Number of tokens read from prompt cache + */ cacheReadTokens?: number; + /** + * Number of tokens written to prompt cache + */ cacheWriteTokens?: number; + /** + * Model multiplier cost for billing purposes + */ cost?: number; + /** + * Duration of the API call in milliseconds + */ duration?: number; + /** + * What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls + */ initiator?: string; + /** + * Completion ID from the model provider (e.g., chatcmpl-abc123) + */ apiCallId?: string; + /** + * GitHub request tracing ID (x-github-request-id header) for server-side log correlation + */ providerCallId?: string; + /** + * Parent tool call ID when this usage originates from a sub-agent + */ parentToolCallId?: string; + /** + * Per-quota resource usage snapshots, keyed by quota identifier + */ quotaSnapshots?: { [k: string]: { + /** + * Whether the user has an unlimited usage entitlement + */ isUnlimitedEntitlement: boolean; + /** + * Total requests allowed by the entitlement + */ entitlementRequests: number; + /** + * Number of requests already consumed + */ usedRequests: number; + /** + * Whether usage is still permitted after quota exhaustion + */ usageAllowedWithExhaustedQuota: boolean; + /** + * Number of requests over the entitlement limit + */ overage: number; + /** + * Whether overage is allowed when quota is exhausted + */ overageAllowedWithExhaustedQuota: boolean; + /** + * Percentage of quota remaining (0.0 to 1.0) + */ remainingPercentage: number; + /** + * Date when the quota resets + */ resetDate?: string; }; }; + /** + * Per-request cost and usage data from the CAPI copilot_usage response field + */ copilotUsage?: { + /** + * Itemized token usage breakdown + */ tokenDetails: { + /** + * Number of tokens in this billing batch + */ batchSize: number; + /** + * Cost per batch of tokens + */ costPerBatch: number; + /** + * Total token count for this entry + */ tokenCount: number; + /** + * Token category (e.g., "input", "output") + */ tokenType: string; }[]; + /** + * Total cost in nano-AIU (AI Units) for this request + */ totalNanoAiu: number; }; }; } | { + /** + * 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: "abort"; data: { + /** + * Reason the current turn was aborted (e.g., "user initiated") + */ reason: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "tool.user_requested"; data: { + /** + * Unique identifier for this tool call + */ toolCallId: string; + /** + * Name of the tool the user wants to invoke + */ toolName: string; - arguments?: unknown; + /** + * Arguments for the tool invocation + */ + arguments?: { + [k: string]: unknown; + }; }; } | { + /** + * 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: "tool.execution_start"; data: { + /** + * Unique identifier for this tool call + */ toolCallId: string; + /** + * Name of the tool being executed + */ toolName: string; - arguments?: unknown; + /** + * Arguments passed to the tool + */ + arguments?: { + [k: string]: unknown; + }; + /** + * Name of the MCP server hosting this tool, when the tool is an MCP tool + */ mcpServerName?: string; + /** + * Original tool name on the MCP server, when the tool is an MCP tool + */ mcpToolName?: string; + /** + * Tool call ID of the parent tool invocation when this event originates from a sub-agent + */ parentToolCallId?: 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: "tool.execution_partial_result"; data: { + /** + * Tool call ID this partial result belongs to + */ toolCallId: string; + /** + * Incremental output chunk from the running tool + */ partialOutput: 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: "tool.execution_progress"; data: { + /** + * Tool call ID this progress notification belongs to + */ toolCallId: string; + /** + * Human-readable progress status message (e.g., from an MCP server) + */ progressMessage: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "tool.execution_complete"; data: { + /** + * Unique identifier for the completed tool call + */ toolCallId: string; + /** + * Whether the tool execution completed successfully + */ success: boolean; + /** + * Model identifier that generated this tool call + */ model?: string; + /** + * CAPI interaction ID for correlating this tool execution with upstream telemetry + */ interactionId?: string; + /** + * Whether this tool call was explicitly requested by the user rather than the assistant + */ isUserRequested?: boolean; + /** + * Tool execution result on success + */ result?: { + /** + * Concise tool result text sent to the LLM for chat completion, potentially truncated for token efficiency + */ content: string; + /** + * Full detailed tool result for UI/timeline display, preserving complete content such as diffs. Falls back to content when absent. + */ detailedContent?: string; + /** + * Structured content blocks (text, images, audio, resources) returned by the tool in their native format + */ contents?: ( | { + /** + * Content block type discriminator + */ type: "text"; + /** + * The text content + */ text: string; } | { + /** + * Content block type discriminator + */ type: "terminal"; + /** + * Terminal/shell output text + */ text: string; + /** + * Process exit code, if the command has completed + */ exitCode?: number; + /** + * Working directory where the command was executed + */ cwd?: string; } | { + /** + * Content block type discriminator + */ type: "image"; + /** + * Base64-encoded image data + */ data: string; + /** + * MIME type of the image (e.g., image/png, image/jpeg) + */ mimeType: string; } | { + /** + * Content block type discriminator + */ type: "audio"; + /** + * Base64-encoded audio data + */ data: string; + /** + * MIME type of the audio (e.g., audio/wav, audio/mpeg) + */ mimeType: string; } | { + /** + * Icons associated with this resource + */ icons?: { + /** + * URL or path to the icon image + */ src: string; + /** + * MIME type of the icon image + */ mimeType?: string; + /** + * Available icon sizes (e.g., ['16x16', '32x32']) + */ sizes?: string[]; + /** + * Theme variant this icon is intended for + */ theme?: "light" | "dark"; }[]; + /** + * Resource name identifier + */ name: string; + /** + * Human-readable display title for the resource + */ title?: string; + /** + * URI identifying the resource + */ uri: string; + /** + * Human-readable description of the resource + */ description?: string; + /** + * MIME type of the resource content + */ mimeType?: string; + /** + * Size of the resource in bytes + */ size?: number; + /** + * Content block type discriminator + */ type: "resource_link"; } | { + /** + * Content block type discriminator + */ type: "resource"; + /** + * The embedded resource contents, either text or base64-encoded binary + */ resource: | { + /** + * URI identifying the resource + */ uri: string; + /** + * MIME type of the text content + */ mimeType?: string; + /** + * Text content of the resource + */ text: string; } | { + /** + * URI identifying the resource + */ uri: string; + /** + * MIME type of the blob content + */ mimeType?: string; + /** + * Base64-encoded binary content of the resource + */ blob: string; }; } )[]; }; + /** + * Error details when the tool execution failed + */ error?: { + /** + * Human-readable error message + */ message: string; + /** + * Machine-readable error code + */ code?: string; }; + /** + * Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) + */ toolTelemetry?: { [k: string]: unknown; }; + /** + * Tool call ID of the parent tool invocation when this event originates from a sub-agent + */ parentToolCallId?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "skill.invoked"; data: { + /** + * Name of the invoked skill + */ name: string; + /** + * File path to the SKILL.md definition + */ path: string; + /** + * Full content of the skill file, injected into the conversation for the model + */ content: string; + /** + * Tool names that should be auto-approved when this skill is active + */ allowedTools?: string[]; + /** + * Name of the plugin this skill originated from, when applicable + */ pluginName?: string; + /** + * Version of the plugin this skill originated from, when applicable + */ pluginVersion?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "subagent.started"; data: { + /** + * Tool call ID of the parent tool invocation that spawned this sub-agent + */ toolCallId: string; + /** + * Internal name of the sub-agent + */ agentName: string; + /** + * Human-readable display name of the sub-agent + */ agentDisplayName: string; + /** + * Description of what the sub-agent does + */ agentDescription: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "subagent.completed"; data: { + /** + * Tool call ID of the parent tool invocation that spawned this sub-agent + */ toolCallId: string; + /** + * Internal name of the sub-agent + */ agentName: string; + /** + * Human-readable display name of the sub-agent + */ agentDisplayName: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "subagent.failed"; data: { + /** + * Tool call ID of the parent tool invocation that spawned this sub-agent + */ toolCallId: string; + /** + * Internal name of the sub-agent + */ agentName: string; + /** + * Human-readable display name of the sub-agent + */ agentDisplayName: string; + /** + * Error message describing why the sub-agent failed + */ 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "subagent.selected"; data: { + /** + * Internal name of the selected custom agent + */ agentName: string; + /** + * Human-readable display name of the selected custom agent + */ agentDisplayName: string; + /** + * List of tool names available to this agent, or null for all tools + */ tools: string[] | null; }; } | { + /** + * 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: "subagent.deselected"; + /** + * Empty payload; the event signals that the custom agent was deselected, returning to the default agent + */ 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "hook.start"; data: { + /** + * Unique identifier for this hook invocation + */ hookInvocationId: string; + /** + * Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + */ hookType: string; - input?: unknown; + /** + * Input data passed to the hook + */ + input?: { + [k: string]: unknown; + }; }; } | { + /** + * 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: "hook.end"; data: { + /** + * Identifier matching the corresponding hook.start event + */ hookInvocationId: string; + /** + * Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + */ hookType: string; - output?: unknown; + /** + * Output data produced by the hook + */ + output?: { + [k: string]: unknown; + }; + /** + * Whether the hook completed successfully + */ success: boolean; + /** + * Error details when the hook failed + */ error?: { + /** + * Human-readable error message + */ message: string; + /** + * Error stack trace, when available + */ stack?: 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; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ ephemeral?: boolean; type: "system.message"; data: { + /** + * The system or developer prompt text + */ content: string; + /** + * Message role: "system" for system prompts, "developer" for developer-injected instructions + */ role: "system" | "developer"; + /** + * Optional name identifier for the message source + */ name?: string; + /** + * Metadata about the prompt template and its construction + */ metadata?: { + /** + * Version identifier of the prompt template used + */ promptVersion?: string; + /** + * Template variables used when constructing the prompt + */ variables?: { [k: string]: unknown; }; @@ -753,136 +2153,555 @@ 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; ephemeral: true; type: "permission.requested"; data: { + /** + * Unique identifier for this permission request; used to respond via session.respondToPermission() + */ requestId: string; + /** + * Details of the permission being requested + */ permissionRequest: | { + /** + * Permission kind discriminator + */ kind: "shell"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * The complete shell command text to be executed + */ fullCommandText: string; + /** + * Human-readable description of what the command intends to do + */ intention: string; + /** + * Parsed command identifiers found in the command text + */ commands: { + /** + * Command identifier (e.g., executable name) + */ identifier: string; + /** + * Whether this command is read-only (no side effects) + */ readOnly: boolean; }[]; + /** + * File paths that may be read or written by the command + */ possiblePaths: string[]; + /** + * URLs that may be accessed by the command + */ possibleUrls: { + /** + * URL that may be accessed by the command + */ url: string; }[]; + /** + * Whether the command includes a file write redirection (e.g., > or >>) + */ hasWriteFileRedirection: boolean; + /** + * Whether the UI can offer session-wide approval for this command pattern + */ canOfferSessionApproval: boolean; + /** + * Optional warning message about risks of running this command + */ warning?: string; } | { + /** + * Permission kind discriminator + */ kind: "write"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Human-readable description of the intended file change + */ intention: string; + /** + * Path of the file being written to + */ fileName: string; + /** + * Unified diff showing the proposed changes + */ diff: string; + /** + * Complete new file contents for newly created files + */ newFileContents?: string; } | { + /** + * Permission kind discriminator + */ kind: "read"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Human-readable description of why the file is being read + */ intention: string; + /** + * Path of the file or directory being read + */ path: string; } | { + /** + * Permission kind discriminator + */ kind: "mcp"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Name of the MCP server providing the tool + */ serverName: string; + /** + * Internal name of the MCP tool + */ toolName: string; + /** + * Human-readable title of the MCP tool + */ toolTitle: string; - args?: unknown; + /** + * Arguments to pass to the MCP tool + */ + args?: { + [k: string]: unknown; + }; + /** + * Whether this MCP tool is read-only (no side effects) + */ readOnly: boolean; } | { + /** + * Permission kind discriminator + */ kind: "url"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Human-readable description of why the URL is being accessed + */ intention: string; + /** + * URL to be fetched + */ url: string; } | { + /** + * Permission kind discriminator + */ kind: "memory"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Topic or subject of the memory being stored + */ subject: string; + /** + * The fact or convention being stored + */ fact: string; + /** + * Source references for the stored fact + */ citations: string; } | { + /** + * Permission kind discriminator + */ kind: "custom-tool"; + /** + * Tool call ID that triggered this permission request + */ toolCallId?: string; + /** + * Name of the custom tool + */ toolName: string; + /** + * Description of what the custom tool does + */ toolDescription: string; - args?: unknown; + /** + * Arguments to pass to the custom tool + */ + args?: { + [k: string]: unknown; + }; }; }; } | { + /** + * 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: "permission.completed"; data: { + /** + * Request ID of the resolved permission request; clients should dismiss any UI for this request + */ requestId: string; + /** + * The result of the permission request + */ + result: { + /** + * The outcome of the permission request + */ + kind: + | "approved" + | "denied-by-rules" + | "denied-no-approval-rule-and-could-not-request-from-user" + | "denied-interactively-by-user" + | "denied-by-content-exclusion-policy"; + }; }; } | { + /** + * 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: "user_input.requested"; data: { + /** + * Unique identifier for this input request; used to respond via session.respondToUserInput() + */ requestId: string; + /** + * The question or prompt to present to the user + */ question: string; + /** + * Predefined choices for the user to select from, if applicable + */ choices?: string[]; + /** + * Whether the user can provide a free-form text response in addition to predefined choices + */ allowFreeform?: 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: "user_input.completed"; data: { + /** + * Request ID of the resolved user input 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: "elicitation.requested"; data: { + /** + * Unique identifier for this elicitation request; used to respond via session.respondToElicitation() + */ requestId: string; + /** + * Message describing what information is needed from the user + */ message: string; + /** + * Elicitation mode; currently only "form" is supported. Defaults to "form" when absent. + */ mode?: "form"; + /** + * JSON Schema describing the form fields to present to the user + */ requestedSchema: { type: "object"; + /** + * Form field definitions, keyed by field name + */ properties: { [k: string]: unknown; }; + /** + * List of required field names + */ required?: string[]; }; [k: string]: unknown; }; } | { + /** + * 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: "elicitation.completed"; data: { + /** + * Request ID of the resolved elicitation 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: "external_tool.requested"; + data: { + /** + * Unique identifier for this request; used to respond via session.respondToExternalTool() + */ + requestId: string; + /** + * Session ID that this external tool request belongs to + */ + sessionId: string; + /** + * Tool call ID assigned to this external tool invocation + */ + toolCallId: string; + /** + * Name of the external tool to invoke + */ + toolName: string; + /** + * Arguments to pass to the external tool + */ + arguments?: { + [k: string]: unknown; + }; + }; + } + | { + /** + * 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: "external_tool.completed"; + data: { + /** + * Request ID of the resolved external tool 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: "command.queued"; + data: { + /** + * Unique identifier for this request; used to respond via session.respondToQueuedCommand() + */ + requestId: string; + /** + * The slash command text to be executed (e.g., /help, /clear) + */ + 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.completed"; + data: { + /** + * Request ID of the resolved command 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: "exit_plan_mode.requested"; + data: { + /** + * Unique identifier for this request; used to respond via session.respondToExitPlanMode() + */ + requestId: string; + /** + * Summary of the plan that was created + */ + summary: string; + /** + * Full content of the plan file + */ + planContent: string; + /** + * Available actions the user can take (e.g., approve, edit, reject) + */ + actions: string[]; + /** + * The recommended action for the user to take + */ + recommendedAction: 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: "exit_plan_mode.completed"; + data: { + /** + * Request ID of the resolved exit plan mode request; clients should dismiss any UI for this request + */ requestId: string; }; }; diff --git a/nodejs/src/sdkProtocolVersion.ts b/nodejs/src/sdkProtocolVersion.ts index 9485bc00..ce4e8b1a 100644 --- a/nodejs/src/sdkProtocolVersion.ts +++ b/nodejs/src/sdkProtocolVersion.ts @@ -5,15 +5,29 @@ // Code generated by update-protocol-version.ts. DO NOT EDIT. /** - * The SDK protocol version. + * The maximum SDK protocol version supported. * This must match the version expected by the copilot-agent-runtime server. */ -export const SDK_PROTOCOL_VERSION = 2; +export const SDK_PROTOCOL_VERSION = 3; /** - * Gets the SDK protocol version. + * The minimum SDK protocol version supported. + * Servers reporting a version in [MIN, MAX] are considered compatible. + */ +export const MIN_SDK_PROTOCOL_VERSION = 2; + +/** + * Gets the SDK protocol version (maximum supported). * @returns The protocol version number */ export function getSdkProtocolVersion(): number { return SDK_PROTOCOL_VERSION; } + +/** + * Gets the minimum SDK protocol version supported. + * @returns The minimum protocol version number + */ +export function getMinSdkProtocolVersion(): number { + return MIN_SDK_PROTOCOL_VERSION; +} diff --git a/python/copilot/client.py b/python/copilot/client.py index 782abcd6..4345c795 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -27,7 +27,7 @@ from .generated.rpc import ServerRpc from .generated.session_events import session_event_from_dict from .jsonrpc import JsonRpcClient, ProcessExitedError -from .sdk_protocol_version import get_sdk_protocol_version +from .sdk_protocol_version import get_min_sdk_protocol_version, get_sdk_protocol_version from .session import CopilotSession from .types import ( ConnectionState, @@ -211,6 +211,7 @@ def __init__(self, options: CopilotClientOptions | None = None): ] = {} self._lifecycle_handlers_lock = threading.Lock() self._rpc: ServerRpc | None = None + self._negotiated_protocol_version: int = 0 @property def rpc(self) -> ServerRpc: @@ -1126,25 +1127,30 @@ def _dispatch_lifecycle_event(self, event: SessionLifecycleEvent) -> None: pass # Ignore handler errors async def _verify_protocol_version(self) -> None: - """Verify that the server's protocol version matches the SDK's expected version.""" - expected_version = get_sdk_protocol_version() + """Verify that the server's protocol version is within the supported range.""" + max_version = get_sdk_protocol_version() + min_version = get_min_sdk_protocol_version() ping_result = await self.ping() server_version = ping_result.protocolVersion if server_version is None: raise RuntimeError( - f"SDK protocol version mismatch: SDK expects version {expected_version}, " - f"but server does not report a protocol version. " - f"Please update your server to ensure compatibility." + "SDK protocol version mismatch: " + f"SDK supports versions {min_version}-{max_version}, " + "but server does not report a protocol version. " + "Please update your server to ensure compatibility." ) - if server_version != expected_version: + if server_version < min_version or server_version > max_version: raise RuntimeError( - f"SDK protocol version mismatch: SDK expects version {expected_version}, " + "SDK protocol version mismatch: " + f"SDK supports versions {min_version}-{max_version}, " f"but server reports version {server_version}. " - f"Please update your SDK or server to ensure compatibility." + "Please update your SDK or server to ensure compatibility." ) + self._negotiated_protocol_version = server_version + def _convert_provider_to_wire_format( self, provider: ProviderConfig | dict[str, Any] ) -> dict[str, Any]: @@ -1342,12 +1348,27 @@ def handle_notification(method: str, params: dict): if method == "session.event": session_id = params["sessionId"] event_dict = params["event"] - # Convert dict to SessionEvent object - event = session_event_from_dict(event_dict) + # Convert dict to SessionEvent object (skip unknown event types) + try: + event = session_event_from_dict(event_dict) + except Exception: + event = None with self._sessions_lock: session = self._sessions.get(session_id) - if session: + if session and event: session._dispatch_event(event) + + # v3 protocol: intercept tool/permission broadcast events + if self._negotiated_protocol_version >= 3: + event_type = event_dict.get("type") + if event_type == "external_tool.requested": + asyncio.ensure_future( + self._handle_external_tool_requested(session_id, event_dict) + ) + elif event_type == "permission.requested": + asyncio.ensure_future( + self._handle_permission_requested_event(session_id, event_dict) + ) elif method == "session.lifecycle": # Handle session lifecycle events lifecycle_event = SessionLifecycleEvent.from_dict(params) @@ -1424,11 +1445,26 @@ def handle_notification(method: str, params: dict): if method == "session.event": session_id = params["sessionId"] event_dict = params["event"] - # Convert dict to SessionEvent object - event = session_event_from_dict(event_dict) + # Convert dict to SessionEvent object (skip unknown event types) + try: + event = session_event_from_dict(event_dict) + except Exception: + event = None session = self._sessions.get(session_id) - if session: + if session and event: session._dispatch_event(event) + + # v3 protocol: intercept tool/permission broadcast events + if self._negotiated_protocol_version >= 3: + event_type = event_dict.get("type") + if event_type == "external_tool.requested": + asyncio.ensure_future( + self._handle_external_tool_requested(session_id, event_dict) + ) + elif event_type == "permission.requested": + asyncio.ensure_future( + self._handle_permission_requested_event(session_id, event_dict) + ) elif method == "session.lifecycle": # Handle session lifecycle events lifecycle_event = SessionLifecycleEvent.from_dict(params) @@ -1479,6 +1515,111 @@ async def _handle_permission_request(self, params: dict) -> dict: } } + async def _handle_external_tool_requested(self, session_id: str, event_dict: dict) -> None: + """Handle a v3 external_tool.requested broadcast event. + + Extracts tool call info from the event, executes the tool, and sends + the result back via session.tools.handlePendingToolCall RPC. + """ + try: + data = event_dict.get("data", {}) + request_id = data.get("requestId") + tool_call_id = data.get("toolCallId") + tool_name = data.get("toolName") + + if not request_id or not tool_call_id or not tool_name: + return + + with self._sessions_lock: + session = self._sessions.get(session_id) + if not session: + return + + handler = session._get_tool_handler(tool_name) + if not handler: + result = self._build_unsupported_tool_result(tool_name) + else: + arguments = data.get("arguments") + result = await self._execute_tool_call( + session_id, tool_call_id, tool_name, arguments, handler + ) + + if not self._client: + return + await self._client.request( + "session.tools.handlePendingToolCall", + { + "sessionId": session_id, + "requestId": request_id, + "result": result, + }, + ) + except Exception: # pylint: disable=broad-except + # Best-effort: try to send an error response back + try: + data = event_dict.get("data", {}) + request_id = data.get("requestId") + if request_id and self._client: + await self._client.request( + "session.tools.handlePendingToolCall", + { + "sessionId": session_id, + "requestId": request_id, + "error": "tool execution failed", + }, + ) + except Exception: # pylint: disable=broad-except + pass + + async def _handle_permission_requested_event(self, session_id: str, event_dict: dict) -> None: + """Handle a v3 permission.requested broadcast event. + + Extracts permission info from the event, runs the permission handler, + and sends the result back via session.permissions.handlePendingPermissionRequest RPC. + """ + try: + data = event_dict.get("data", {}) + request_id = data.get("requestId") + permission_request = data.get("permissionRequest") + + if not request_id or not permission_request: + return + + with self._sessions_lock: + session = self._sessions.get(session_id) + if not session: + return + + result = await session._handle_permission_request(permission_request) + if not self._client: + return + await self._client.request( + "session.permissions.handlePendingPermissionRequest", + { + "sessionId": session_id, + "requestId": request_id, + "result": result, + }, + ) + except Exception: # pylint: disable=broad-except + # On error, send a denial response + try: + data = event_dict.get("data", {}) + request_id = data.get("requestId") + if request_id and self._client: + await self._client.request( + "session.permissions.handlePendingPermissionRequest", + { + "sessionId": session_id, + "requestId": request_id, + "result": { + "kind": "denied-no-approval-rule-and-could-not-request-from-user", + }, + }, + ) + except Exception: # pylint: disable=broad-except + pass + async def _handle_user_input_request(self, params: dict) -> dict: """ Handle a user input request from the CLI server. diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ed199f13..504c806d 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -547,22 +547,27 @@ def to_dict(self) -> dict: @dataclass class SessionPlanReadResult: exists: bool - """Whether plan.md exists in the workspace""" + """Whether the plan file exists in the workspace""" content: str | None = None - """The content of plan.md, or null if it does not exist""" + """The content of the plan file, or null if it does not exist""" + + path: str | None = None + """Absolute file path of the plan file, or null if workspace is not enabled""" @staticmethod def from_dict(obj: Any) -> 'SessionPlanReadResult': assert isinstance(obj, dict) exists = from_bool(obj.get("exists")) content = from_union([from_none, from_str], obj.get("content")) - return SessionPlanReadResult(exists, content) + path = from_union([from_none, from_str], obj.get("path")) + return SessionPlanReadResult(exists, content, path) def to_dict(self) -> dict: result: dict = {} result["exists"] = from_bool(self.exists) result["content"] = from_union([from_none, from_str], self.content) + result["path"] = from_union([from_none, from_str], self.path) return result @@ -581,7 +586,7 @@ def to_dict(self) -> dict: @dataclass class SessionPlanUpdateParams: content: str - """The new content for plan.md""" + """The new content for the plan file""" @staticmethod def from_dict(obj: Any) -> 'SessionPlanUpdateParams': @@ -917,6 +922,149 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionToolsHandlePendingToolCallResult: + success: bool + + @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 SessionPermissionsHandlePendingPermissionRequestResult: + success: bool + + @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 + + def ping_result_from_dict(s: Any) -> PingResult: return PingResult.from_dict(s) @@ -1149,6 +1297,38 @@ def session_compaction_compact_result_to_dict(x: SessionCompactionCompactResult) return to_class(SessionCompactionCompactResult, x) +def session_tools_handle_pending_tool_call_result_from_dict(s: Any) -> SessionToolsHandlePendingToolCallResult: + return SessionToolsHandlePendingToolCallResult.from_dict(s) + + +def session_tools_handle_pending_tool_call_result_to_dict(x: SessionToolsHandlePendingToolCallResult) -> Any: + return to_class(SessionToolsHandlePendingToolCallResult, x) + + +def session_tools_handle_pending_tool_call_params_from_dict(s: Any) -> SessionToolsHandlePendingToolCallParams: + return SessionToolsHandlePendingToolCallParams.from_dict(s) + + +def session_tools_handle_pending_tool_call_params_to_dict(x: SessionToolsHandlePendingToolCallParams) -> Any: + return to_class(SessionToolsHandlePendingToolCallParams, x) + + +def session_permissions_handle_pending_permission_request_result_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestResult: + return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(s) + + +def session_permissions_handle_pending_permission_request_result_to_dict(x: SessionPermissionsHandlePendingPermissionRequestResult) -> Any: + return to_class(SessionPermissionsHandlePendingPermissionRequestResult, x) + + +def session_permissions_handle_pending_permission_request_params_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestParams: + return SessionPermissionsHandlePendingPermissionRequestParams.from_dict(s) + + +def session_permissions_handle_pending_permission_request_params_to_dict(x: SessionPermissionsHandlePendingPermissionRequestParams) -> Any: + return to_class(SessionPermissionsHandlePendingPermissionRequestParams, x) + + def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" if timeout is not None: @@ -1298,6 +1478,28 @@ async def compact(self, *, timeout: float | None = None) -> SessionCompactionCom return SessionCompactionCompactResult.from_dict(await self._client.request("session.compaction.compact", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) +class ToolsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def handle_pending_tool_call(self, params: SessionToolsHandlePendingToolCallParams, *, timeout: float | None = None) -> SessionToolsHandlePendingToolCallResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionToolsHandlePendingToolCallResult.from_dict(await self._client.request("session.tools.handlePendingToolCall", params_dict, **_timeout_kwargs(timeout))) + + +class PermissionsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def handle_pending_permission_request(self, params: SessionPermissionsHandlePendingPermissionRequestParams, *, timeout: float | None = None) -> SessionPermissionsHandlePendingPermissionRequestResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(await self._client.request("session.permissions.handlePendingPermissionRequest", params_dict, **_timeout_kwargs(timeout))) + + class SessionRpc: """Typed session-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient", session_id: str): @@ -1310,4 +1512,6 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.fleet = FleetApi(client, session_id) self.agent = AgentApi(client, session_id) self.compaction = CompactionApi(client, session_id) + self.tools = ToolsApi(client, session_id) + self.permissions = PermissionsApi(client, session_id) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 74d3c64d..1b442530 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -80,6 +80,8 @@ def from_int(x: Any) -> int: class AgentMode(Enum): + """The agent mode that was active when this message was sent""" + AUTOPILOT = "autopilot" INTERACTIVE = "interactive" PLAN = "plan" @@ -88,8 +90,13 @@ class AgentMode(Enum): @dataclass class LineRange: + """Optional line range to scope the attachment to a specific section of the file""" + end: float + """End line number (1-based, inclusive)""" + start: float + """Start line number (1-based)""" @staticmethod def from_dict(obj: Any) -> 'LineRange': @@ -106,6 +113,8 @@ def to_dict(self) -> dict: class ReferenceType(Enum): + """Type of GitHub reference""" + DISCUSSION = "discussion" ISSUE = "issue" PR = "pr" @@ -114,7 +123,10 @@ class ReferenceType(Enum): @dataclass class End: character: float + """End character offset within the line (0-based)""" + line: float + """End line number (0-based)""" @staticmethod def from_dict(obj: Any) -> 'End': @@ -133,7 +145,10 @@ def to_dict(self) -> dict: @dataclass class Start: character: float + """Start character offset within the line (0-based)""" + line: float + """Start line number (0-based)""" @staticmethod def from_dict(obj: Any) -> 'Start': @@ -151,6 +166,8 @@ def to_dict(self) -> dict: @dataclass class Selection: + """Position range of the selection within the file""" + end: End start: Start @@ -178,17 +195,42 @@ class AttachmentType(Enum): @dataclass class Attachment: type: AttachmentType + """Attachment type discriminator""" + display_name: str | None = None + """User-facing display name for the attachment + + User-facing display name for the selection + """ line_range: LineRange | None = None + """Optional line range to scope the attachment to a specific section of the file""" + path: str | None = None + """Absolute file or directory path""" + file_path: str | None = None + """Absolute path to the file containing the selection""" + selection: Selection | None = None + """Position range of the selection within the file""" + text: str | None = None + """The selected text content""" + number: float | None = None + """Issue, pull request, or discussion number""" + reference_type: ReferenceType | None = None + """Type of GitHub reference""" + state: str | None = None + """Current state of the referenced item (e.g., open, closed, merged)""" + title: str | None = None + """Title of the referenced item""" + url: str | None = None + """URL to the referenced item on GitHub""" @staticmethod def from_dict(obj: Any) -> 'Attachment': @@ -235,11 +277,93 @@ def to_dict(self) -> dict: return result +@dataclass +class Agent: + agent_id: str + """Unique identifier of the background agent""" + + agent_type: str + """Type of the background agent""" + + description: str | None = None + """Human-readable description of the agent task""" + + @staticmethod + def from_dict(obj: Any) -> 'Agent': + assert isinstance(obj, dict) + agent_id = from_str(obj.get("agentId")) + agent_type = from_str(obj.get("agentType")) + description = from_union([from_str, from_none], obj.get("description")) + return Agent(agent_id, agent_type, description) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + result["agentType"] = from_str(self.agent_type) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + return result + + +@dataclass +class Shell: + shell_id: str + """Unique identifier of the background shell""" + + description: str | None = None + """Human-readable description of the shell command""" + + @staticmethod + def from_dict(obj: Any) -> 'Shell': + assert isinstance(obj, dict) + shell_id = from_str(obj.get("shellId")) + description = from_union([from_str, from_none], obj.get("description")) + return Shell(shell_id, description) + + def to_dict(self) -> dict: + result: dict = {} + result["shellId"] = from_str(self.shell_id) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + return result + + +@dataclass +class BackgroundTasks: + """Background tasks still running when the agent became idle""" + + agents: list[Agent] + """Currently running background agents""" + + shells: list[Shell] + """Currently running background shell commands""" + + @staticmethod + def from_dict(obj: Any) -> 'BackgroundTasks': + assert isinstance(obj, dict) + agents = from_list(Agent.from_dict, obj.get("agents")) + shells = from_list(Shell.from_dict, obj.get("shells")) + return BackgroundTasks(agents, shells) + + def to_dict(self) -> dict: + result: dict = {} + result["agents"] = from_list(lambda x: to_class(Agent, x), self.agents) + result["shells"] = from_list(lambda x: to_class(Shell, x), self.shells) + return result + + @dataclass class CodeChanges: + """Aggregate code change metrics for the session""" + files_modified: list[str] + """List of file paths that were modified during the session""" + lines_added: float + """Total number of lines added during the session""" + lines_removed: float + """Total number of lines removed during the session""" @staticmethod def from_dict(obj: Any) -> 'CodeChanges': @@ -259,9 +383,16 @@ def to_dict(self) -> dict: @dataclass class CompactionTokensUsed: + """Token usage breakdown for the compaction LLM call""" + cached_input: float + """Cached input tokens reused in the compaction LLM call""" + input: float + """Input tokens consumed by the compaction LLM call""" + output: float + """Output tokens produced by the compaction LLM call""" @staticmethod def from_dict(obj: Any) -> 'CompactionTokensUsed': @@ -281,10 +412,21 @@ def to_dict(self) -> dict: @dataclass class ContextClass: + """Working directory and git context at session start + + Updated working directory and git context at resume time + """ cwd: str + """Current working directory path""" + branch: str | None = None + """Current git branch name""" + git_root: str | None = None + """Root directory of the git repository, resolved via git rev-parse""" + repository: str | None = None + """Repository identifier in "owner/name" format, derived from the git remote URL""" @staticmethod def from_dict(obj: Any) -> 'ContextClass': @@ -310,9 +452,16 @@ def to_dict(self) -> dict: @dataclass class TokenDetail: batch_size: float + """Number of tokens in this billing batch""" + cost_per_batch: float + """Cost per batch of tokens""" + token_count: float + """Total token count for this entry""" + token_type: str + """Token category (e.g., "input", "output")""" @staticmethod def from_dict(obj: Any) -> 'TokenDetail': @@ -334,8 +483,13 @@ def to_dict(self) -> dict: @dataclass class CopilotUsage: + """Per-request cost and usage data from the CAPI copilot_usage response field""" + token_details: list[TokenDetail] + """Itemized token usage breakdown""" + total_nano_aiu: float + """Total cost in nano-AIU (AI Units) for this request""" @staticmethod def from_dict(obj: Any) -> 'CopilotUsage': @@ -353,9 +507,18 @@ def to_dict(self) -> dict: @dataclass class ErrorClass: + """Error details when the tool execution failed + + Error details when the hook failed + """ message: str + """Human-readable error message""" + code: str | None = None + """Machine-readable error code""" + stack: str | None = None + """Error stack trace, when available""" @staticmethod def from_dict(obj: Any) -> 'ErrorClass': @@ -377,8 +540,13 @@ def to_dict(self) -> dict: @dataclass class Metadata: + """Metadata about the prompt template and its construction""" + prompt_version: str | None = None + """Version identifier of the prompt template used""" + variables: dict[str, Any] | None = None + """Template variables used when constructing the prompt""" @staticmethod def from_dict(obj: Any) -> 'Metadata': @@ -402,8 +570,13 @@ class Mode(Enum): @dataclass class Requests: + """Request count and cost metrics""" + cost: float + """Cumulative cost multiplier for requests to this model""" + count: float + """Total number of API requests made to this model""" @staticmethod def from_dict(obj: Any) -> 'Requests': @@ -421,10 +594,19 @@ def to_dict(self) -> dict: @dataclass class Usage: + """Token usage breakdown""" + cache_read_tokens: float + """Total tokens read from prompt cache across all requests""" + cache_write_tokens: float + """Total tokens written to prompt cache across all requests""" + input_tokens: float + """Total input tokens consumed across all requests to this model""" + output_tokens: float + """Total output tokens produced across all requests to this model""" @staticmethod def from_dict(obj: Any) -> 'Usage': @@ -447,7 +629,10 @@ def to_dict(self) -> dict: @dataclass class ModelMetric: requests: Requests + """Request count and cost metrics""" + usage: Usage + """Token usage breakdown""" @staticmethod def from_dict(obj: Any) -> 'ModelMetric': @@ -464,6 +649,10 @@ def to_dict(self) -> dict: class Operation(Enum): + """The type of operation performed on the plan file + + Whether the file was newly created or updated + """ CREATE = "create" DELETE = "delete" UPDATE = "update" @@ -472,7 +661,10 @@ class Operation(Enum): @dataclass class Command: identifier: str + """Command identifier (e.g., executable name)""" + read_only: bool + """Whether this command is read-only (no side effects)""" @staticmethod def from_dict(obj: Any) -> 'Command': @@ -488,7 +680,7 @@ def to_dict(self) -> dict: return result -class Kind(Enum): +class PermissionRequestKind(Enum): CUSTOM_TOOL = "custom-tool" MCP = "mcp" MEMORY = "memory" @@ -501,6 +693,7 @@ class Kind(Enum): @dataclass class PossibleURL: url: str + """URL that may be accessed by the command""" @staticmethod def from_dict(obj: Any) -> 'PossibleURL': @@ -516,35 +709,94 @@ def to_dict(self) -> dict: @dataclass class PermissionRequest: - kind: Kind + """Details of the permission being requested""" + + kind: PermissionRequestKind + """Permission kind discriminator""" + can_offer_session_approval: bool | None = None + """Whether the UI can offer session-wide approval for this command pattern""" + commands: list[Command] | None = None + """Parsed command identifiers found in the command text""" + full_command_text: str | None = None + """The complete shell command text to be executed""" + has_write_file_redirection: bool | None = None + """Whether the command includes a file write redirection (e.g., > or >>)""" + intention: str | None = None + """Human-readable description of what the command intends to do + + Human-readable description of the intended file change + + Human-readable description of why the file is being read + + Human-readable description of why the URL is being accessed + """ possible_paths: list[str] | None = None + """File paths that may be read or written by the command""" + possible_urls: list[PossibleURL] | None = None + """URLs that may be accessed by the command""" + tool_call_id: str | None = None + """Tool call ID that triggered this permission request""" + warning: str | None = None + """Optional warning message about risks of running this command""" + diff: str | None = None + """Unified diff showing the proposed changes""" + file_name: str | None = None + """Path of the file being written to""" + new_file_contents: str | None = None + """Complete new file contents for newly created files""" + path: str | None = None + """Path of the file or directory being read""" + args: Any = None + """Arguments to pass to the MCP tool + + Arguments to pass to the custom tool + """ read_only: bool | None = None + """Whether this MCP tool is read-only (no side effects)""" + server_name: str | None = None + """Name of the MCP server providing the tool""" + tool_name: str | None = None + """Internal name of the MCP tool + + Name of the custom tool + """ tool_title: str | None = None + """Human-readable title of the MCP tool""" + url: str | None = None + """URL to be fetched""" + citations: str | None = None + """Source references for the stored fact""" + fact: str | None = None + """The fact or convention being stored""" + subject: str | None = None + """Topic or subject of the memory being stored""" + tool_description: str | None = None + """Description of what the custom tool does""" @staticmethod def from_dict(obj: Any) -> 'PermissionRequest': assert isinstance(obj, dict) - kind = Kind(obj.get("kind")) + 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")) full_command_text = from_union([from_str, from_none], obj.get("fullCommandText")) @@ -572,7 +824,7 @@ def from_dict(obj: Any) -> 'PermissionRequest': def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(Kind, self.kind) + result["kind"] = to_enum(PermissionRequestKind, self.kind) 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: @@ -625,13 +877,28 @@ def to_dict(self) -> dict: @dataclass class QuotaSnapshot: entitlement_requests: float + """Total requests allowed by the entitlement""" + is_unlimited_entitlement: bool + """Whether the user has an unlimited usage entitlement""" + overage: float + """Number of requests over the entitlement limit""" + overage_allowed_with_exhausted_quota: bool + """Whether overage is allowed when quota is exhausted""" + remaining_percentage: float + """Percentage of quota remaining (0.0 to 1.0)""" + usage_allowed_with_exhausted_quota: bool + """Whether usage is still permitted after quota exhaustion""" + used_requests: float + """Number of requests already consumed""" + reset_date: datetime | None = None + """Date when the quota resets""" @staticmethod def from_dict(obj: Any) -> 'QuotaSnapshot': @@ -662,9 +929,16 @@ def to_dict(self) -> dict: @dataclass class RepositoryClass: + """Repository context for the handed-off session""" + name: str + """Repository name""" + owner: str + """Repository owner (user or organization)""" + branch: str | None = None + """Git branch name, if applicable""" @staticmethod def from_dict(obj: Any) -> 'RepositoryClass': @@ -689,9 +963,14 @@ class RequestedSchemaType(Enum): @dataclass class RequestedSchema: + """JSON Schema describing the form fields to present to the user""" + properties: dict[str, Any] + """Form field definitions, keyed by field name""" + type: RequestedSchemaType required: list[str] | None = None + """List of required field names""" @staticmethod def from_dict(obj: Any) -> 'RequestedSchema': @@ -711,6 +990,8 @@ def to_dict(self) -> dict: class Theme(Enum): + """Theme variant this icon is intended for""" + DARK = "dark" LIGHT = "light" @@ -718,9 +999,16 @@ class Theme(Enum): @dataclass class Icon: src: str + """URL or path to the icon image""" + mime_type: str | None = None + """MIME type of the icon image""" + sizes: list[str] | None = None + """Available icon sizes (e.g., ['16x16', '32x32'])""" + theme: Theme | None = None + """Theme variant this icon is intended for""" @staticmethod def from_dict(obj: Any) -> 'Icon': @@ -745,10 +1033,21 @@ def to_dict(self) -> dict: @dataclass class Resource: + """The embedded resource contents, either text or base64-encoded binary""" + uri: str + """URI identifying the resource""" + mime_type: str | None = None + """MIME type of the text content + + MIME type of the blob content + """ text: str | None = None + """Text content of the resource""" + blob: str | None = None + """Base64-encoded binary content of the resource""" @staticmethod def from_dict(obj: Any) -> 'Resource': @@ -783,18 +1082,51 @@ class ContentType(Enum): @dataclass class Content: type: ContentType + """Content block type discriminator""" + text: str | None = None + """The text content + + Terminal/shell output text + """ cwd: str | None = None + """Working directory where the command was executed""" + exit_code: float | None = None + """Process exit code, if the command has completed""" + data: str | None = None + """Base64-encoded image data + + Base64-encoded audio data + """ mime_type: str | None = None + """MIME type of the image (e.g., image/png, image/jpeg) + + MIME type of the audio (e.g., audio/wav, audio/mpeg) + + MIME type of the resource content + """ description: str | None = None + """Human-readable description of the resource""" + icons: list[Icon] | None = None + """Icons associated with this resource""" + name: str | None = None + """Resource name identifier""" + size: float | None = None + """Size of the resource in bytes""" + title: str | None = None + """Human-readable display title for the resource""" + uri: str | None = None + """URI identifying the resource""" + resource: Resource | None = None + """The embedded resource contents, either text or base64-encoded binary""" @staticmethod def from_dict(obj: Any) -> 'Content': @@ -844,46 +1176,84 @@ def to_dict(self) -> dict: return result +class ResultKind(Enum): + """The outcome of the permission request""" + + 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 Result: - content: str + """Tool execution result on success + + The result of the permission request + """ + content: str | None = None + """Concise tool result text sent to the LLM for chat completion, potentially truncated for + token efficiency + """ contents: list[Content] | None = None + """Structured content blocks (text, images, audio, resources) returned by the tool in their + native format + """ detailed_content: str | None = None + """Full detailed tool result for UI/timeline display, preserving complete content such as + diffs. Falls back to content when absent. + """ + kind: ResultKind | None = None + """The outcome of the permission request""" @staticmethod def from_dict(obj: Any) -> 'Result': assert isinstance(obj, dict) - content = from_str(obj.get("content")) + content = from_union([from_str, from_none], obj.get("content")) contents = from_union([lambda x: from_list(Content.from_dict, x), from_none], obj.get("contents")) detailed_content = from_union([from_str, from_none], obj.get("detailedContent")) - return Result(content, contents, detailed_content) + kind = from_union([ResultKind, from_none], obj.get("kind")) + return Result(content, contents, detailed_content, kind) def to_dict(self) -> dict: result: dict = {} - result["content"] = from_str(self.content) + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) if self.contents is not None: result["contents"] = from_union([lambda x: from_list(lambda x: to_class(Content, x), x), from_none], self.contents) if self.detailed_content is not None: result["detailedContent"] = from_union([from_str, from_none], self.detailed_content) + if self.kind is not None: + result["kind"] = from_union([lambda x: to_enum(ResultKind, x), from_none], self.kind) return result class Role(Enum): + """Message role: "system" for system prompts, "developer" for developer-injected instructions""" + DEVELOPER = "developer" SYSTEM = "system" class ShutdownType(Enum): + """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" + ERROR = "error" ROUTINE = "routine" class SourceType(Enum): + """Origin type of the session being handed off""" + LOCAL = "local" REMOTE = "remote" class ToolRequestType(Enum): + """Tool call type: "function" for standard tool calls, "custom" for grammar-based tool + calls. Defaults to "function" when absent. + """ CUSTOM = "custom" FUNCTION = "function" @@ -891,9 +1261,18 @@ class ToolRequestType(Enum): @dataclass class ToolRequest: name: str + """Name of the tool being invoked""" + tool_call_id: str + """Unique identifier for this tool call""" + arguments: Any = None + """Arguments to pass to the tool, format depends on 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. + """ @staticmethod def from_dict(obj: Any) -> 'ToolRequest': @@ -917,131 +1296,532 @@ def to_dict(self) -> dict: @dataclass class Data: + """Payload indicating the agent is idle; includes any background tasks still in flight + + Empty payload; the event signals that LLM-powered conversation compaction has begun + + Empty payload; the event signals that the pending message queue has changed + + Empty payload; the event signals that the custom agent was deselected, returning to the + default agent + """ context: ContextClass | str | None = None + """Working directory and git context at session start + + Updated working directory and git context at resume time + + Additional context information for the handoff + """ copilot_version: str | None = None + """Version string of the Copilot application""" + producer: str | None = None + """Identifier of the software producing the events (e.g., "copilot-agent")""" + selected_model: str | None = None + """Model selected at session creation time, if any""" + session_id: str | None = None + """Unique identifier for the session + + Session ID that this external tool request belongs to + """ start_time: datetime | None = None + """ISO 8601 timestamp when the session was created""" + version: float | None = None + """Schema version number for the session event format""" + event_count: float | None = None + """Total number of persisted events in the session at the time of resume""" + resume_time: datetime | None = None + """ISO 8601 timestamp when the session was resumed""" + error_type: str | None = None + """Category of error (e.g., "authentication", "authorization", "quota", "rate_limit", + "query") + """ message: str | None = None + """Human-readable error message + + Human-readable informational message for display in the timeline + + Human-readable warning message for display in the timeline + + Message describing what information is needed from the user + """ provider_call_id: str | None = None + """GitHub request tracing ID (x-github-request-id header) for correlating with server-side + logs + + GitHub request tracing ID (x-github-request-id header) for server-side log correlation + """ stack: str | None = None + """Error stack trace, when available""" + status_code: int | None = None + """HTTP status code from the upstream request, if applicable""" + + background_tasks: BackgroundTasks | None = None + """Background tasks still running when the agent became idle""" + title: str | None = None + """The new display title for the session""" + info_type: str | None = None + """Category of informational message (e.g., "notification", "timing", "context_window", + "mcp", "snapshot", "configuration", "authentication", "model") + """ warning_type: str | None = None + """Category of warning (e.g., "subscription", "policy", "mcp")""" + new_model: str | None = None + """Newly selected model identifier""" + previous_model: str | None = None + """Model that was previously selected, if any""" + new_mode: str | None = None + """Agent mode after the change (e.g., "interactive", "plan", "autopilot")""" + previous_mode: str | None = None + """Agent mode before the change (e.g., "interactive", "plan", "autopilot")""" + operation: Operation | None = None + """The type of operation performed on the plan file + + Whether the file was newly created or updated + """ path: str | None = None - """Relative path within the workspace files directory""" - + """Relative path within the session workspace files directory + + File path to the SKILL.md definition + """ handoff_time: datetime | None = None + """ISO 8601 timestamp when the handoff occurred""" + remote_session_id: str | None = None + """Session ID of the remote session being handed off""" + repository: RepositoryClass | str | None = None + """Repository context for the handed-off session + + Repository identifier in "owner/name" format, derived from the git remote URL + """ source_type: SourceType | None = None + """Origin type of the session being handed off""" + 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 plan that was created + """ messages_removed_during_truncation: float | None = None + """Number of messages removed by truncation""" + performed_by: str | None = None + """Identifier of the component that performed truncation (e.g., "BasicTruncator")""" + post_truncation_messages_length: float | None = None + """Number of conversation messages after truncation""" + post_truncation_tokens_in_messages: float | None = None + """Total tokens in conversation messages after truncation""" + pre_truncation_messages_length: float | None = None + """Number of conversation messages before truncation""" + pre_truncation_tokens_in_messages: float | None = None + """Total tokens in conversation messages before truncation""" + token_limit: float | None = None + """Maximum token count for the model's context window""" + tokens_removed_during_truncation: float | None = None + """Number of tokens removed by truncation""" + events_removed: float | None = None + """Number of events that were removed by the rewind""" + up_to_event_id: str | None = None + """Event ID that was rewound to; all events after this one were removed""" + code_changes: CodeChanges | None = None + """Aggregate code change metrics for the session""" + current_model: str | None = None + """Model that was selected at the time of shutdown""" + error_reason: str | None = None + """Error description when shutdownType is "error\"""" + model_metrics: dict[str, ModelMetric] | None = None + """Per-model usage breakdown, keyed by model identifier""" + session_start_time: float | None = None + """Unix timestamp (milliseconds) when the session started""" + shutdown_type: ShutdownType | None = None + """Whether the session ended normally ("routine") or due to a crash/fatal error ("error")""" + total_api_duration_ms: float | None = None + """Cumulative time spent in API calls during the session, in milliseconds""" + total_premium_requests: float | None = None + """Total number of premium API requests used during the session""" + branch: str | None = None + """Current git branch name""" + cwd: str | None = None + """Current working directory path""" + git_root: str | None = None + """Root directory of the git repository, resolved via git rev-parse""" + current_tokens: float | None = None + """Current number of tokens in the context window""" + messages_length: float | None = None + """Current number of messages in the conversation""" + checkpoint_number: float | None = None + """Checkpoint snapshot number created for recovery""" + checkpoint_path: str | None = None + """File path where the checkpoint was stored""" + compaction_tokens_used: CompactionTokensUsed | None = None + """Token usage breakdown for the compaction LLM call""" + error: ErrorClass | str | None = None + """Error message if compaction failed + + Error details when the tool execution failed + + Error message describing why the sub-agent failed + + Error details when the hook failed + """ messages_removed: float | None = None + """Number of messages removed during compaction""" + post_compaction_tokens: float | None = None + """Total tokens in conversation after compaction""" + pre_compaction_messages_length: float | None = None + """Number of messages before compaction""" + pre_compaction_tokens: float | None = None + """Total tokens in conversation before compaction""" + request_id: str | None = None + """GitHub request tracing ID (x-github-request-id header) for the compaction LLM call + + Unique identifier for this permission request; used to respond via + session.respondToPermission() + + Request ID of the resolved permission request; clients should dismiss any UI for this + request + + Unique identifier for this input request; used to respond via + session.respondToUserInput() + + Request ID of the resolved user input request; clients should dismiss any UI for this + request + + Unique identifier for this elicitation request; used to respond via + session.respondToElicitation() + + Request ID of the resolved elicitation request; clients should dismiss any UI for this + 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 + request + + Unique identifier for this request; used to respond via session.respondToQueuedCommand() + + Request ID of the resolved command request; clients should dismiss any UI for this + request + + Unique identifier for this request; used to respond via session.respondToExitPlanMode() + + Request ID of the resolved exit plan mode request; clients should dismiss any UI for this + request + """ success: bool | None = None + """Whether compaction completed successfully + + Whether the tool execution completed successfully + + Whether the hook completed successfully + """ summary_content: str | None = None + """LLM-generated summary of the compacted conversation history""" + tokens_removed: float | None = None + """Number of tokens removed during compaction""" + agent_mode: AgentMode | None = None + """The agent mode that was active when this message was sent""" + attachments: list[Attachment] | None = None + """Files, selections, or GitHub references attached to the message""" + content: str | None = None + """The user's message text as displayed in the timeline + + The complete extended thinking text from the model + + The assistant's text response content + + Full content of the skill file, injected into the conversation for the model + + The system or developer prompt text + """ interaction_id: str | None = None + """CAPI interaction ID for correlating this user message with its turn + + CAPI interaction ID for correlating this turn with upstream telemetry + + CAPI interaction ID for correlating this message with upstream telemetry + + 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) + """ transformed_content: str | None = None + """Transformed version of the message sent to the model, with XML wrapping, timestamps, and + other augmentations for prompt caching + """ turn_id: str | None = None + """Identifier for this turn within the agentic loop, typically a stringified turn number + + Identifier of the turn that has ended, matching the corresponding assistant.turn_start + event + """ intent: str | None = None + """Short description of what the agent is currently doing or planning to do""" + reasoning_id: str | None = None + """Unique identifier for this reasoning block + + Reasoning block ID this delta belongs to, matching the corresponding assistant.reasoning + event + """ delta_content: str | None = None + """Incremental text chunk to append to the reasoning content + + Incremental text chunk to append to the message content + """ total_response_size_bytes: float | None = None + """Cumulative total bytes received from the streaming response so far""" + encrypted_content: str | None = None + """Encrypted reasoning content from OpenAI models. Session-bound and stripped on resume.""" + message_id: str | None = None + """Unique identifier for this assistant message + + Message ID this delta belongs to, matching the corresponding assistant.message event + """ + output_tokens: float | None = None + """Actual output token count from the API response (completion_tokens), used for accurate + token accounting + + Number of output tokens produced + """ parent_tool_call_id: str | None = None + """Tool call ID of the parent tool invocation when this event originates from a sub-agent + + Parent tool call ID when this usage originates from a sub-agent + """ phase: str | None = None + """Generation phase for phased-output models (e.g., thinking vs. response phases)""" + reasoning_opaque: str | None = None + """Opaque/encrypted extended thinking data from Anthropic models. Session-bound and stripped + on resume. + """ reasoning_text: str | None = None + """Readable reasoning text from the model's extended thinking""" + tool_requests: list[ToolRequest] | None = None + """Tool invocations requested by the assistant in this message""" + api_call_id: str | None = None + """Completion ID from the model provider (e.g., chatcmpl-abc123)""" + cache_read_tokens: float | None = None + """Number of tokens read from prompt cache""" + cache_write_tokens: float | None = None + """Number of tokens written to prompt cache""" + copilot_usage: CopilotUsage | None = None + """Per-request cost and usage data from the CAPI copilot_usage response field""" + cost: float | None = None + """Model multiplier cost for billing purposes""" + duration: float | None = None + """Duration of the API call in milliseconds""" + initiator: str | None = None + """What initiated this API call (e.g., "sub-agent"); absent for user-initiated calls""" + input_tokens: float | None = None + """Number of input tokens consumed""" + model: str | None = None - output_tokens: float | None = None + """Model identifier used for this API call + + Model identifier that generated this tool call + """ quota_snapshots: dict[str, QuotaSnapshot] | None = None + """Per-quota resource usage snapshots, keyed by quota identifier""" + reason: str | None = None + """Reason the current turn was aborted (e.g., "user initiated")""" + arguments: Any = None + """Arguments for the tool invocation + + Arguments passed to the tool + + Arguments to pass to the external tool + """ tool_call_id: str | None = None + """Unique identifier for this tool call + + Tool call ID this partial result belongs to + + Tool call ID this progress notification belongs to + + Unique identifier for the completed tool call + + Tool call ID of the parent tool invocation that spawned this sub-agent + + Tool call ID assigned to this external tool invocation + """ tool_name: str | None = None + """Name of the tool the user wants to invoke + + Name of the tool being executed + + Name of the external tool to invoke + """ mcp_server_name: str | None = None + """Name of the MCP server hosting this tool, when the tool is an MCP tool""" + mcp_tool_name: str | None = None + """Original tool name on the MCP server, when the tool is an MCP tool""" + partial_output: str | None = None + """Incremental output chunk from the running tool""" + progress_message: str | None = None + """Human-readable progress status message (e.g., from an MCP server)""" + is_user_requested: bool | None = None + """Whether this tool call was explicitly requested by the user rather than the assistant""" + result: Result | None = None + """Tool execution result on success + + The result of the permission request + """ tool_telemetry: dict[str, Any] | None = None + """Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts)""" + allowed_tools: list[str] | None = None + """Tool names that should be auto-approved when this skill is active""" + name: str | None = None + """Name of the invoked skill + + Optional name identifier for the message source + """ plugin_name: str | None = None + """Name of the plugin this skill originated from, when applicable""" + plugin_version: str | None = None + """Version of the plugin this skill originated from, when applicable""" + agent_description: str | None = None + """Description of what the sub-agent does""" + agent_display_name: str | None = None + """Human-readable display name of the sub-agent + + Human-readable display name of the selected custom agent + """ agent_name: str | None = None + """Internal name of the sub-agent + + Internal name of the selected custom agent + """ tools: list[str] | None = None + """List of tool names available to this agent, or null for all tools""" + hook_invocation_id: str | None = None + """Unique identifier for this hook invocation + + Identifier matching the corresponding hook.start event + """ hook_type: str | None = None + """Type of hook being invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + + Type of hook that was invoked (e.g., "preToolUse", "postToolUse", "sessionStart") + """ input: Any = None + """Input data passed to the hook""" + output: Any = None + """Output data produced by the hook""" + metadata: Metadata | None = None + """Metadata about the prompt template and its construction""" + role: Role | None = None + """Message role: "system" for system prompts, "developer" for developer-injected instructions""" + permission_request: PermissionRequest | None = None + """Details of the permission being requested""" + allow_freeform: bool | None = None + """Whether the user can provide a free-form text response in addition to predefined choices""" + choices: list[str] | None = None + """Predefined choices for the user to select from, if applicable""" + 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.""" + requested_schema: RequestedSchema | None = None + """JSON Schema describing the form fields to present to the user""" + + command: str | None = None + """The slash command text to be executed (e.g., /help, /clear)""" + + actions: list[str] | None = None + """Available actions the user can take (e.g., approve, edit, reject)""" + + plan_content: str | None = None + """Full content of the plan file""" + + recommended_action: str | None = None + """The recommended action for the user to take""" @staticmethod def from_dict(obj: Any) -> 'Data': @@ -1060,6 +1840,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")) + 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")) warning_type = from_union([from_str, from_none], obj.get("warningType")) @@ -1122,6 +1903,7 @@ def from_dict(obj: Any) -> 'Data': total_response_size_bytes = from_union([from_float, from_none], obj.get("totalResponseSizeBytes")) encrypted_content = from_union([from_str, from_none], obj.get("encryptedContent")) message_id = from_union([from_str, from_none], obj.get("messageId")) + output_tokens = from_union([from_float, from_none], obj.get("outputTokens")) parent_tool_call_id = from_union([from_str, from_none], obj.get("parentToolCallId")) phase = from_union([from_str, from_none], obj.get("phase")) reasoning_opaque = from_union([from_str, from_none], obj.get("reasoningOpaque")) @@ -1136,7 +1918,6 @@ def from_dict(obj: Any) -> 'Data': initiator = from_union([from_str, from_none], obj.get("initiator")) input_tokens = from_union([from_float, from_none], obj.get("inputTokens")) model = from_union([from_str, from_none], obj.get("model")) - output_tokens = from_union([from_float, from_none], obj.get("outputTokens")) quota_snapshots = from_union([lambda x: from_dict(QuotaSnapshot.from_dict, x), from_none], obj.get("quotaSnapshots")) reason = from_union([from_str, from_none], obj.get("reason")) arguments = obj.get("arguments") @@ -1169,7 +1950,11 @@ 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")) - 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, 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, 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, output_tokens, 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 = 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(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) def to_dict(self) -> dict: result: dict = {} @@ -1201,6 +1986,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.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: result["title"] = from_union([from_str, from_none], self.title) if self.info_type is not None: @@ -1325,6 +2112,8 @@ def to_dict(self) -> dict: result["encryptedContent"] = from_union([from_str, from_none], self.encrypted_content) if self.message_id is not None: result["messageId"] = from_union([from_str, from_none], self.message_id) + if self.output_tokens is not None: + result["outputTokens"] = from_union([to_float, from_none], self.output_tokens) if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_str, from_none], self.parent_tool_call_id) if self.phase is not None: @@ -1353,8 +2142,6 @@ def to_dict(self) -> dict: result["inputTokens"] = from_union([to_float, from_none], self.input_tokens) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) - if self.output_tokens is not None: - result["outputTokens"] = from_union([to_float, from_none], self.output_tokens) if self.quota_snapshots is not None: result["quotaSnapshots"] = from_union([lambda x: from_dict(lambda x: to_class(QuotaSnapshot, x), x), from_none], self.quota_snapshots) if self.reason is not None: @@ -1419,6 +2206,14 @@ 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.command is not None: + result["command"] = from_union([from_str, from_none], self.command) + 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) return result @@ -1433,8 +2228,14 @@ class SessionEventType(Enum): ASSISTANT_TURN_END = "assistant.turn_end" ASSISTANT_TURN_START = "assistant.turn_start" ASSISTANT_USAGE = "assistant.usage" + COMMAND_COMPLETED = "command.completed" + COMMAND_QUEUED = "command.queued" ELICITATION_COMPLETED = "elicitation.completed" ELICITATION_REQUESTED = "elicitation.requested" + EXIT_PLAN_MODE_COMPLETED = "exit_plan_mode.completed" + EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" + EXTERNAL_TOOL_COMPLETED = "external_tool.completed" + EXTERNAL_TOOL_REQUESTED = "external_tool.requested" HOOK_END = "hook.end" HOOK_START = "hook.start" PENDING_MESSAGES_MODIFIED = "pending_messages.modified" @@ -1488,11 +2289,29 @@ def _missing_(cls, value: object) -> "SessionEventType": @dataclass class SessionEvent: data: Data + """Payload indicating the agent is idle; includes any background tasks still in flight + + Empty payload; the event signals that LLM-powered conversation compaction has begun + + Empty payload; the event signals that the pending message queue has changed + + Empty payload; the event signals that the custom agent was deselected, returning to the + default agent + """ id: UUID + """Unique event identifier (UUID v4), generated when the event is emitted""" + timestamp: datetime + """ISO 8601 timestamp when the event was created""" + type: SessionEventType ephemeral: bool | None = None + """When true, the event is transient and not persisted to the session event log on disk""" + parent_id: UUID | None = None + """ID of the chronologically preceding event in the session, forming a linked chain. Null + for the first event. + """ @staticmethod def from_dict(obj: Any) -> 'SessionEvent': diff --git a/python/copilot/sdk_protocol_version.py b/python/copilot/sdk_protocol_version.py index 77008267..1b038ce9 100644 --- a/python/copilot/sdk_protocol_version.py +++ b/python/copilot/sdk_protocol_version.py @@ -6,14 +6,28 @@ This must match the version expected by the copilot-agent-runtime server. """ -SDK_PROTOCOL_VERSION = 2 +SDK_PROTOCOL_VERSION = 3 +"""The maximum SDK protocol version supported.""" + +MIN_SDK_PROTOCOL_VERSION = 2 +"""The minimum SDK protocol version supported.""" def get_sdk_protocol_version() -> int: """ - Gets the SDK protocol version. + Gets the SDK protocol version (maximum supported). Returns: The protocol version number """ return SDK_PROTOCOL_VERSION + + +def get_min_sdk_protocol_version() -> int: + """ + Gets the minimum SDK protocol version supported. + + Returns: + The minimum protocol version number + """ + return MIN_SDK_PROTOCOL_VERSION diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index a759c113..05f555db 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -670,14 +670,14 @@ function emitSessionRpcClasses(node: Record, classes: string[]) 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);`); + for (const [groupName] of groups) srLines.push(` ${toPascalCase(groupName)} = new Session${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("", ` public Session${toPascalCase(groupName)}Api ${toPascalCase(groupName)} { get; }`); srLines.push(`}`); result.push(srLines.join("\n")); for (const [groupName, groupNode] of groups) { - result.push(emitSessionApiClass(`${toPascalCase(groupName)}Api`, groupNode as Record, classes)); + result.push(emitSessionApiClass(`Session${toPascalCase(groupName)}Api`, groupNode as Record, classes)); } return result; } diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 411d1c90..4c69f5a7 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -195,10 +195,11 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio const wrapperName = isSession ? "SessionRpc" : "ServerRpc"; const apiSuffix = "RpcApi"; + const apiPrefix = isSession ? "Session" : ""; // Emit API structs for groups for (const [groupName, groupNode] of groups) { - const apiName = toPascalCase(groupName) + apiSuffix; + const apiName = apiPrefix + toPascalCase(groupName) + apiSuffix; const fields = isSession ? "client *jsonrpc2.Client; sessionID string" : "client *jsonrpc2.Client"; lines.push(`type ${apiName} struct { ${fields} }`); lines.push(``); @@ -214,7 +215,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(` client *jsonrpc2.Client`); if (isSession) lines.push(` sessionID string`); for (const [groupName] of groups) { - lines.push(` ${toPascalCase(groupName)} *${toPascalCase(groupName)}${apiSuffix}`); + lines.push(` ${toPascalCase(groupName)} *${apiPrefix}${toPascalCase(groupName)}${apiSuffix}`); } lines.push(`}`); lines.push(``); @@ -232,8 +233,8 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(` return &${wrapperName}{${ctorFields}`); for (const [groupName] of groups) { const apiInit = isSession - ? `&${toPascalCase(groupName)}${apiSuffix}{client: client, sessionID: sessionID}` - : `&${toPascalCase(groupName)}${apiSuffix}{client: client}`; + ? `&${apiPrefix}${toPascalCase(groupName)}${apiSuffix}{client: client, sessionID: sessionID}` + : `&${apiPrefix}${toPascalCase(groupName)}${apiSuffix}{client: client}`; lines.push(` ${toPascalCase(groupName)}: ${apiInit},`); } lines.push(` }`); diff --git a/sdk-protocol-version.json b/sdk-protocol-version.json index 4bb5680c..7f0c791b 100644 --- a/sdk-protocol-version.json +++ b/sdk-protocol-version.json @@ -1,3 +1,4 @@ { - "version": 2 + "version": 3, + "minVersion": 2 } diff --git a/test/scenarios/auth/byok-anthropic/csharp/Program.cs b/test/scenarios/auth/byok-anthropic/csharp/Program.cs index 6bb9dd23..6e77d464 100644 --- a/test/scenarios/auth/byok-anthropic/csharp/Program.cs +++ b/test/scenarios/auth/byok-anthropic/csharp/Program.cs @@ -21,6 +21,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = model, Provider = new ProviderConfig { diff --git a/test/scenarios/auth/byok-anthropic/go/main.go b/test/scenarios/auth/byok-anthropic/go/main.go index 048d20f6..626997b8 100644 --- a/test/scenarios/auth/byok-anthropic/go/main.go +++ b/test/scenarios/auth/byok-anthropic/go/main.go @@ -35,6 +35,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: model, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "anthropic", BaseURL: baseUrl, diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index e50a33c1..fb496eae 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, PermissionHandler ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -20,6 +20,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": ANTHROPIC_MODEL, "provider": { "type": "anthropic", diff --git a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts index a7f460d8..7b026e93 100644 --- a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts +++ b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const apiKey = process.env.ANTHROPIC_API_KEY; @@ -14,7 +14,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model, provider: { type: "anthropic", diff --git a/test/scenarios/auth/byok-azure/csharp/Program.cs b/test/scenarios/auth/byok-azure/csharp/Program.cs index e6b2789a..d55ed34e 100644 --- a/test/scenarios/auth/byok-azure/csharp/Program.cs +++ b/test/scenarios/auth/byok-azure/csharp/Program.cs @@ -22,6 +22,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = model, Provider = new ProviderConfig { diff --git a/test/scenarios/auth/byok-azure/go/main.go b/test/scenarios/auth/byok-azure/go/main.go index 03f3b9dc..e2651017 100644 --- a/test/scenarios/auth/byok-azure/go/main.go +++ b/test/scenarios/auth/byok-azure/go/main.go @@ -36,6 +36,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: model, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "azure", BaseURL: endpoint, diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 89f37178..ee282cd2 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, PermissionHandler AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -21,6 +21,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": AZURE_OPENAI_MODEL, "provider": { "type": "azure", diff --git a/test/scenarios/auth/byok-azure/typescript/src/index.ts b/test/scenarios/auth/byok-azure/typescript/src/index.ts index 397a0a18..e6396150 100644 --- a/test/scenarios/auth/byok-azure/typescript/src/index.ts +++ b/test/scenarios/auth/byok-azure/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const endpoint = process.env.AZURE_OPENAI_ENDPOINT; @@ -15,7 +15,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model, provider: { type: "azure", diff --git a/test/scenarios/auth/byok-ollama/csharp/Program.cs b/test/scenarios/auth/byok-ollama/csharp/Program.cs index 585157b6..931b7564 100644 --- a/test/scenarios/auth/byok-ollama/csharp/Program.cs +++ b/test/scenarios/auth/byok-ollama/csharp/Program.cs @@ -17,6 +17,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = model, Provider = new ProviderConfig { diff --git a/test/scenarios/auth/byok-ollama/go/main.go b/test/scenarios/auth/byok-ollama/go/main.go index b8b34c5b..3be34033 100644 --- a/test/scenarios/auth/byok-ollama/go/main.go +++ b/test/scenarios/auth/byok-ollama/go/main.go @@ -32,6 +32,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: model, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: baseUrl, diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index b86c76ba..a57dd0f3 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, PermissionHandler OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -19,6 +19,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": OLLAMA_MODEL, "provider": { "type": "openai", diff --git a/test/scenarios/auth/byok-ollama/typescript/src/index.ts b/test/scenarios/auth/byok-ollama/typescript/src/index.ts index 936d118a..38a38dbc 100644 --- a/test/scenarios/auth/byok-ollama/typescript/src/index.ts +++ b/test/scenarios/auth/byok-ollama/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; const OLLAMA_MODEL = process.env.OLLAMA_MODEL ?? "llama3.2:3b"; @@ -12,7 +12,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: OLLAMA_MODEL, provider: { type: "openai", diff --git a/test/scenarios/auth/byok-openai/csharp/Program.cs b/test/scenarios/auth/byok-openai/csharp/Program.cs index 5d549bd5..3adcc1f9 100644 --- a/test/scenarios/auth/byok-openai/csharp/Program.cs +++ b/test/scenarios/auth/byok-openai/csharp/Program.cs @@ -21,6 +21,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = model, Provider = new ProviderConfig { diff --git a/test/scenarios/auth/byok-openai/go/main.go b/test/scenarios/auth/byok-openai/go/main.go index fc05c71b..f2dcef2e 100644 --- a/test/scenarios/auth/byok-openai/go/main.go +++ b/test/scenarios/auth/byok-openai/go/main.go @@ -35,6 +35,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: model, + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: baseUrl, diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index b501bb10..3703f016 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, PermissionHandler 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") @@ -20,6 +20,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": OPENAI_MODEL, "provider": { "type": "openai", diff --git a/test/scenarios/auth/byok-openai/typescript/src/index.ts b/test/scenarios/auth/byok-openai/typescript/src/index.ts index 41eda577..999b489d 100644 --- a/test/scenarios/auth/byok-openai/typescript/src/index.ts +++ b/test/scenarios/auth/byok-openai/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "claude-haiku-4.5"; @@ -15,7 +15,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: OPENAI_MODEL, provider: { type: "openai", diff --git a/test/scenarios/auth/gh-app/csharp/Program.cs b/test/scenarios/auth/gh-app/csharp/Program.cs index 1f2e27cc..f61e8e6e 100644 --- a/test/scenarios/auth/gh-app/csharp/Program.cs +++ b/test/scenarios/auth/gh-app/csharp/Program.cs @@ -70,6 +70,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/auth/gh-app/go/main.go b/test/scenarios/auth/gh-app/go/main.go index d84d030c..d0f310f0 100644 --- a/test/scenarios/auth/gh-app/go/main.go +++ b/test/scenarios/auth/gh-app/go/main.go @@ -173,6 +173,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4886fe07..6a290b42 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, PermissionHandler DEVICE_CODE_URL = "https://github.com/login/device/code" @@ -84,7 +84,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait({"prompt": "What is the capital of France?"}) if response: print(response.data.content) diff --git a/test/scenarios/auth/gh-app/typescript/src/index.ts b/test/scenarios/auth/gh-app/typescript/src/index.ts index a5b8f28e..c9360839 100644 --- a/test/scenarios/auth/gh-app/typescript/src/index.ts +++ b/test/scenarios/auth/gh-app/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; @@ -115,7 +115,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", }); diff --git a/test/scenarios/bundling/app-backend-to-server/csharp/Program.cs b/test/scenarios/bundling/app-backend-to-server/csharp/Program.cs index df3a335b..c968652f 100644 --- a/test/scenarios/bundling/app-backend-to-server/csharp/Program.cs +++ b/test/scenarios/bundling/app-backend-to-server/csharp/Program.cs @@ -28,6 +28,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/bundling/app-backend-to-server/go/main.go b/test/scenarios/bundling/app-backend-to-server/go/main.go index df2be62b..ef4308ad 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/main.go +++ b/test/scenarios/bundling/app-backend-to-server/go/main.go @@ -65,6 +65,7 @@ func chatHandler(w http.ResponseWriter, r *http.Request) { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { writeJSON(w, http.StatusInternalServerError, chatResponse{Error: err.Error()}) 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 29563149..7f455d16 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, PermissionHandler app = Flask(__name__) @@ -16,7 +16,7 @@ async def ask_copilot(prompt: str) -> str: client = CopilotClient({"cli_url": CLI_URL}) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait({"prompt": prompt}) diff --git a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts index 7ab734d1..e216c176 100644 --- a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts @@ -1,5 +1,5 @@ import express from "express"; -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const PORT = parseInt(process.env.PORT || "8080", 10); const CLI_URL = process.env.CLI_URL || process.env.COPILOT_CLI_URL || "localhost:3000"; @@ -17,7 +17,7 @@ app.post("/chat", async (req, res) => { const client = new CopilotClient({ cliUrl: CLI_URL }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt }); diff --git a/test/scenarios/bundling/app-backend-to-server/verify.sh b/test/scenarios/bundling/app-backend-to-server/verify.sh index 812a2cda..b0778b50 100755 --- a/test/scenarios/bundling/app-backend-to-server/verify.sh +++ b/test/scenarios/bundling/app-backend-to-server/verify.sh @@ -31,7 +31,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/bundling/app-direct-server/csharp/Program.cs b/test/scenarios/bundling/app-direct-server/csharp/Program.cs index 6dd14e9d..80c98c34 100644 --- a/test/scenarios/bundling/app-direct-server/csharp/Program.cs +++ b/test/scenarios/bundling/app-direct-server/csharp/Program.cs @@ -9,6 +9,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/bundling/app-direct-server/go/main.go b/test/scenarios/bundling/app-direct-server/go/main.go index 8be7dd60..282b4d8a 100644 --- a/test/scenarios/bundling/app-direct-server/go/main.go +++ b/test/scenarios/bundling/app-direct-server/go/main.go @@ -27,6 +27,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index c407d4fe..31647110 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 +from copilot import CopilotClient, PermissionHandler 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({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/bundling/app-direct-server/typescript/src/index.ts b/test/scenarios/bundling/app-direct-server/typescript/src/index.ts index 29a19dd1..69c113c0 100644 --- a/test/scenarios/bundling/app-direct-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-direct-server/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -6,7 +6,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/bundling/app-direct-server/verify.sh b/test/scenarios/bundling/app-direct-server/verify.sh index 6a4bbcc3..b29579e6 100755 --- a/test/scenarios/bundling/app-direct-server/verify.sh +++ b/test/scenarios/bundling/app-direct-server/verify.sh @@ -26,7 +26,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/bundling/container-proxy/csharp/Program.cs b/test/scenarios/bundling/container-proxy/csharp/Program.cs index 6dd14e9d..80c98c34 100644 --- a/test/scenarios/bundling/container-proxy/csharp/Program.cs +++ b/test/scenarios/bundling/container-proxy/csharp/Program.cs @@ -9,6 +9,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/bundling/container-proxy/go/main.go b/test/scenarios/bundling/container-proxy/go/main.go index 8be7dd60..282b4d8a 100644 --- a/test/scenarios/bundling/container-proxy/go/main.go +++ b/test/scenarios/bundling/container-proxy/go/main.go @@ -27,6 +27,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index c407d4fe..31647110 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 +from copilot import CopilotClient, PermissionHandler 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({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/bundling/container-proxy/typescript/src/index.ts b/test/scenarios/bundling/container-proxy/typescript/src/index.ts index 29a19dd1..69c113c0 100644 --- a/test/scenarios/bundling/container-proxy/typescript/src/index.ts +++ b/test/scenarios/bundling/container-proxy/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -6,7 +6,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/bundling/fully-bundled/csharp/Program.cs b/test/scenarios/bundling/fully-bundled/csharp/Program.cs index cb67c903..80eb9e13 100644 --- a/test/scenarios/bundling/fully-bundled/csharp/Program.cs +++ b/test/scenarios/bundling/fully-bundled/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/bundling/fully-bundled/go/main.go b/test/scenarios/bundling/fully-bundled/go/main.go index b8902fd9..935cd231 100644 --- a/test/scenarios/bundling/fully-bundled/go/main.go +++ b/test/scenarios/bundling/fully-bundled/go/main.go @@ -23,6 +23,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index d1441361..e781f6d8 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,7 +10,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index bee246f6..f0703f35 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/modes/default/csharp/Program.cs b/test/scenarios/modes/default/csharp/Program.cs index 243fcb92..0664f459 100644 --- a/test/scenarios/modes/default/csharp/Program.cs +++ b/test/scenarios/modes/default/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/modes/default/go/main.go b/test/scenarios/modes/default/go/main.go index dd2b45d3..7f01a7c0 100644 --- a/test/scenarios/modes/default/go/main.go +++ b/test/scenarios/modes/default/go/main.go @@ -22,6 +22,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index dadc0e7b..f052e32f 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,6 +11,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", }) diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index 89aab359..717d16a0 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", }); diff --git a/test/scenarios/modes/minimal/csharp/Program.cs b/test/scenarios/modes/minimal/csharp/Program.cs index 94cbc203..dfe01240 100644 --- a/test/scenarios/modes/minimal/csharp/Program.cs +++ b/test/scenarios/modes/minimal/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", AvailableTools = new List(), SystemMessage = new SystemMessageConfig diff --git a/test/scenarios/modes/minimal/go/main.go b/test/scenarios/modes/minimal/go/main.go index c3624b11..7c604d6b 100644 --- a/test/scenarios/modes/minimal/go/main.go +++ b/test/scenarios/modes/minimal/go/main.go @@ -22,6 +22,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, AvailableTools: []string{}, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 0b243caf..a0f32f49 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,6 +11,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "available_tools": [], "system_message": { diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index f20e476d..6e904442 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", availableTools: [], systemMessage: { diff --git a/test/scenarios/prompts/attachments/csharp/Program.cs b/test/scenarios/prompts/attachments/csharp/Program.cs index 357444a6..ba4d40d8 100644 --- a/test/scenarios/prompts/attachments/csharp/Program.cs +++ b/test/scenarios/prompts/attachments/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Replace, Content = "You are a helpful assistant. Answer questions about attached files concisely." }, AvailableTools = [], diff --git a/test/scenarios/prompts/attachments/go/main.go b/test/scenarios/prompts/attachments/go/main.go index 95eb2b4d..fa07fe1c 100644 --- a/test/scenarios/prompts/attachments/go/main.go +++ b/test/scenarios/prompts/attachments/go/main.go @@ -25,6 +25,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: systemPrompt, diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index c7e21e8b..b52152a9 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 +from copilot import CopilotClient, PermissionHandler SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" @@ -14,6 +14,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, "available_tools": [], diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 100f7e17..f8387ff9 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; @@ -11,7 +11,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", availableTools: [], systemMessage: { diff --git a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs index 71965088..3ffe09b3 100644 --- a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs +++ b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-opus-4.6", ReasoningEffort = "low", AvailableTools = new List(), diff --git a/test/scenarios/prompts/reasoning-effort/go/main.go b/test/scenarios/prompts/reasoning-effort/go/main.go index ccb4e528..d8ac7914 100644 --- a/test/scenarios/prompts/reasoning-effort/go/main.go +++ b/test/scenarios/prompts/reasoning-effort/go/main.go @@ -22,6 +22,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-opus-4.6", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, ReasoningEffort: "low", AvailableTools: []string{}, SystemMessage: &copilot.SystemMessageConfig{ diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index b38452a8..b9decca5 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,6 +11,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": "claude-opus-4.6", "reasoning_effort": "low", "available_tools": [], diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index e569fd70..91d19917 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -8,7 +8,7 @@ async function main() { try { // Test with "low" reasoning effort - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-opus-4.6", reasoningEffort: "low", availableTools: [], diff --git a/test/scenarios/prompts/system-message/csharp/Program.cs b/test/scenarios/prompts/system-message/csharp/Program.cs index 5f22cb02..532c5571 100644 --- a/test/scenarios/prompts/system-message/csharp/Program.cs +++ b/test/scenarios/prompts/system-message/csharp/Program.cs @@ -14,6 +14,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { diff --git a/test/scenarios/prompts/system-message/go/main.go b/test/scenarios/prompts/system-message/go/main.go index 074c9994..bc306cbe 100644 --- a/test/scenarios/prompts/system-message/go/main.go +++ b/test/scenarios/prompts/system-message/go/main.go @@ -24,6 +24,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: piratePrompt, diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 5e396c8c..e51d9177 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 +from copilot import CopilotClient, PermissionHandler PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" @@ -14,6 +14,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, "available_tools": [], diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index e0eb0aab..0a81d5f4 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.`; @@ -9,7 +9,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", systemMessage: { mode: "replace", content: PIRATE_PROMPT }, availableTools: [], diff --git a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs index 142bcb26..88a32ae5 100644 --- a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs @@ -15,6 +15,7 @@ { var session1Task = client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Replace, Content = PiratePrompt }, AvailableTools = [], @@ -22,6 +23,7 @@ var session2Task = client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { Mode = SystemMessageMode.Replace, Content = RobotPrompt }, AvailableTools = [], diff --git a/test/scenarios/sessions/concurrent-sessions/go/main.go b/test/scenarios/sessions/concurrent-sessions/go/main.go index ced91553..b1a1a8e1 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/main.go +++ b/test/scenarios/sessions/concurrent-sessions/go/main.go @@ -26,6 +26,7 @@ func main() { session1, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: piratePrompt, @@ -39,6 +40,7 @@ func main() { session2, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: robotPrompt, diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index ebca8990..de5a5314 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 +from copilot import CopilotClient, PermissionHandler PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" @@ -16,6 +16,7 @@ async def main(): session1, session2 = await asyncio.gather( client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "system_message": {"mode": "replace", "content": PIRATE_PROMPT}, "available_tools": [], @@ -23,6 +24,7 @@ async def main(): ), client.create_session( { + "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/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 89543d28..02fd3acd 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always say Arrr!`; const ROBOT_PROMPT = `You are a robot. Always say BEEP BOOP!`; @@ -11,12 +11,12 @@ async function main() { try { const [session1, session2] = await Promise.all([ - client.createSession({ + client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", systemMessage: { mode: "replace", content: PIRATE_PROMPT }, availableTools: [], }), - client.createSession({ + client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", systemMessage: { mode: "replace", content: ROBOT_PROMPT }, availableTools: [], diff --git a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs index fe281292..b171bf7c 100644 --- a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", AvailableTools = new List(), SystemMessage = new SystemMessageConfig diff --git a/test/scenarios/sessions/infinite-sessions/go/main.go b/test/scenarios/sessions/infinite-sessions/go/main.go index 540f8f6b..79a478ce 100644 --- a/test/scenarios/sessions/infinite-sessions/go/main.go +++ b/test/scenarios/sessions/infinite-sessions/go/main.go @@ -25,6 +25,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, AvailableTools: []string{}, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 23749d06..5c47afbb 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -11,6 +11,7 @@ async def main(): try: session = await client.create_session({ + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "available_tools": [], "system_message": { diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index 9de7b34f..6815dbdd 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", availableTools: [], systemMessage: { diff --git a/test/scenarios/sessions/multi-user-long-lived/verify.sh b/test/scenarios/sessions/multi-user-long-lived/verify.sh index a9e9a6df..24055d3c 100755 --- a/test/scenarios/sessions/multi-user-long-lived/verify.sh +++ b/test/scenarios/sessions/multi-user-long-lived/verify.sh @@ -28,7 +28,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/sessions/multi-user-short-lived/verify.sh b/test/scenarios/sessions/multi-user-short-lived/verify.sh index 24f29601..886da5a5 100755 --- a/test/scenarios/sessions/multi-user-short-lived/verify.sh +++ b/test/scenarios/sessions/multi-user-short-lived/verify.sh @@ -26,7 +26,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 7eb5e0ca..bb98c615 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -13,6 +13,7 @@ async def main(): # 1. Create a session session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "available_tools": [], } diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index 9e0a1685..6a6fa61c 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -8,7 +8,7 @@ async function main() { try { // 1. Create a session - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", availableTools: [], }); diff --git a/test/scenarios/sessions/streaming/csharp/Program.cs b/test/scenarios/sessions/streaming/csharp/Program.cs index 01683df7..5edcd032 100644 --- a/test/scenarios/sessions/streaming/csharp/Program.cs +++ b/test/scenarios/sessions/streaming/csharp/Program.cs @@ -19,6 +19,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", Streaming = true, }); diff --git a/test/scenarios/sessions/streaming/go/main.go b/test/scenarios/sessions/streaming/go/main.go index 6243a166..6e747b5e 100644 --- a/test/scenarios/sessions/streaming/go/main.go +++ b/test/scenarios/sessions/streaming/go/main.go @@ -22,6 +22,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Streaming: true, }) if err != nil { diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 94569de1..abd91492 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -12,6 +12,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "streaming": True, } diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index f70dccce..618676a6 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", streaming: true, }); diff --git a/test/scenarios/tools/custom-agents/csharp/Program.cs b/test/scenarios/tools/custom-agents/csharp/Program.cs index c5c6525f..6f1e7184 100644 --- a/test/scenarios/tools/custom-agents/csharp/Program.cs +++ b/test/scenarios/tools/custom-agents/csharp/Program.cs @@ -14,6 +14,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", CustomAgents = [ diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go index f2add822..28d82055 100644 --- a/test/scenarios/tools/custom-agents/go/main.go +++ b/test/scenarios/tools/custom-agents/go/main.go @@ -22,6 +22,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, CustomAgents: []copilot.CustomAgentConfig{ { Name: "researcher", diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 0b5f073d..a90c0c14 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -12,6 +12,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "custom_agents": [ { diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index f6e16325..62023575 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", customAgents: [ { diff --git a/test/scenarios/tools/mcp-servers/csharp/Program.cs b/test/scenarios/tools/mcp-servers/csharp/Program.cs index 2ee25aac..e310d721 100644 --- a/test/scenarios/tools/mcp-servers/csharp/Program.cs +++ b/test/scenarios/tools/mcp-servers/csharp/Program.cs @@ -25,6 +25,7 @@ var config = new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", AvailableTools = new List(), SystemMessage = new SystemMessageConfig diff --git a/test/scenarios/tools/mcp-servers/go/main.go b/test/scenarios/tools/mcp-servers/go/main.go index a6e2e9c1..dbf98857 100644 --- a/test/scenarios/tools/mcp-servers/go/main.go +++ b/test/scenarios/tools/mcp-servers/go/main.go @@ -39,6 +39,7 @@ func main() { sessionConfig := &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: "You are a helpful assistant. Answer questions concisely.", diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index f092fb9a..c6c5c36c 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -23,6 +23,7 @@ async def main(): } session_config = { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "available_tools": [], "system_message": { diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index 1e8c1146..8cdf3efe 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -19,7 +19,7 @@ async function main() { }; } - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", ...(Object.keys(mcpServers).length > 0 && { mcpServers }), availableTools: [], diff --git a/test/scenarios/tools/no-tools/csharp/Program.cs b/test/scenarios/tools/no-tools/csharp/Program.cs index c3de1de5..789f9c23 100644 --- a/test/scenarios/tools/no-tools/csharp/Program.cs +++ b/test/scenarios/tools/no-tools/csharp/Program.cs @@ -19,6 +19,7 @@ You can only respond with text based on your training data. { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { diff --git a/test/scenarios/tools/no-tools/go/main.go b/test/scenarios/tools/no-tools/go/main.go index 62af3bce..cee72e4b 100644 --- a/test/scenarios/tools/no-tools/go/main.go +++ b/test/scenarios/tools/no-tools/go/main.go @@ -27,6 +27,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: systemPrompt, diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index a3824bab..371dd73e 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, PermissionHandler SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -17,6 +17,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, "available_tools": [], diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index 487b4762..f4b4ace8 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; const SYSTEM_PROMPT = `You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -12,7 +12,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", systemMessage: { mode: "replace", content: SYSTEM_PROMPT }, availableTools: [], diff --git a/test/scenarios/tools/tool-filtering/csharp/Program.cs b/test/scenarios/tools/tool-filtering/csharp/Program.cs index f21482b1..59fe3159 100644 --- a/test/scenarios/tools/tool-filtering/csharp/Program.cs +++ b/test/scenarios/tools/tool-filtering/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", SystemMessage = new SystemMessageConfig { diff --git a/test/scenarios/tools/tool-filtering/go/main.go b/test/scenarios/tools/tool-filtering/go/main.go index 851ca311..07fd4f0c 100644 --- a/test/scenarios/tools/tool-filtering/go/main.go +++ b/test/scenarios/tools/tool-filtering/go/main.go @@ -24,6 +24,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, SystemMessage: &copilot.SystemMessageConfig{ Mode: "replace", Content: systemPrompt, diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 1fdfacc7..f879effc 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 +from copilot import CopilotClient, PermissionHandler 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.""" @@ -14,6 +14,7 @@ async def main(): try: session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "model": "claude-haiku-4.5", "system_message": {"mode": "replace", "content": SYSTEM_PROMPT}, "available_tools": ["grep", "glob", "view"], diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index 9976e38f..a731a87c 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5", systemMessage: { mode: "replace", diff --git a/test/scenarios/tools/tool-overrides/go/tool-overrides-go b/test/scenarios/tools/tool-overrides/go/tool-overrides-go new file mode 100755 index 00000000..58143eb1 Binary files /dev/null and b/test/scenarios/tools/tool-overrides/go/tool-overrides-go differ diff --git a/test/scenarios/transport/reconnect/csharp/Program.cs b/test/scenarios/transport/reconnect/csharp/Program.cs index 80dc482d..6a606970 100644 --- a/test/scenarios/transport/reconnect/csharp/Program.cs +++ b/test/scenarios/transport/reconnect/csharp/Program.cs @@ -11,6 +11,7 @@ Console.WriteLine("--- Session 1 ---"); await using var session1 = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); @@ -34,6 +35,7 @@ Console.WriteLine("--- Session 2 ---"); await using var session2 = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/transport/reconnect/go/main.go b/test/scenarios/transport/reconnect/go/main.go index 493e9d25..fce6ad37 100644 --- a/test/scenarios/transport/reconnect/go/main.go +++ b/test/scenarios/transport/reconnect/go/main.go @@ -25,6 +25,7 @@ func main() { fmt.Println("--- Session 1 ---") session1, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) @@ -51,6 +52,7 @@ func main() { fmt.Println("--- Session 2 ---") session2, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 1b82b109..4f6ac840 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 +from copilot import CopilotClient, PermissionHandler 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({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response1 = await session1.send_and_wait( {"prompt": "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({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response2 = await session2.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/transport/reconnect/typescript/src/index.ts b/test/scenarios/transport/reconnect/typescript/src/index.ts index ca28df94..7a767152 100644 --- a/test/scenarios/transport/reconnect/typescript/src/index.ts +++ b/test/scenarios/transport/reconnect/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -8,7 +8,7 @@ async function main() { try { // First session console.log("--- Session 1 ---"); - const session1 = await client.createSession({ model: "claude-haiku-4.5" }); + const session1 = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response1 = await session1.sendAndWait({ prompt: "What is the capital of France?", @@ -26,7 +26,7 @@ async function main() { // Second session — tests that the server accepts new sessions console.log("--- Session 2 ---"); - const session2 = await client.createSession({ model: "claude-haiku-4.5" }); + const session2 = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response2 = await session2.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/transport/reconnect/verify.sh b/test/scenarios/transport/reconnect/verify.sh index 28dd7326..321ab9d4 100755 --- a/test/scenarios/transport/reconnect/verify.sh +++ b/test/scenarios/transport/reconnect/verify.sh @@ -26,7 +26,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/transport/stdio/csharp/Program.cs b/test/scenarios/transport/stdio/csharp/Program.cs index cb67c903..80eb9e13 100644 --- a/test/scenarios/transport/stdio/csharp/Program.cs +++ b/test/scenarios/transport/stdio/csharp/Program.cs @@ -12,6 +12,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/transport/stdio/go/main.go b/test/scenarios/transport/stdio/go/main.go index b8902fd9..935cd231 100644 --- a/test/scenarios/transport/stdio/go/main.go +++ b/test/scenarios/transport/stdio/go/main.go @@ -23,6 +23,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index d1441361..e781f6d8 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 +from copilot import CopilotClient, PermissionHandler async def main(): @@ -10,7 +10,7 @@ async def main(): client = CopilotClient(opts) try: - session = await client.create_session({"model": "claude-haiku-4.5"}) + session = await client.create_session({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index bee246f6..f0703f35 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -7,7 +7,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/transport/tcp/csharp/Program.cs b/test/scenarios/transport/tcp/csharp/Program.cs index 051c877d..8681066e 100644 --- a/test/scenarios/transport/tcp/csharp/Program.cs +++ b/test/scenarios/transport/tcp/csharp/Program.cs @@ -13,6 +13,7 @@ { await using var session = await client.CreateSessionAsync(new SessionConfig { + OnPermissionRequest = PermissionHandler.ApproveAll, Model = "claude-haiku-4.5", }); diff --git a/test/scenarios/transport/tcp/go/main.go b/test/scenarios/transport/tcp/go/main.go index 8be7dd60..282b4d8a 100644 --- a/test/scenarios/transport/tcp/go/main.go +++ b/test/scenarios/transport/tcp/go/main.go @@ -27,6 +27,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { log.Fatal(err) diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index c407d4fe..31647110 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 +from copilot import CopilotClient, PermissionHandler 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({"model": "claude-haiku-4.5", "on_permission_request": PermissionHandler.approve_all}) response = await session.send_and_wait( {"prompt": "What is the capital of France?"} diff --git a/test/scenarios/transport/tcp/typescript/src/index.ts b/test/scenarios/transport/tcp/typescript/src/index.ts index 29a19dd1..69c113c0 100644 --- a/test/scenarios/transport/tcp/typescript/src/index.ts +++ b/test/scenarios/transport/tcp/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient } from "@github/copilot-sdk"; +import { CopilotClient, approveAll } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ @@ -6,7 +6,7 @@ async function main() { }); try { - const session = await client.createSession({ model: "claude-haiku-4.5" }); + const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-haiku-4.5" }); const response = await session.sendAndWait({ prompt: "What is the capital of France?", diff --git a/test/scenarios/transport/tcp/verify.sh b/test/scenarios/transport/tcp/verify.sh index 711e0959..44a39a64 100755 --- a/test/scenarios/transport/tcp/verify.sh +++ b/test/scenarios/transport/tcp/verify.sh @@ -26,7 +26,7 @@ if [ -z "${COPILOT_CLI_PATH:-}" ]; then # Try to resolve from the TypeScript sample node_modules TS_DIR="$SCRIPT_DIR/typescript" if [ -d "$TS_DIR/node_modules/@github/copilot" ]; then - COPILOT_CLI_PATH="$(node -e "console.log(require.resolve('@github/copilot'))" 2>/dev/null || true)" + COPILOT_CLI_PATH="$(node --input-type=module -e "import{fileURLToPath}from'url';import{dirname,join}from'path';const u=import.meta.resolve('@github/copilot/sdk');console.log(join(dirname(dirname(fileURLToPath(u))),'index.js'));" 2>/dev/null || true)" fi # Fallback: check PATH if [ -z "${COPILOT_CLI_PATH:-}" ]; then diff --git a/test/scenarios/verify.sh b/test/scenarios/verify.sh index 543c93d2..90f8fcb7 100755 --- a/test/scenarios/verify.sh +++ b/test/scenarios/verify.sh @@ -13,7 +13,19 @@ trap cleanup EXIT if [ -n "${COPILOT_CLI_PATH:-}" ]; then echo "Using CLI override: $COPILOT_CLI_PATH" else - echo "No COPILOT_CLI_PATH set — SDKs will use their bundled CLI." + # Auto-discover CLI from the Node SDK's bundled @github/copilot package + DISCOVERED_CLI=$(node --input-type=module -e " + import { fileURLToPath } from 'url'; + import { dirname, join } from 'path'; + const sdkUrl = import.meta.resolve('@github/copilot/sdk'); + console.log(join(dirname(dirname(fileURLToPath(sdkUrl))), 'index.js')); + " 2>/dev/null || true) + if [ -n "$DISCOVERED_CLI" ] && [ -f "$DISCOVERED_CLI" ]; then + export COPILOT_CLI_PATH="$DISCOVERED_CLI" + echo "Auto-discovered CLI: $COPILOT_CLI_PATH" + else + echo "⚠️ Could not auto-discover CLI — SDKs will attempt bundled CLI resolution." + fi fi # ── Auth ────────────────────────────────────────────────────────────