From 73100912cb09295fa894ae2cb982313e1a166ee3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 4 Mar 2026 10:03:20 -0500 Subject: [PATCH 1/3] Register sessions before RPC to prevent dropped events Register sessions in the client's sessions map before issuing the session.create and session.resume RPC calls, so that events emitted by the CLI during the RPC (e.g. session.start, permission requests, tool calls) are not dropped. Previously, sessions were registered only after the RPC completed, creating a window where notifications for the session had no target. The session ID is now generated client-side (via UUID) rather than extracted from the server response. On RPC failure, the session is cleaned up from the map. Changes across all four SDKs (Node.js, Python, Go, .NET): - Generate sessionId client-side before the RPC - Create and register the session in the sessions map before the RPC - Set workspacePath from the RPC response after it completes - Remove the session from the map if the RPC fails Includes unit tests for Node.js, Python, and Go that verify: - Session is in the map when session.create RPC is called - Session is in the map when session.resume RPC is called - Session is cleaned up from map on RPC failure (create and resume) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 139 ++++++----- dotnet/src/Session.cs | 2 +- go/client.go | 72 ++++-- go/client_test.go | 232 ++++++++++++++++++ go/go.mod | 2 + go/go.sum | 2 + go/internal/e2e/session_test.go | 4 +- nodejs/src/client.ts | 159 ++++++------ nodejs/src/session.ts | 2 +- nodejs/test/client.test.ts | 88 +++++++ python/copilot/client.py | 38 ++- python/test_client.py | 112 +++++++++ test/scenarios/auth/byok-anthropic/go/go.mod | 5 +- test/scenarios/auth/byok-anthropic/go/go.sum | 2 + test/scenarios/auth/byok-azure/go/go.mod | 5 +- test/scenarios/auth/byok-azure/go/go.sum | 2 + test/scenarios/auth/byok-ollama/go/go.mod | 5 +- test/scenarios/auth/byok-ollama/go/go.sum | 2 + test/scenarios/auth/byok-openai/go/go.mod | 5 +- test/scenarios/auth/byok-openai/go/go.sum | 2 + test/scenarios/auth/gh-app/go/go.mod | 5 +- test/scenarios/auth/gh-app/go/go.sum | 2 + .../bundling/app-backend-to-server/go/go.mod | 5 +- .../bundling/app-backend-to-server/go/go.sum | 2 + .../bundling/app-direct-server/go/go.mod | 5 +- .../bundling/app-direct-server/go/go.sum | 2 + .../bundling/container-proxy/go/go.mod | 5 +- .../bundling/container-proxy/go/go.sum | 2 + .../bundling/fully-bundled/go/go.mod | 5 +- .../bundling/fully-bundled/go/go.sum | 2 + test/scenarios/callbacks/hooks/go/go.mod | 5 +- test/scenarios/callbacks/hooks/go/go.sum | 2 + .../scenarios/callbacks/permissions/go/go.mod | 5 +- .../scenarios/callbacks/permissions/go/go.sum | 2 + test/scenarios/callbacks/user-input/go/go.mod | 5 +- test/scenarios/callbacks/user-input/go/go.sum | 2 + test/scenarios/modes/default/go/go.mod | 5 +- test/scenarios/modes/default/go/go.sum | 2 + test/scenarios/modes/minimal/go/go.mod | 5 +- test/scenarios/modes/minimal/go/go.sum | 2 + test/scenarios/prompts/attachments/go/go.mod | 5 +- test/scenarios/prompts/attachments/go/go.sum | 2 + .../prompts/reasoning-effort/go/go.mod | 5 +- .../prompts/reasoning-effort/go/go.sum | 2 + .../prompts/system-message/go/go.mod | 5 +- .../prompts/system-message/go/go.sum | 2 + .../sessions/concurrent-sessions/go/go.mod | 5 +- .../sessions/concurrent-sessions/go/go.sum | 2 + .../sessions/infinite-sessions/go/go.mod | 5 +- .../sessions/infinite-sessions/go/go.sum | 2 + .../sessions/session-resume/go/go.mod | 5 +- .../sessions/session-resume/go/go.sum | 2 + test/scenarios/sessions/streaming/go/go.mod | 5 +- test/scenarios/sessions/streaming/go/go.sum | 2 + test/scenarios/tools/custom-agents/go/go.mod | 5 +- test/scenarios/tools/custom-agents/go/go.sum | 2 + test/scenarios/tools/mcp-servers/go/go.mod | 5 +- test/scenarios/tools/mcp-servers/go/go.sum | 2 + test/scenarios/tools/no-tools/go/go.mod | 5 +- test/scenarios/tools/no-tools/go/go.sum | 2 + test/scenarios/tools/skills/go/go.mod | 5 +- test/scenarios/tools/skills/go/go.sum | 2 + test/scenarios/tools/tool-filtering/go/go.mod | 5 +- test/scenarios/tools/tool-filtering/go/go.sum | 2 + test/scenarios/tools/tool-overrides/go/go.mod | 5 +- test/scenarios/tools/tool-overrides/go/go.sum | 2 + .../tools/virtual-filesystem/go/go.mod | 5 +- .../tools/virtual-filesystem/go/go.sum | 2 + test/scenarios/transport/reconnect/go/go.mod | 5 +- test/scenarios/transport/reconnect/go/go.sum | 2 + test/scenarios/transport/stdio/go/go.mod | 5 +- test/scenarios/transport/stdio/go/go.sum | 2 + test/scenarios/transport/tcp/go/go.mod | 5 +- test/scenarios/transport/tcp/go/go.sum | 2 + 74 files changed, 873 insertions(+), 196 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index cf6c5a29d..25f9f0d31 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -381,33 +381,11 @@ public async Task CreateSessionAsync(SessionConfig config, Cance config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); - var request = new CreateSessionRequest( - config.Model, - config.SessionId, - config.ClientName, - config.ReasoningEffort, - config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, - config.AvailableTools, - config.ExcludedTools, - config.Provider, - (bool?)true, - config.OnUserInputRequest != null ? true : null, - hasHooks ? true : null, - config.WorkingDirectory, - config.Streaming is true ? true : null, - config.McpServers, - "direct", - config.CustomAgents, - config.ConfigDir, - config.SkillDirectories, - config.DisabledSkills, - config.InfiniteSessions); - - var response = await InvokeRpcAsync( - connection.Rpc, "session.create", [request], cancellationToken); - - var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); + var sessionId = config.SessionId ?? Guid.NewGuid().ToString(); + + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + var session = new CopilotSession(sessionId, connection.Rpc); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) @@ -418,10 +396,42 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { session.RegisterHooks(config.Hooks); } + _sessions[sessionId] = session; - if (!_sessions.TryAdd(response.SessionId, session)) + try { - throw new InvalidOperationException($"Session {response.SessionId} already exists"); + var request = new CreateSessionRequest( + config.Model, + sessionId, + config.ClientName, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, + (bool?)true, + config.OnUserInputRequest != null ? true : null, + hasHooks ? true : null, + config.WorkingDirectory, + config.Streaming is true ? true : null, + config.McpServers, + "direct", + config.CustomAgents, + config.ConfigDir, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); + + var response = await InvokeRpcAsync( + connection.Rpc, "session.create", [request], cancellationToken); + + session.WorkspacePath = response.WorkspacePath; + } + catch + { + _sessions.TryRemove(sessionId, out _); + throw; } return session; @@ -472,34 +482,9 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.Hooks.OnSessionEnd != null || config.Hooks.OnErrorOccurred != null); - var request = new ResumeSessionRequest( - sessionId, - config.ClientName, - config.Model, - config.ReasoningEffort, - config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), - config.SystemMessage, - config.AvailableTools, - config.ExcludedTools, - config.Provider, - (bool?)true, - config.OnUserInputRequest != null ? true : null, - hasHooks ? true : null, - config.WorkingDirectory, - config.ConfigDir, - config.DisableResume is true ? true : null, - config.Streaming is true ? true : null, - config.McpServers, - "direct", - config.CustomAgents, - config.SkillDirectories, - config.DisabledSkills, - config.InfiniteSessions); - - var response = await InvokeRpcAsync( - connection.Rpc, "session.resume", [request], cancellationToken); - - var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + var session = new CopilotSession(sessionId, connection.Rpc); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); if (config.OnUserInputRequest != null) @@ -510,9 +495,45 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { session.RegisterHooks(config.Hooks); } + _sessions[sessionId] = session; + + try + { + var request = new ResumeSessionRequest( + sessionId, + config.ClientName, + config.Model, + config.ReasoningEffort, + config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), + config.SystemMessage, + config.AvailableTools, + config.ExcludedTools, + config.Provider, + (bool?)true, + config.OnUserInputRequest != null ? true : null, + hasHooks ? true : null, + config.WorkingDirectory, + config.ConfigDir, + config.DisableResume is true ? true : null, + config.Streaming is true ? true : null, + config.McpServers, + "direct", + config.CustomAgents, + config.SkillDirectories, + config.DisabledSkills, + config.InfiniteSessions); + + var response = await InvokeRpcAsync( + connection.Rpc, "session.resume", [request], cancellationToken); + + session.WorkspacePath = response.WorkspacePath; + } + catch + { + _sessions.TryRemove(sessionId, out _); + throw; + } - // Replace any existing session entry to ensure new config (like permission handler) is used - _sessions[response.SessionId] = session; return session; } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 8bbee6071..8b9751cd5 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -78,7 +78,7 @@ public partial class CopilotSession : IAsyncDisposable /// The path to the workspace containing checkpoints/, plan.md, and files/ subdirectories, /// or null if infinite sessions are disabled. /// - public string? WorkspacePath { get; } + public string? WorkspacePath { get; internal set; } /// /// Initializes a new instance of the class. diff --git a/go/client.go b/go/client.go index c88a68ac3..8a385ee05 100644 --- a/go/client.go +++ b/go/client.go @@ -44,6 +44,8 @@ import ( "sync/atomic" "time" + "github.com/google/uuid" + "github.com/github/copilot-sdk/go/internal/embeddedcli" "github.com/github/copilot-sdk/go/internal/jsonrpc2" "github.com/github/copilot-sdk/go/rpc" @@ -484,7 +486,6 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req := createSessionRequest{} req.Model = config.Model - req.SessionID = config.SessionID req.ClientName = config.ClientName req.ReasoningEffort = config.ReasoningEffort req.ConfigDir = config.ConfigDir @@ -517,17 +518,15 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } req.RequestPermission = Bool(true) - result, err := c.client.Request("session.create", req) - if err != nil { - return nil, fmt.Errorf("failed to create session: %w", err) - } - - var response createSessionResponse - if err := json.Unmarshal(result, &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) + sessionID := config.SessionID + if sessionID == "" { + sessionID = uuid.New().String() } + req.SessionID = sessionID - session := newSession(response.SessionID, c.client, response.WorkspacePath) + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + session := newSession(sessionID, c.client, "") session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) @@ -539,9 +538,27 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } c.sessionsMux.Lock() - c.sessions[response.SessionID] = session + c.sessions[sessionID] = session c.sessionsMux.Unlock() + result, err := c.client.Request("session.create", req) + if err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to create session: %w", err) + } + + var response createSessionResponse + if err := json.Unmarshal(result, &response); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + session.workspacePath = response.WorkspacePath + return session, nil } @@ -616,17 +633,10 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.InfiniteSessions = config.InfiniteSessions req.RequestPermission = Bool(true) - result, err := c.client.Request("session.resume", req) - if err != nil { - return nil, fmt.Errorf("failed to resume session: %w", err) - } - - var response resumeSessionResponse - if err := json.Unmarshal(result, &response); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + session := newSession(sessionID, c.client, "") - session := newSession(response.SessionID, c.client, response.WorkspacePath) session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) if config.OnUserInputRequest != nil { @@ -637,9 +647,27 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } c.sessionsMux.Lock() - c.sessions[response.SessionID] = session + c.sessions[sessionID] = session c.sessionsMux.Unlock() + result, err := c.client.Request("session.resume", req) + if err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to resume session: %w", err) + } + + var response resumeSessionResponse + if err := json.Unmarshal(result, &response); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + session.workspacePath = response.WorkspacePath + return session, nil } diff --git a/go/client_test.go b/go/client_test.go index d791a5a30..16285e98f 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,13 +1,18 @@ package copilot import ( + "bufio" "encoding/json" + "fmt" + "io" "os" "path/filepath" "reflect" "regexp" "sync" "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" ) // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead @@ -569,3 +574,230 @@ func TestClient_StartStopRace(t *testing.T) { t.Fatal(err) } } + +// fakeJSONRPCServer reads one JSON-RPC request from r and sends a response to w. +// onRequest is called with the parsed method and params before the response is sent, +// allowing the caller to inspect state (e.g. the sessions map) during the RPC. +func fakeJSONRPCServer(t *testing.T, r io.Reader, w io.WriteCloser, onRequest func(method string, params json.RawMessage)) { + t.Helper() + reader := bufio.NewReader(r) + + // Read Content-Length header + var contentLength int + for { + line, err := reader.ReadString('\n') + if err != nil { + t.Errorf("failed to read header: %v", err) + w.Close() + return + } + if line == "\r\n" || line == "\n" { + break + } + fmt.Sscanf(line, "Content-Length: %d", &contentLength) + } + + // Read body + body := make([]byte, contentLength) + if _, err := io.ReadFull(reader, body); err != nil { + t.Errorf("failed to read body: %v", err) + w.Close() + return + } + + // Parse request + var req struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + } + if err := json.Unmarshal(body, &req); err != nil { + t.Errorf("failed to unmarshal request: %v", err) + w.Close() + return + } + + onRequest(req.Method, req.Params) + + // Echo sessionId from request params + var params struct { + SessionID string `json:"sessionId"` + } + json.Unmarshal(req.Params, ¶ms) + + result, _ := json.Marshal(map[string]any{"sessionId": params.SessionID, "workspacePath": "/tmp"}) + resp, _ := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": req.ID, + "result": json.RawMessage(result), + }) + header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(resp)) + w.Write([]byte(header)) + w.Write(resp) +} + +// fakeJSONRPCErrorServer reads one JSON-RPC request and returns an error response. +func fakeJSONRPCErrorServer(t *testing.T, r io.Reader, w io.WriteCloser) { + t.Helper() + reader := bufio.NewReader(r) + + var contentLength int + for { + line, err := reader.ReadString('\n') + if err != nil { + w.Close() + return + } + if line == "\r\n" || line == "\n" { + break + } + fmt.Sscanf(line, "Content-Length: %d", &contentLength) + } + + body := make([]byte, contentLength) + if _, err := io.ReadFull(reader, body); err != nil { + w.Close() + return + } + + var req struct { + ID json.RawMessage `json:"id"` + } + json.Unmarshal(body, &req) + + resp, _ := json.Marshal(map[string]any{ + "jsonrpc": "2.0", + "id": req.ID, + "error": map[string]any{"code": -32000, "message": "test error"}, + }) + header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(resp)) + w.Write([]byte(header)) + w.Write(resp) +} + +// newTestClientWithFakeServer creates a Client wired to a fake jsonrpc2.Client +// backed by the provided io pipes. The caller must call jrpcClient.Stop() when done. +func newTestClientWithFakeServer(clientWriter io.WriteCloser, clientReader io.ReadCloser) (*Client, *jsonrpc2.Client) { + jrpcClient := jsonrpc2.NewClient(clientWriter, clientReader) + jrpcClient.Start() + + client := NewClient(nil) + client.client = jrpcClient + client.state = StateConnected + client.sessions = make(map[string]*Session) + return client, jrpcClient +} + +func TestClient_CreateSession_RegistersSessionBeforeRPC(t *testing.T) { + // Create pipes: client writes to serverReader, server writes to clientReader + serverReader, clientWriter := io.Pipe() + clientReader, serverWriter := io.Pipe() + client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) + defer jrpcClient.Stop() + + sessionInMap := false + go fakeJSONRPCServer(t, serverReader, serverWriter, func(method string, params json.RawMessage) { + if method != "session.create" { + t.Errorf("expected session.create, got %s", method) + } + var p struct { + SessionID string `json:"sessionId"` + } + json.Unmarshal(params, &p) + client.sessionsMux.Lock() + _, sessionInMap = client.sessions[p.SessionID] + client.sessionsMux.Unlock() + }) + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + if session == nil { + t.Fatal("expected non-nil session") + } + if !sessionInMap { + t.Error("session was not in sessions map when session.create RPC was issued") + } +} + +func TestClient_ResumeSession_RegistersSessionBeforeRPC(t *testing.T) { + serverReader, clientWriter := io.Pipe() + clientReader, serverWriter := io.Pipe() + client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) + defer jrpcClient.Stop() + + sessionInMap := false + go fakeJSONRPCServer(t, serverReader, serverWriter, func(method string, params json.RawMessage) { + if method != "session.resume" { + t.Errorf("expected session.resume, got %s", method) + } + var p struct { + SessionID string `json:"sessionId"` + } + json.Unmarshal(params, &p) + client.sessionsMux.Lock() + _, sessionInMap = client.sessions[p.SessionID] + client.sessionsMux.Unlock() + }) + + session, err := client.ResumeSessionWithOptions(t.Context(), "test-session-id", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + if session == nil { + t.Fatal("expected non-nil session") + } + if !sessionInMap { + t.Error("session was not in sessions map when session.resume RPC was issued") + } +} + +func TestClient_CreateSession_CleansUpOnRPCFailure(t *testing.T) { + serverReader, clientWriter := io.Pipe() + clientReader, serverWriter := io.Pipe() + client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) + defer jrpcClient.Stop() + + // Send a JSON-RPC error response to simulate failure + go fakeJSONRPCErrorServer(t, serverReader, serverWriter) + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + }) + if err == nil { + t.Fatal("expected error from CreateSession") + } + client.sessionsMux.Lock() + count := len(client.sessions) + client.sessionsMux.Unlock() + if count != 0 { + t.Errorf("expected 0 sessions after failed create, got %d", count) + } +} + +func TestClient_ResumeSession_CleansUpOnRPCFailure(t *testing.T) { + serverReader, clientWriter := io.Pipe() + clientReader, serverWriter := io.Pipe() + client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) + defer jrpcClient.Stop() + + go fakeJSONRPCErrorServer(t, serverReader, serverWriter) + + _, err := client.ResumeSessionWithOptions(t.Context(), "test-session-id", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + }) + if err == nil { + t.Fatal("expected error from ResumeSessionWithOptions") + } + client.sessionsMux.Lock() + count := len(client.sessions) + client.sessionsMux.Unlock() + if count != 0 { + t.Errorf("expected 0 sessions after failed resume, got %d", count) + } +} diff --git a/go/go.mod b/go/go.mod index c835cc889..489582545 100644 --- a/go/go.mod +++ b/go/go.mod @@ -6,3 +6,5 @@ require ( github.com/google/jsonschema-go v0.4.2 github.com/klauspost/compress v1.18.3 ) + +require github.com/google/uuid v1.6.0 diff --git a/go/go.sum b/go/go.sum index 0cc670e8f..2ae02ef35 100644 --- a/go/go.sum +++ b/go/go.sum @@ -2,5 +2,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index f04307c2d..9e1b6285e 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -810,10 +810,10 @@ func TestSession(t *testing.T) { // Verify both sessions are in the list if !contains(sessionIDs, session1.SessionID) { - t.Errorf("Expected session1 ID %s to be in sessions list", session1.SessionID) + t.Errorf("Expected session1 ID %s to be in sessions list %v", session1.SessionID, sessionIDs) } if !contains(sessionIDs, session2.SessionID) { - t.Errorf("Expected session2 ID %s to be in sessions list", session2.SessionID) + t.Errorf("Expected session2 ID %s to be in sessions list %v", session2.SessionID, sessionIDs) } // Verify session metadata structure diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index fe8655b55..eeb7ee36d 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -12,6 +12,7 @@ */ import { spawn, type ChildProcess } from "node:child_process"; +import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { Socket } from "node:net"; import { dirname, join } from "node:path"; @@ -524,40 +525,11 @@ export class CopilotClient { } } - const response = await this.connection!.sendRequest("session.create", { - model: config.model, - sessionId: config.sessionId, - clientName: config.clientName, - reasoningEffort: config.reasoningEffort, - tools: config.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - parameters: toJsonSchema(tool.parameters), - overridesBuiltInTool: tool.overridesBuiltInTool, - })), - systemMessage: config.systemMessage, - availableTools: config.availableTools, - excludedTools: config.excludedTools, - provider: config.provider, - requestPermission: true, - requestUserInput: !!config.onUserInputRequest, - hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), - workingDirectory: config.workingDirectory, - streaming: config.streaming, - mcpServers: config.mcpServers, - envValueMode: "direct", - customAgents: config.customAgents, - configDir: config.configDir, - skillDirectories: config.skillDirectories, - disabledSkills: config.disabledSkills, - infiniteSessions: config.infiniteSessions, - }); + const sessionId = config.sessionId ?? randomUUID(); - const { sessionId, workspacePath } = response as { - sessionId: string; - workspacePath?: string; - }; - const session = new CopilotSession(sessionId, this.connection!, workspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + const session = new CopilotSession(sessionId, this.connection!); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -568,6 +540,46 @@ export class CopilotClient { } this.sessions.set(sessionId, session); + try { + const response = await this.connection!.sendRequest("session.create", { + model: config.model, + sessionId, + clientName: config.clientName, + reasoningEffort: config.reasoningEffort, + tools: config.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: toJsonSchema(tool.parameters), + overridesBuiltInTool: tool.overridesBuiltInTool, + })), + systemMessage: config.systemMessage, + availableTools: config.availableTools, + excludedTools: config.excludedTools, + provider: config.provider, + requestPermission: true, + requestUserInput: !!config.onUserInputRequest, + hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), + workingDirectory: config.workingDirectory, + streaming: config.streaming, + mcpServers: config.mcpServers, + envValueMode: "direct", + customAgents: config.customAgents, + configDir: config.configDir, + skillDirectories: config.skillDirectories, + disabledSkills: config.disabledSkills, + infiniteSessions: config.infiniteSessions, + }); + + const { workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + session["_workspacePath"] = workspacePath; + } catch (e) { + this.sessions.delete(sessionId); + throw e; + } + return session; } @@ -610,41 +622,9 @@ export class CopilotClient { } } - const response = await this.connection!.sendRequest("session.resume", { - sessionId, - clientName: config.clientName, - model: config.model, - reasoningEffort: config.reasoningEffort, - systemMessage: config.systemMessage, - availableTools: config.availableTools, - excludedTools: config.excludedTools, - tools: config.tools?.map((tool) => ({ - name: tool.name, - description: tool.description, - parameters: toJsonSchema(tool.parameters), - overridesBuiltInTool: tool.overridesBuiltInTool, - })), - provider: config.provider, - requestPermission: true, - requestUserInput: !!config.onUserInputRequest, - hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), - workingDirectory: config.workingDirectory, - configDir: config.configDir, - streaming: config.streaming, - mcpServers: config.mcpServers, - envValueMode: "direct", - customAgents: config.customAgents, - skillDirectories: config.skillDirectories, - disabledSkills: config.disabledSkills, - infiniteSessions: config.infiniteSessions, - disableResume: config.disableResume, - }); - - const { sessionId: resumedSessionId, workspacePath } = response as { - sessionId: string; - workspacePath?: string; - }; - const session = new CopilotSession(resumedSessionId, this.connection!, workspacePath); + // Create and register the session before issuing the RPC so that + // events emitted by the CLI (e.g. session.start) are not dropped. + const session = new CopilotSession(sessionId, this.connection!); session.registerTools(config.tools); session.registerPermissionHandler(config.onPermissionRequest); if (config.onUserInputRequest) { @@ -653,7 +633,48 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } - this.sessions.set(resumedSessionId, session); + this.sessions.set(sessionId, session); + + try { + const response = await this.connection!.sendRequest("session.resume", { + sessionId, + clientName: config.clientName, + model: config.model, + reasoningEffort: config.reasoningEffort, + systemMessage: config.systemMessage, + availableTools: config.availableTools, + excludedTools: config.excludedTools, + tools: config.tools?.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: toJsonSchema(tool.parameters), + overridesBuiltInTool: tool.overridesBuiltInTool, + })), + provider: config.provider, + requestPermission: true, + requestUserInput: !!config.onUserInputRequest, + hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), + workingDirectory: config.workingDirectory, + configDir: config.configDir, + streaming: config.streaming, + mcpServers: config.mcpServers, + envValueMode: "direct", + customAgents: config.customAgents, + skillDirectories: config.skillDirectories, + disabledSkills: config.disabledSkills, + infiniteSessions: config.infiniteSessions, + disableResume: config.disableResume, + }); + + const { workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + session["_workspacePath"] = workspacePath; + } catch (e) { + this.sessions.delete(sessionId); + throw e; + } return session; } diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index f7b0ee585..75e4a0362 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -76,7 +76,7 @@ export class CopilotSession { constructor( public readonly sessionId: string, private connection: MessageConnection, - private readonly _workspacePath?: string + private _workspacePath?: string ) {} /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index f9618b6dd..ce7518bdd 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -294,6 +294,94 @@ describe("CopilotClient", () => { }); }); + describe("session registered before RPC", () => { + it("registers session in sessions map before session.create RPC completes", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + let sessionInMapDuringRpc = false; + vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( + async (method: string, params: any) => { + if (method === "session.create") { + sessionInMapDuringRpc = (client as any).sessions.has(params.sessionId); + return { sessionId: params.sessionId }; + } + throw new Error(`Unexpected method: ${method}`); + } + ); + + await client.createSession({ onPermissionRequest: approveAll }); + expect(sessionInMapDuringRpc).toBe(true); + }); + + it("registers session in sessions map before session.resume RPC completes", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + let sessionInMapDuringRpc = false; + vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( + async (method: string, params: any) => { + if (method === "session.resume") { + sessionInMapDuringRpc = (client as any).sessions.has(params.sessionId); + return { sessionId: params.sessionId }; + } + throw new Error(`Unexpected method: ${method}`); + } + ); + + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + expect(sessionInMapDuringRpc).toBe(true); + }); + + it("removes session from sessions map when session.create RPC fails", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( + async (method: string) => { + if (method === "session.create") { + throw new Error("RPC failed"); + } + throw new Error(`Unexpected method: ${method}`); + } + ); + + await expect(client.createSession({ onPermissionRequest: approveAll })).rejects.toThrow( + "RPC failed" + ); + expect((client as any).sessions.size).toBe(0); + }); + + it("removes session from sessions map when session.resume RPC fails", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const sessionCountBefore = (client as any).sessions.size; + + vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( + async (method: string) => { + if (method === "session.resume") { + throw new Error("RPC failed"); + } + throw new Error(`Unexpected method: ${method}`); + } + ); + + await expect( + client.resumeSession("other-session-id", { onPermissionRequest: approveAll }) + ).rejects.toThrow("RPC failed"); + expect((client as any).sessions.size).toBe(sessionCountBefore); + expect((client as any).sessions.has(session.sessionId)).toBe(true); + }); + }); + describe("overridesBuiltInTool in tool definitions", () => { it("sends overridesBuiltInTool in tool definition on session.create", async () => { const client = new CopilotClient(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 59bc9754c..4e85f3417 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -19,6 +19,7 @@ import subprocess import sys import threading +import uuid from collections.abc import Callable from dataclasses import asdict, is_dataclass from pathlib import Path @@ -480,8 +481,6 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: payload: dict[str, Any] = {} if cfg.get("model"): payload["model"] = cfg["model"] - if cfg.get("session_id"): - payload["sessionId"] = cfg["session_id"] if cfg.get("client_name"): payload["clientName"] = cfg["client_name"] if cfg.get("reasoning_effort"): @@ -577,11 +576,13 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: if not self._client: raise RuntimeError("Client not connected") - response = await self._client.request("session.create", payload) - session_id = response["sessionId"] - workspace_path = response.get("workspacePath") - session = CopilotSession(session_id, self._client, workspace_path) + session_id = cfg.get("session_id") or str(uuid.uuid4()) + payload["sessionId"] = session_id + + # Create and register the session before issuing the RPC so that + # events emitted by the CLI (e.g. session.start) are not dropped. + session = CopilotSession(session_id, self._client, None) session._register_tools(tools) session._register_permission_handler(on_permission_request) if on_user_input_request: @@ -591,6 +592,14 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: with self._sessions_lock: self._sessions[session_id] = session + try: + response = await self._client.request("session.create", payload) + session._workspace_path = response.get("workspacePath") + except BaseException: + with self._sessions_lock: + self._sessions.pop(session_id, None) + raise + return session async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession: @@ -761,11 +770,10 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> if not self._client: raise RuntimeError("Client not connected") - response = await self._client.request("session.resume", payload) - resumed_session_id = response["sessionId"] - workspace_path = response.get("workspacePath") - session = CopilotSession(resumed_session_id, self._client, workspace_path) + # Create and register the session before issuing the RPC so that + # events emitted by the CLI (e.g. session.start) are not dropped. + session = CopilotSession(session_id, self._client, None) session._register_tools(cfg.get("tools")) session._register_permission_handler(on_permission_request) if on_user_input_request: @@ -773,7 +781,15 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> if hooks: session._register_hooks(hooks) with self._sessions_lock: - self._sessions[resumed_session_id] = session + self._sessions[session_id] = session + + try: + response = await self._client.request("session.resume", payload) + session._workspace_path = response.get("workspacePath") + except BaseException: + with self._sessions_lock: + self._sessions.pop(session_id, None) + raise return session diff --git a/python/test_client.py b/python/test_client.py index 05b324228..525d3fb74 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -176,6 +176,118 @@ def test_use_logged_in_user_with_cli_url_raises(self): ) +class TestSessionRegistrationBeforeRPC: + @pytest.mark.asyncio + async def test_create_session_registers_before_rpc(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + session_registered_during_rpc = {} + original_request = client._client.request + + async def mock_request(method, params): + if method == "session.create": + with client._sessions_lock: + session_registered_during_rpc["value"] = ( + params["sessionId"] in client._sessions + ) + return {"sessionId": params["sessionId"]} + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + assert session_registered_during_rpc["value"] is True + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_registers_before_rpc(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + session_registered_during_rpc = {} + original_request = client._client.request + + async def mock_request(method, params): + if method == "session.resume": + with client._sessions_lock: + session_registered_during_rpc["value"] = ( + params["sessionId"] in client._sessions + ) + return {"sessionId": params["sessionId"]} + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session( + session.session_id, + {"on_permission_request": PermissionHandler.approve_all}, + ) + assert session_registered_during_rpc["value"] is True + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_create_session_cleans_up_on_rpc_failure(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + original_request = client._client.request + + async def mock_request(method, params): + if method == "session.create": + raise RuntimeError("RPC failed") + return await original_request(method, params) + + client._client.request = mock_request + with pytest.raises(RuntimeError, match="RPC failed"): + await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + with client._sessions_lock: + assert len(client._sessions) == 0 + finally: + client._client.request = original_request + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_cleans_up_on_rpc_failure(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + with client._sessions_lock: + sessions_before = len(client._sessions) + + original_request = client._client.request + + async def mock_request(method, params): + if method == "session.resume": + raise RuntimeError("RPC failed") + return await original_request(method, params) + + client._client.request = mock_request + with pytest.raises(RuntimeError, match="RPC failed"): + await client.resume_session( + "other-session-id", + {"on_permission_request": PermissionHandler.approve_all}, + ) + with client._sessions_lock: + assert len(client._sessions) == sessions_before + assert session.session_id in client._sessions + finally: + await client.force_stop() + + class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): diff --git a/test/scenarios/auth/byok-anthropic/go/go.mod b/test/scenarios/auth/byok-anthropic/go/go.mod index 9a727c69c..005601ee3 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.mod +++ b/test/scenarios/auth/byok-anthropic/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-anthropic/go/go.sum b/test/scenarios/auth/byok-anthropic/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/auth/byok-anthropic/go/go.sum +++ b/test/scenarios/auth/byok-anthropic/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-azure/go/go.mod b/test/scenarios/auth/byok-azure/go/go.mod index f0dd08661..21997114b 100644 --- a/test/scenarios/auth/byok-azure/go/go.mod +++ b/test/scenarios/auth/byok-azure/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-azure/go/go.sum b/test/scenarios/auth/byok-azure/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/auth/byok-azure/go/go.sum +++ b/test/scenarios/auth/byok-azure/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-ollama/go/go.mod b/test/scenarios/auth/byok-ollama/go/go.mod index 806aaa5c2..a6891a811 100644 --- a/test/scenarios/auth/byok-ollama/go/go.mod +++ b/test/scenarios/auth/byok-ollama/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-ollama/go/go.sum b/test/scenarios/auth/byok-ollama/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/auth/byok-ollama/go/go.sum +++ b/test/scenarios/auth/byok-ollama/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/byok-openai/go/go.mod b/test/scenarios/auth/byok-openai/go/go.mod index 2d5a75ecf..65b3c9028 100644 --- a/test/scenarios/auth/byok-openai/go/go.mod +++ b/test/scenarios/auth/byok-openai/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/byok-openai/go/go.sum b/test/scenarios/auth/byok-openai/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/auth/byok-openai/go/go.sum +++ b/test/scenarios/auth/byok-openai/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/auth/gh-app/go/go.mod b/test/scenarios/auth/gh-app/go/go.mod index a0d270c6e..7012daa68 100644 --- a/test/scenarios/auth/gh-app/go/go.mod +++ b/test/scenarios/auth/gh-app/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/auth/gh-app/go/go.sum b/test/scenarios/auth/gh-app/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/auth/gh-app/go/go.sum +++ b/test/scenarios/auth/gh-app/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.mod b/test/scenarios/bundling/app-backend-to-server/go/go.mod index 6d01df73b..c225d6a2c 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.mod +++ b/test/scenarios/bundling/app-backend-to-server/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-backend-to-server/go/go.sum b/test/scenarios/bundling/app-backend-to-server/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/go.sum +++ b/test/scenarios/bundling/app-backend-to-server/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/app-direct-server/go/go.mod b/test/scenarios/bundling/app-direct-server/go/go.mod index db24ae393..e36e0f50d 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.mod +++ b/test/scenarios/bundling/app-direct-server/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/app-direct-server/go/go.sum b/test/scenarios/bundling/app-direct-server/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/bundling/app-direct-server/go/go.sum +++ b/test/scenarios/bundling/app-direct-server/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/container-proxy/go/go.mod b/test/scenarios/bundling/container-proxy/go/go.mod index 086f43175..270a60c61 100644 --- a/test/scenarios/bundling/container-proxy/go/go.mod +++ b/test/scenarios/bundling/container-proxy/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/container-proxy/go/go.sum b/test/scenarios/bundling/container-proxy/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/bundling/container-proxy/go/go.sum +++ b/test/scenarios/bundling/container-proxy/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/bundling/fully-bundled/go/go.mod b/test/scenarios/bundling/fully-bundled/go/go.mod index 93af1915a..5c7d03b11 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.mod +++ b/test/scenarios/bundling/fully-bundled/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/bundling/fully-bundled/go/go.sum b/test/scenarios/bundling/fully-bundled/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/bundling/fully-bundled/go/go.sum +++ b/test/scenarios/bundling/fully-bundled/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/hooks/go/go.mod b/test/scenarios/callbacks/hooks/go/go.mod index 51b27e491..3220cd506 100644 --- a/test/scenarios/callbacks/hooks/go/go.mod +++ b/test/scenarios/callbacks/hooks/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/hooks/go/go.sum b/test/scenarios/callbacks/hooks/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/callbacks/hooks/go/go.sum +++ b/test/scenarios/callbacks/hooks/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/permissions/go/go.mod b/test/scenarios/callbacks/permissions/go/go.mod index 25eb7d22a..bf88ca7ec 100644 --- a/test/scenarios/callbacks/permissions/go/go.mod +++ b/test/scenarios/callbacks/permissions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/permissions/go/go.sum b/test/scenarios/callbacks/permissions/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/callbacks/permissions/go/go.sum +++ b/test/scenarios/callbacks/permissions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/callbacks/user-input/go/go.mod b/test/scenarios/callbacks/user-input/go/go.mod index 11419b634..b050ef88b 100644 --- a/test/scenarios/callbacks/user-input/go/go.mod +++ b/test/scenarios/callbacks/user-input/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/callbacks/user-input/go/go.sum b/test/scenarios/callbacks/user-input/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/callbacks/user-input/go/go.sum +++ b/test/scenarios/callbacks/user-input/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/modes/default/go/go.mod b/test/scenarios/modes/default/go/go.mod index 50b92181f..5ce3524d7 100644 --- a/test/scenarios/modes/default/go/go.mod +++ b/test/scenarios/modes/default/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/default/go/go.sum b/test/scenarios/modes/default/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/modes/default/go/go.sum +++ b/test/scenarios/modes/default/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/modes/minimal/go/go.mod b/test/scenarios/modes/minimal/go/go.mod index 72fbe3540..c8eb4bbfd 100644 --- a/test/scenarios/modes/minimal/go/go.mod +++ b/test/scenarios/modes/minimal/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/modes/minimal/go/go.sum b/test/scenarios/modes/minimal/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/modes/minimal/go/go.sum +++ b/test/scenarios/modes/minimal/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/attachments/go/go.mod b/test/scenarios/prompts/attachments/go/go.mod index 0a5dc6c1f..22aa80a14 100644 --- a/test/scenarios/prompts/attachments/go/go.mod +++ b/test/scenarios/prompts/attachments/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/attachments/go/go.sum b/test/scenarios/prompts/attachments/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/prompts/attachments/go/go.sum +++ b/test/scenarios/prompts/attachments/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/reasoning-effort/go/go.mod b/test/scenarios/prompts/reasoning-effort/go/go.mod index f2aa4740c..b3fafcc1c 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.mod +++ b/test/scenarios/prompts/reasoning-effort/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/reasoning-effort/go/go.sum b/test/scenarios/prompts/reasoning-effort/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/prompts/reasoning-effort/go/go.sum +++ b/test/scenarios/prompts/reasoning-effort/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/prompts/system-message/go/go.mod b/test/scenarios/prompts/system-message/go/go.mod index b8301c15a..8bc1c55ce 100644 --- a/test/scenarios/prompts/system-message/go/go.mod +++ b/test/scenarios/prompts/system-message/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/prompts/system-message/go/go.sum b/test/scenarios/prompts/system-message/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/prompts/system-message/go/go.sum +++ b/test/scenarios/prompts/system-message/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.mod b/test/scenarios/sessions/concurrent-sessions/go/go.mod index c01642320..a69dedd16 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.mod +++ b/test/scenarios/sessions/concurrent-sessions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/concurrent-sessions/go/go.sum b/test/scenarios/sessions/concurrent-sessions/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/go.sum +++ b/test/scenarios/sessions/concurrent-sessions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/infinite-sessions/go/go.mod b/test/scenarios/sessions/infinite-sessions/go/go.mod index cb8d2713d..15f8e48f7 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.mod +++ b/test/scenarios/sessions/infinite-sessions/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/infinite-sessions/go/go.sum b/test/scenarios/sessions/infinite-sessions/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/sessions/infinite-sessions/go/go.sum +++ b/test/scenarios/sessions/infinite-sessions/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/session-resume/go/go.mod b/test/scenarios/sessions/session-resume/go/go.mod index 3722b78d2..ab1b82c39 100644 --- a/test/scenarios/sessions/session-resume/go/go.mod +++ b/test/scenarios/sessions/session-resume/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/session-resume/go/go.sum b/test/scenarios/sessions/session-resume/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/sessions/session-resume/go/go.sum +++ b/test/scenarios/sessions/session-resume/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/sessions/streaming/go/go.mod b/test/scenarios/sessions/streaming/go/go.mod index acb516379..f6c553680 100644 --- a/test/scenarios/sessions/streaming/go/go.mod +++ b/test/scenarios/sessions/streaming/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/sessions/streaming/go/go.sum b/test/scenarios/sessions/streaming/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/sessions/streaming/go/go.sum +++ b/test/scenarios/sessions/streaming/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/custom-agents/go/go.mod b/test/scenarios/tools/custom-agents/go/go.mod index 9acbccb06..f6f670b8c 100644 --- a/test/scenarios/tools/custom-agents/go/go.mod +++ b/test/scenarios/tools/custom-agents/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/custom-agents/go/go.sum b/test/scenarios/tools/custom-agents/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/custom-agents/go/go.sum +++ b/test/scenarios/tools/custom-agents/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/mcp-servers/go/go.mod b/test/scenarios/tools/mcp-servers/go/go.mod index 4b93e09e7..65de0a40b 100644 --- a/test/scenarios/tools/mcp-servers/go/go.mod +++ b/test/scenarios/tools/mcp-servers/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/mcp-servers/go/go.sum b/test/scenarios/tools/mcp-servers/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/mcp-servers/go/go.sum +++ b/test/scenarios/tools/mcp-servers/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/no-tools/go/go.mod b/test/scenarios/tools/no-tools/go/go.mod index 74131d3e6..387c1b51d 100644 --- a/test/scenarios/tools/no-tools/go/go.mod +++ b/test/scenarios/tools/no-tools/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/no-tools/go/go.sum b/test/scenarios/tools/no-tools/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/no-tools/go/go.sum +++ b/test/scenarios/tools/no-tools/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/skills/go/go.mod b/test/scenarios/tools/skills/go/go.mod index 1467fd64f..ad94ef6b7 100644 --- a/test/scenarios/tools/skills/go/go.mod +++ b/test/scenarios/tools/skills/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/skills/go/go.sum b/test/scenarios/tools/skills/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/skills/go/go.sum +++ b/test/scenarios/tools/skills/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/tool-filtering/go/go.mod b/test/scenarios/tools/tool-filtering/go/go.mod index c3051c52b..ad36d3f63 100644 --- a/test/scenarios/tools/tool-filtering/go/go.mod +++ b/test/scenarios/tools/tool-filtering/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-filtering/go/go.sum b/test/scenarios/tools/tool-filtering/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/tool-filtering/go/go.sum +++ b/test/scenarios/tools/tool-filtering/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/tool-overrides/go/go.mod b/test/scenarios/tools/tool-overrides/go/go.mod index 353066761..ba48b0e7b 100644 --- a/test/scenarios/tools/tool-overrides/go/go.mod +++ b/test/scenarios/tools/tool-overrides/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/tool-overrides/go/go.sum b/test/scenarios/tools/tool-overrides/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/tool-overrides/go/go.sum +++ b/test/scenarios/tools/tool-overrides/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/tools/virtual-filesystem/go/go.mod b/test/scenarios/tools/virtual-filesystem/go/go.mod index d6606bb7b..e5f121611 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.mod +++ b/test/scenarios/tools/virtual-filesystem/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/tools/virtual-filesystem/go/go.sum b/test/scenarios/tools/virtual-filesystem/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/tools/virtual-filesystem/go/go.sum +++ b/test/scenarios/tools/virtual-filesystem/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/reconnect/go/go.mod b/test/scenarios/transport/reconnect/go/go.mod index 7a1f80d6c..e1267bb72 100644 --- a/test/scenarios/transport/reconnect/go/go.mod +++ b/test/scenarios/transport/reconnect/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/reconnect/go/go.sum b/test/scenarios/transport/reconnect/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/transport/reconnect/go/go.sum +++ b/test/scenarios/transport/reconnect/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/stdio/go/go.mod b/test/scenarios/transport/stdio/go/go.mod index 2dcc35310..63ad24bee 100644 --- a/test/scenarios/transport/stdio/go/go.mod +++ b/test/scenarios/transport/stdio/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/stdio/go/go.sum b/test/scenarios/transport/stdio/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/transport/stdio/go/go.sum +++ b/test/scenarios/transport/stdio/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/test/scenarios/transport/tcp/go/go.mod b/test/scenarios/transport/tcp/go/go.mod index dc1a0b6f9..85fac7926 100644 --- a/test/scenarios/transport/tcp/go/go.mod +++ b/test/scenarios/transport/tcp/go/go.mod @@ -4,6 +4,9 @@ go 1.24 require github.com/github/copilot-sdk/go v0.0.0 -require github.com/google/jsonschema-go v0.4.2 // indirect +require ( + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/google/uuid v1.6.0 // indirect +) replace github.com/github/copilot-sdk/go => ../../../../../go diff --git a/test/scenarios/transport/tcp/go/go.sum b/test/scenarios/transport/tcp/go/go.sum index 6e171099c..6029a9b71 100644 --- a/test/scenarios/transport/tcp/go/go.sum +++ b/test/scenarios/transport/tcp/go/go.sum @@ -2,3 +2,5 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From ad5b4d399991372bbe6fef59e9f0d1f9fc2f6546 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 4 Mar 2026 16:15:14 -0500 Subject: [PATCH 2/3] Add SessionConfig.OnEvent --- dotnet/src/Client.cs | 8 ++ dotnet/src/Types.cs | 20 +++ dotnet/test/SessionTests.cs | 12 +- go/client.go | 6 + go/client_test.go | 232 -------------------------------- go/internal/e2e/session_test.go | 22 ++- go/types.go | 13 +- nodejs/src/client.ts | 6 + nodejs/src/types.ts | 12 ++ nodejs/test/client.test.ts | 88 ------------ nodejs/test/e2e/session.test.ts | 14 +- python/copilot/client.py | 6 + python/copilot/types.py | 11 +- python/e2e/test_session.py | 16 ++- python/test_client.py | 112 --------------- 15 files changed, 135 insertions(+), 443 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 25f9f0d31..eb2e8c0b5 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -396,6 +396,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance { session.RegisterHooks(config.Hooks); } + if (config.OnEvent != null) + { + session.On(config.OnEvent); + } _sessions[sessionId] = session; try @@ -495,6 +499,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes { session.RegisterHooks(config.Hooks); } + if (config.OnEvent != null) + { + session.On(config.OnEvent); + } _sessions[sessionId] = session; try diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 97f5ebbbc..b2300c21b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -766,6 +766,7 @@ protected SessionConfig(SessionConfig? other) ? new Dictionary(other.McpServers, other.McpServers.Comparer) : null; Model = other.Model; + OnEvent = other.OnEvent; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -864,6 +865,18 @@ protected SessionConfig(SessionConfig? other) /// public InfiniteSessionConfig? InfiniteSessions { get; set; } + /// + /// Optional event handler that is registered on the session before the + /// session.create RPC is issued. + /// + /// + /// Equivalent to calling immediately + /// after creation, but executes earlier in the lifecycle so no events are missed. + /// Using this property rather than guarantees that early events emitted + /// by the CLI during session creation (e.g. session.start) are delivered to the handler. + /// + public SessionEventHandler? OnEvent { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -905,6 +918,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) ? new Dictionary(other.McpServers, other.McpServers.Comparer) : null; Model = other.Model; + OnEvent = other.OnEvent; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -1020,6 +1034,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public InfiniteSessionConfig? InfiniteSessions { get; set; } + /// + /// Optional event handler registered before the session.resume RPC is issued, + /// ensuring early events are delivered. See . + /// + public SessionEventHandler? OnEvent { get; set; } + /// /// Creates a shallow clone of this instance. /// diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index eac00b06e..f69fcc96d 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -296,7 +296,17 @@ public async Task Should_Pass_Streaming_Option_To_Session_Creation() [Fact] public async Task Should_Receive_Session_Events() { - var session = await CreateSessionAsync(); + // Use OnEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + var earlyEvents = new List(); + var session = await CreateSessionAsync(new SessionConfig + { + OnEvent = evt => earlyEvents.Add(evt), + }); + + Assert.Contains(earlyEvents, evt => evt is SessionStartEvent); + var receivedEvents = new List(); var idleReceived = new TaskCompletionSource(); diff --git a/go/client.go b/go/client.go index 8a385ee05..3a98236f5 100644 --- a/go/client.go +++ b/go/client.go @@ -536,6 +536,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.Hooks != nil { session.registerHooks(config.Hooks) } + if config.OnEvent != nil { + session.On(config.OnEvent) + } c.sessionsMux.Lock() c.sessions[sessionID] = session @@ -645,6 +648,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.Hooks != nil { session.registerHooks(config.Hooks) } + if config.OnEvent != nil { + session.On(config.OnEvent) + } c.sessionsMux.Lock() c.sessions[sessionID] = session diff --git a/go/client_test.go b/go/client_test.go index 16285e98f..d791a5a30 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,18 +1,13 @@ package copilot import ( - "bufio" "encoding/json" - "fmt" - "io" "os" "path/filepath" "reflect" "regexp" "sync" "testing" - - "github.com/github/copilot-sdk/go/internal/jsonrpc2" ) // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.go instead @@ -574,230 +569,3 @@ func TestClient_StartStopRace(t *testing.T) { t.Fatal(err) } } - -// fakeJSONRPCServer reads one JSON-RPC request from r and sends a response to w. -// onRequest is called with the parsed method and params before the response is sent, -// allowing the caller to inspect state (e.g. the sessions map) during the RPC. -func fakeJSONRPCServer(t *testing.T, r io.Reader, w io.WriteCloser, onRequest func(method string, params json.RawMessage)) { - t.Helper() - reader := bufio.NewReader(r) - - // Read Content-Length header - var contentLength int - for { - line, err := reader.ReadString('\n') - if err != nil { - t.Errorf("failed to read header: %v", err) - w.Close() - return - } - if line == "\r\n" || line == "\n" { - break - } - fmt.Sscanf(line, "Content-Length: %d", &contentLength) - } - - // Read body - body := make([]byte, contentLength) - if _, err := io.ReadFull(reader, body); err != nil { - t.Errorf("failed to read body: %v", err) - w.Close() - return - } - - // Parse request - var req struct { - ID json.RawMessage `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` - } - if err := json.Unmarshal(body, &req); err != nil { - t.Errorf("failed to unmarshal request: %v", err) - w.Close() - return - } - - onRequest(req.Method, req.Params) - - // Echo sessionId from request params - var params struct { - SessionID string `json:"sessionId"` - } - json.Unmarshal(req.Params, ¶ms) - - result, _ := json.Marshal(map[string]any{"sessionId": params.SessionID, "workspacePath": "/tmp"}) - resp, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "id": req.ID, - "result": json.RawMessage(result), - }) - header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(resp)) - w.Write([]byte(header)) - w.Write(resp) -} - -// fakeJSONRPCErrorServer reads one JSON-RPC request and returns an error response. -func fakeJSONRPCErrorServer(t *testing.T, r io.Reader, w io.WriteCloser) { - t.Helper() - reader := bufio.NewReader(r) - - var contentLength int - for { - line, err := reader.ReadString('\n') - if err != nil { - w.Close() - return - } - if line == "\r\n" || line == "\n" { - break - } - fmt.Sscanf(line, "Content-Length: %d", &contentLength) - } - - body := make([]byte, contentLength) - if _, err := io.ReadFull(reader, body); err != nil { - w.Close() - return - } - - var req struct { - ID json.RawMessage `json:"id"` - } - json.Unmarshal(body, &req) - - resp, _ := json.Marshal(map[string]any{ - "jsonrpc": "2.0", - "id": req.ID, - "error": map[string]any{"code": -32000, "message": "test error"}, - }) - header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(resp)) - w.Write([]byte(header)) - w.Write(resp) -} - -// newTestClientWithFakeServer creates a Client wired to a fake jsonrpc2.Client -// backed by the provided io pipes. The caller must call jrpcClient.Stop() when done. -func newTestClientWithFakeServer(clientWriter io.WriteCloser, clientReader io.ReadCloser) (*Client, *jsonrpc2.Client) { - jrpcClient := jsonrpc2.NewClient(clientWriter, clientReader) - jrpcClient.Start() - - client := NewClient(nil) - client.client = jrpcClient - client.state = StateConnected - client.sessions = make(map[string]*Session) - return client, jrpcClient -} - -func TestClient_CreateSession_RegistersSessionBeforeRPC(t *testing.T) { - // Create pipes: client writes to serverReader, server writes to clientReader - serverReader, clientWriter := io.Pipe() - clientReader, serverWriter := io.Pipe() - client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) - defer jrpcClient.Stop() - - sessionInMap := false - go fakeJSONRPCServer(t, serverReader, serverWriter, func(method string, params json.RawMessage) { - if method != "session.create" { - t.Errorf("expected session.create, got %s", method) - } - var p struct { - SessionID string `json:"sessionId"` - } - json.Unmarshal(params, &p) - client.sessionsMux.Lock() - _, sessionInMap = client.sessions[p.SessionID] - client.sessionsMux.Unlock() - }) - - session, err := client.CreateSession(t.Context(), &SessionConfig{ - OnPermissionRequest: PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - if session == nil { - t.Fatal("expected non-nil session") - } - if !sessionInMap { - t.Error("session was not in sessions map when session.create RPC was issued") - } -} - -func TestClient_ResumeSession_RegistersSessionBeforeRPC(t *testing.T) { - serverReader, clientWriter := io.Pipe() - clientReader, serverWriter := io.Pipe() - client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) - defer jrpcClient.Stop() - - sessionInMap := false - go fakeJSONRPCServer(t, serverReader, serverWriter, func(method string, params json.RawMessage) { - if method != "session.resume" { - t.Errorf("expected session.resume, got %s", method) - } - var p struct { - SessionID string `json:"sessionId"` - } - json.Unmarshal(params, &p) - client.sessionsMux.Lock() - _, sessionInMap = client.sessions[p.SessionID] - client.sessionsMux.Unlock() - }) - - session, err := client.ResumeSessionWithOptions(t.Context(), "test-session-id", &ResumeSessionConfig{ - OnPermissionRequest: PermissionHandler.ApproveAll, - }) - if err != nil { - t.Fatalf("ResumeSessionWithOptions failed: %v", err) - } - if session == nil { - t.Fatal("expected non-nil session") - } - if !sessionInMap { - t.Error("session was not in sessions map when session.resume RPC was issued") - } -} - -func TestClient_CreateSession_CleansUpOnRPCFailure(t *testing.T) { - serverReader, clientWriter := io.Pipe() - clientReader, serverWriter := io.Pipe() - client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) - defer jrpcClient.Stop() - - // Send a JSON-RPC error response to simulate failure - go fakeJSONRPCErrorServer(t, serverReader, serverWriter) - - _, err := client.CreateSession(t.Context(), &SessionConfig{ - OnPermissionRequest: PermissionHandler.ApproveAll, - }) - if err == nil { - t.Fatal("expected error from CreateSession") - } - client.sessionsMux.Lock() - count := len(client.sessions) - client.sessionsMux.Unlock() - if count != 0 { - t.Errorf("expected 0 sessions after failed create, got %d", count) - } -} - -func TestClient_ResumeSession_CleansUpOnRPCFailure(t *testing.T) { - serverReader, clientWriter := io.Pipe() - clientReader, serverWriter := io.Pipe() - client, jrpcClient := newTestClientWithFakeServer(clientWriter, clientReader) - defer jrpcClient.Stop() - - go fakeJSONRPCErrorServer(t, serverReader, serverWriter) - - _, err := client.ResumeSessionWithOptions(t.Context(), "test-session-id", &ResumeSessionConfig{ - OnPermissionRequest: PermissionHandler.ApproveAll, - }) - if err == nil { - t.Fatal("expected error from ResumeSessionWithOptions") - } - client.sessionsMux.Lock() - count := len(client.sessions) - client.sessionsMux.Unlock() - if count != 0 { - t.Errorf("expected 0 sessions after failed resume, got %d", count) - } -} diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index 9e1b6285e..4c0d727a1 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -661,11 +661,31 @@ func TestSession(t *testing.T) { t.Run("should receive session events", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}) + // Use OnEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + var earlyEvents []copilot.SessionEvent + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + OnEvent: func(event copilot.SessionEvent) { + earlyEvents = append(earlyEvents, event) + }, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } + hasSessionStart := false + for _, evt := range earlyEvents { + if evt.Type == "session.start" { + hasSessionStart = true + break + } + } + if !hasSessionStart { + t.Error("Expected session.start event via OnEvent during creation") + } + var receivedEvents []copilot.SessionEvent idle := make(chan bool) diff --git a/go/types.go b/go/types.go index 8f034db7d..7dc71bf95 100644 --- a/go/types.go +++ b/go/types.go @@ -406,9 +406,13 @@ type SessionConfig struct { // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. // When enabled (default), sessions automatically manage context limits and persist state. InfiniteSessions *InfiniteSessionConfig + // OnEvent is an optional event handler that is registered on the session before + // the session.create RPC is issued. This guarantees that early events emitted + // by the CLI during session creation (e.g. session.start) are delivered to the + // handler. Equivalent to calling session.On(handler) immediately after creation, + // but executes earlier in the lifecycle so no events are missed. + OnEvent SessionEventHandler } - -// Tool describes a caller-implemented tool that can be invoked by Copilot type Tool struct { Name string `json:"name"` Description string `json:"description,omitempty"` @@ -491,9 +495,10 @@ type ResumeSessionConfig struct { // DisableResume, when true, skips emitting the session.resume event. // Useful for reconnecting to a session without triggering resume-related side effects. DisableResume bool + // OnEvent is an optional event handler registered before the session.resume RPC + // is issued, ensuring early events are delivered. See SessionConfig.OnEvent. + OnEvent SessionEventHandler } - -// ProviderConfig configures a custom model provider type ProviderConfig struct { // Type is the provider type: "openai", "azure", or "anthropic". Defaults to "openai". Type string `json:"type,omitempty"` diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index eeb7ee36d..639d09f76 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -538,6 +538,9 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + if (config.onEvent) { + session.on(config.onEvent); + } this.sessions.set(sessionId, session); try { @@ -633,6 +636,9 @@ export class CopilotClient { if (config.hooks) { session.registerHooks(config.hooks); } + if (config.onEvent) { + session.on(config.onEvent); + } this.sessions.set(sessionId, session); try { diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 482216a98..39a7c6f49 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -738,6 +738,17 @@ export interface SessionConfig { * Set to `{ enabled: false }` to disable. */ infiniteSessions?: InfiniteSessionConfig; + + /** + * Optional event handler that is registered on the session before the + * session.create RPC is issued. This guarantees that early events emitted + * by the CLI during session creation (e.g. session.start) are delivered to + * the handler. + * + * Equivalent to calling `session.on(handler)` immediately after creation, + * but executes earlier in the lifecycle so no events are missed. + */ + onEvent?: SessionEventHandler; } /** @@ -764,6 +775,7 @@ export type ResumeSessionConfig = Pick< | "skillDirectories" | "disabledSkills" | "infiniteSessions" + | "onEvent" > & { /** * When true, skips emitting the session.resume event. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index ce7518bdd..f9618b6dd 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -294,94 +294,6 @@ describe("CopilotClient", () => { }); }); - describe("session registered before RPC", () => { - it("registers session in sessions map before session.create RPC completes", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => client.forceStop()); - - let sessionInMapDuringRpc = false; - vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( - async (method: string, params: any) => { - if (method === "session.create") { - sessionInMapDuringRpc = (client as any).sessions.has(params.sessionId); - return { sessionId: params.sessionId }; - } - throw new Error(`Unexpected method: ${method}`); - } - ); - - await client.createSession({ onPermissionRequest: approveAll }); - expect(sessionInMapDuringRpc).toBe(true); - }); - - it("registers session in sessions map before session.resume RPC completes", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => client.forceStop()); - - const session = await client.createSession({ onPermissionRequest: approveAll }); - - let sessionInMapDuringRpc = false; - vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( - async (method: string, params: any) => { - if (method === "session.resume") { - sessionInMapDuringRpc = (client as any).sessions.has(params.sessionId); - return { sessionId: params.sessionId }; - } - throw new Error(`Unexpected method: ${method}`); - } - ); - - await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); - expect(sessionInMapDuringRpc).toBe(true); - }); - - it("removes session from sessions map when session.create RPC fails", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => client.forceStop()); - - vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( - async (method: string) => { - if (method === "session.create") { - throw new Error("RPC failed"); - } - throw new Error(`Unexpected method: ${method}`); - } - ); - - await expect(client.createSession({ onPermissionRequest: approveAll })).rejects.toThrow( - "RPC failed" - ); - expect((client as any).sessions.size).toBe(0); - }); - - it("removes session from sessions map when session.resume RPC fails", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => client.forceStop()); - - const session = await client.createSession({ onPermissionRequest: approveAll }); - const sessionCountBefore = (client as any).sessions.size; - - vi.spyOn((client as any).connection!, "sendRequest").mockImplementation( - async (method: string) => { - if (method === "session.resume") { - throw new Error("RPC failed"); - } - throw new Error(`Unexpected method: ${method}`); - } - ); - - await expect( - client.resumeSession("other-session-id", { onPermissionRequest: approveAll }) - ).rejects.toThrow("RPC failed"); - expect((client as any).sessions.size).toBe(sessionCountBefore); - expect((client as any).sessions.has(session.sessionId)).toBe(true); - }); - }); - describe("overridesBuiltInTool in tool definitions", () => { it("sends overridesBuiltInTool in tool definition on session.create", async () => { const client = new CopilotClient(); diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 93731d617..cd9a71d89 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -335,7 +335,19 @@ describe("Sessions", async () => { }); it("should receive session events", async () => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + // Use onEvent to capture events dispatched during session creation. + // session.start is emitted during the session.create RPC; if the session + // weren't registered in the sessions map before the RPC, it would be dropped. + const earlyEvents: Array<{ type: string }> = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + onEvent: (event) => { + earlyEvents.push(event); + }, + }); + + expect(earlyEvents.some((e) => e.type === "session.start")).toBe(true); + const receivedEvents: Array<{ type: string }> = []; session.on((event) => { diff --git a/python/copilot/client.py b/python/copilot/client.py index 4e85f3417..ad8197f68 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -589,6 +589,9 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + on_event = cfg.get("on_event") + if on_event: + session.on(on_event) with self._sessions_lock: self._sessions[session_id] = session @@ -780,6 +783,9 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> session._register_user_input_handler(on_user_input_request) if hooks: session._register_hooks(hooks) + on_event = cfg.get("on_event") + if on_event: + session.on(on_event) with self._sessions_lock: self._sessions[session_id] = session diff --git a/python/copilot/types.py b/python/copilot/types.py index 42006d6b4..e3a37f9a5 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -507,9 +507,11 @@ class SessionConfig(TypedDict, total=False): # When enabled (default), sessions automatically manage context limits and persist state. # Set to {"enabled": False} to disable. infinite_sessions: InfiniteSessionConfig - - -# Azure-specific provider options + # Optional event handler that is registered on the session before the + # session.create RPC is issued, ensuring early events (e.g. session.start) + # are delivered. Equivalent to calling session.on(handler) immediately + # after creation, but executes earlier in the lifecycle so no events are missed. + on_event: Callable[["SessionEvent"], None] class AzureProviderOptions(TypedDict, total=False): """Azure-specific provider configuration""" @@ -573,6 +575,9 @@ class ResumeSessionConfig(TypedDict, total=False): # When True, skips emitting the session.resume event. # Useful for reconnecting to a session without triggering resume-related side effects. disable_resume: bool + # Optional event handler registered before the session.resume RPC is issued, + # ensuring early events are delivered. See SessionConfig.on_event. + on_event: Callable[["SessionEvent"], None] # Options for sending a message to a session diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 13e749507..f630f3482 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -481,9 +481,23 @@ async def test_should_pass_streaming_option_to_session_creation(self, ctx: E2ETe async def test_should_receive_session_events(self, ctx: E2ETestContext): import asyncio + # Use on_event to capture events dispatched during session creation. + # session.start is emitted during the session.create RPC; if the session + # weren't registered in the sessions map before the RPC, it would be dropped. + early_events = [] + + def capture_early(event): + early_events.append(event) + session = await ctx.client.create_session( - {"on_permission_request": PermissionHandler.approve_all} + { + "on_permission_request": PermissionHandler.approve_all, + "on_event": capture_early, + } ) + + assert any(e.type.value == "session.start" for e in early_events) + received_events = [] idle_event = asyncio.Event() diff --git a/python/test_client.py b/python/test_client.py index 525d3fb74..05b324228 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -176,118 +176,6 @@ def test_use_logged_in_user_with_cli_url_raises(self): ) -class TestSessionRegistrationBeforeRPC: - @pytest.mark.asyncio - async def test_create_session_registers_before_rpc(self): - client = CopilotClient({"cli_path": CLI_PATH}) - await client.start() - - try: - session_registered_during_rpc = {} - original_request = client._client.request - - async def mock_request(method, params): - if method == "session.create": - with client._sessions_lock: - session_registered_during_rpc["value"] = ( - params["sessionId"] in client._sessions - ) - return {"sessionId": params["sessionId"]} - return await original_request(method, params) - - client._client.request = mock_request - await client.create_session({"on_permission_request": PermissionHandler.approve_all}) - assert session_registered_during_rpc["value"] is True - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_resume_session_registers_before_rpc(self): - client = CopilotClient({"cli_path": CLI_PATH}) - await client.start() - - try: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) - - session_registered_during_rpc = {} - original_request = client._client.request - - async def mock_request(method, params): - if method == "session.resume": - with client._sessions_lock: - session_registered_during_rpc["value"] = ( - params["sessionId"] in client._sessions - ) - return {"sessionId": params["sessionId"]} - return await original_request(method, params) - - client._client.request = mock_request - await client.resume_session( - session.session_id, - {"on_permission_request": PermissionHandler.approve_all}, - ) - assert session_registered_during_rpc["value"] is True - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_create_session_cleans_up_on_rpc_failure(self): - client = CopilotClient({"cli_path": CLI_PATH}) - await client.start() - - try: - original_request = client._client.request - - async def mock_request(method, params): - if method == "session.create": - raise RuntimeError("RPC failed") - return await original_request(method, params) - - client._client.request = mock_request - with pytest.raises(RuntimeError, match="RPC failed"): - await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) - with client._sessions_lock: - assert len(client._sessions) == 0 - finally: - client._client.request = original_request - await client.force_stop() - - @pytest.mark.asyncio - async def test_resume_session_cleans_up_on_rpc_failure(self): - client = CopilotClient({"cli_path": CLI_PATH}) - await client.start() - - try: - session = await client.create_session( - {"on_permission_request": PermissionHandler.approve_all} - ) - with client._sessions_lock: - sessions_before = len(client._sessions) - - original_request = client._client.request - - async def mock_request(method, params): - if method == "session.resume": - raise RuntimeError("RPC failed") - return await original_request(method, params) - - client._client.request = mock_request - with pytest.raises(RuntimeError, match="RPC failed"): - await client.resume_session( - "other-session-id", - {"on_permission_request": PermissionHandler.approve_all}, - ) - with client._sessions_lock: - assert len(client._sessions) == sessions_before - assert session.session_id in client._sessions - finally: - await client.force_stop() - - class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): From c4c149f8e5261c6fcb5ae7ad1f47e611bd3d7dd0 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 4 Mar 2026 17:44:26 -0500 Subject: [PATCH 3/3] Fix Python formatting and lint in types.py - Add missing blank lines between SessionConfig and AzureProviderOptions (ruff format) - Remove unnecessary quotes on SessionEvent type annotations (ruff lint UP037) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/copilot/types.py b/python/copilot/types.py index e3a37f9a5..579b93129 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -511,7 +511,9 @@ class SessionConfig(TypedDict, total=False): # session.create RPC is issued, ensuring early events (e.g. session.start) # are delivered. Equivalent to calling session.on(handler) immediately # after creation, but executes earlier in the lifecycle so no events are missed. - on_event: Callable[["SessionEvent"], None] + on_event: Callable[[SessionEvent], None] + + class AzureProviderOptions(TypedDict, total=False): """Azure-specific provider configuration""" @@ -577,7 +579,7 @@ class ResumeSessionConfig(TypedDict, total=False): disable_resume: bool # Optional event handler registered before the session.resume RPC is issued, # ensuring early events are delivered. See SessionConfig.on_event. - on_event: Callable[["SessionEvent"], None] + on_event: Callable[[SessionEvent], None] # Options for sending a message to a session