Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,369 changes: 7,030 additions & 3,339 deletions dotnet/src/Generated/Rpc.cs

Large diffs are not rendered by default.

409 changes: 406 additions & 3 deletions dotnet/src/Generated/SessionEvents.cs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,7 @@ await Rpc.Model.SwitchToAsync(
null,
options.ModelCapabilities,
options.ContextTier,
null,
cancellationToken);
}

Expand Down
116 changes: 116 additions & 0 deletions dotnet/src/SessionFsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*--------------------------------------------------------------------------------------------*/

using GitHub.Copilot.Rpc;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

namespace GitHub.Copilot;
Expand All @@ -27,6 +28,23 @@
public long? LastInsertRowid { get; set; }
}

/// <summary>
/// One statement in an atomic SQLite transaction passed to
/// <see cref="ISessionFsSqliteProvider.TransactionAsync"/>.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteStatement
{
/// <summary>How to execute: <c>"exec"</c>, <c>"query"</c>, or <c>"run"</c>.</summary>
public SessionFsSqliteQueryType QueryType { get; set; }

/// <summary>SQL statement to execute.</summary>
public string Query { get; set; } = string.Empty;

/// <summary>Optional named bind parameters.</summary>
public IDictionary<string, object?>? Params { get; set; }
}

/// <summary>
/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support
/// per-session SQLite databases. Implement this interface on your provider to enable
Expand All @@ -48,13 +66,53 @@
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken);

/// <summary>
/// Executes <paramref name="statements"/> atomically against the per-session database.
/// </summary>
/// <param name="statements">Statements to execute in order, inside a single transaction.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>One result per statement, in the same order as <paramref name="statements"/>.</returns>
/// <exception cref="SessionFsSqliteTransactionException">
/// Thrown to tell the runtime how the failure should be classified. Any other exception
/// is reported as <see cref="SessionFsSqliteTransactionErrorClass.Fatal"/>.
/// </exception>
Task<IList<SessionFsSqliteResult>> TransactionAsync(
IList<SessionFsSqliteStatement> statements,
CancellationToken cancellationToken);

/// <summary>
/// Checks whether the per-session SQLite database already exists, without creating it.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<bool> ExistsAsync(CancellationToken cancellationToken);
}

/// <summary>
/// Thrown by an <see cref="ISessionFsSqliteProvider"/> to classify a failed SQLite transaction.
/// <see cref="SessionFsSqliteTransactionErrorClass.BusyOrLocked"/> guarantees the transaction
/// rolled back and is safe to retry; <see cref="SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous"/>
/// must never be retried.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteTransactionException : Exception
{
/// <summary>Initializes a new instance of the <see cref="SessionFsSqliteTransactionException"/> class.</summary>
/// <param name="message">Human-readable failure description.</param>
/// <param name="errorClass">How the runtime should classify the failure.</param>
/// <param name="innerException">Optional underlying exception.</param>
public SessionFsSqliteTransactionException(
string message,
SessionFsSqliteTransactionErrorClass errorClass,
Exception? innerException = null)
: base(message, innerException)
{
ErrorClass = errorClass;
}

/// <summary>Gets the failure classification reported to the runtime.</summary>
public SessionFsSqliteTransactionErrorClass ErrorClass { get; }
}

/// <summary>
/// Base class for session filesystem providers. Subclasses override the
/// virtual methods and use normal C# patterns (return values, throw exceptions).
Expand Down Expand Up @@ -309,6 +367,64 @@
}
}

async Task<SessionFsSqliteTransactionResult> ISessionFsHandler.SqliteTransactionAsync(SessionFsSqliteTransactionRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError
{
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
Message = "SQLite is not supported by this provider.",
},
};
}

IList<SessionFsSqliteResult> results;
try
{
var statements = request.Statements.Select(statement => new SessionFsSqliteStatement
{
QueryType = statement.QueryType,
Query = statement.Query,
Params = statement.Params?.ToDictionary(kvp => kvp.Key, kvp => JsonElementToValue(kvp.Value)),
}).ToList();
results = await sqliteProvider.TransactionAsync(statements, cancellationToken).ConfigureAwait(false);
}
catch (SessionFsSqliteTransactionException ex)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError { ErrorClass = ex.ErrorClass, Message = ex.Message },
};
}
catch (Exception ex)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError
{
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
Message = ex.Message,
},
};
}
Comment thread
MackinnonBuck marked this conversation as resolved.
Dismissed

return new SessionFsSqliteTransactionResult
{
Results = results.Select(result => new SessionFsSqliteQueryResult
{
Rows = result.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
Columns = result.Columns ?? [],
RowsAffected = result.RowsAffected,
LastInsertRowid = result.LastInsertRowid,
}).ToList(),
};
}

async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
Expand Down
8 changes: 4 additions & 4 deletions dotnet/test/E2E/CommandsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task Session_Commands_List_Returns_Builtins_And_Respects_Client_Com
await TestHelper.WaitForConditionAsync(
async () =>
{
clientCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
clientCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = false,
IncludeClientCommands = true,
Expand All @@ -45,7 +45,7 @@ await TestHelper.WaitForConditionAsync(
Assert.Contains(clientCommands.Commands, c => IsCommand(c, "rollback", SlashCommandKind.Client));
Assert.DoesNotContain(clientCommands.Commands, c => c.Kind == SlashCommandKind.Builtin);

var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = true,
IncludeClientCommands = false,
Expand All @@ -64,7 +64,7 @@ public async Task Session_Commands_Invoke_Known_Builtin_Returns_Expected_Result(
{
var session = await CreateSessionAsync();

var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = true,
IncludeClientCommands = false,
Expand Down Expand Up @@ -128,7 +128,7 @@ public async Task Session_Commands_Execute_Runs_Registered_Command_Handler()
await TestHelper.WaitForConditionAsync(
async () =>
{
var commands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var commands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = false,
IncludeClientCommands = true,
Expand Down
58 changes: 50 additions & 8 deletions dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,68 @@
string query,
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken)
{
return Task.FromResult(RunStatement(GetOrCreateDb(), null, queryType, query, bindParams));
}

public Task<IList<SessionFsSqliteResult>> TransactionAsync(
IList<SessionFsSqliteStatement> statements,
CancellationToken cancellationToken)
{
var db = GetOrCreateDb();
using var transaction = db.BeginTransaction();
try
{
IList<SessionFsSqliteResult> results = statements
.Select(statement => RunStatement(db, transaction, statement.QueryType, statement.Query, statement.Params)
?? new SessionFsSqliteResult())
.ToList();
transaction.Commit();
return Task.FromResult(results);
}
catch (SqliteException ex)
{
transaction.Rollback();
var errorClass = ex.SqliteErrorCode is 5 or 6
? SessionFsSqliteTransactionErrorClass.BusyOrLocked
: SessionFsSqliteTransactionErrorClass.Fatal;
throw new SessionFsSqliteTransactionException(ex.Message, errorClass, ex);
}
catch (Exception ex)
{
transaction.Rollback();
throw new SessionFsSqliteTransactionException(ex.Message, SessionFsSqliteTransactionErrorClass.Fatal, ex);
}
Comment thread
MackinnonBuck marked this conversation as resolved.
Dismissed
}

private SessionFsSqliteResult? RunStatement(
SqliteConnection db,
SqliteTransaction? transaction,
SessionFsSqliteQueryType queryType,
string query,
IDictionary<string, object?>? bindParams)
{
sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query));

var trimmed = query.Trim();
if (trimmed.Length == 0)
{
return Task.FromResult<SessionFsSqliteResult?>(null);
return null;
}

var db = GetOrCreateDb();

if (queryType == SessionFsSqliteQueryType.Exec)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
cmd.ExecuteNonQuery();
return Task.FromResult<SessionFsSqliteResult?>(null);
return null;
}

if (queryType == SessionFsSqliteQueryType.Query)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
AddParams(cmd, bindParams);

Expand All @@ -88,33 +128,35 @@
rows.Add(row);
}

return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
return new SessionFsSqliteResult
{
Columns = columns,
Rows = rows,
RowsAffected = 0,
});
};
}

if (queryType == SessionFsSqliteQueryType.Run)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
AddParams(cmd, bindParams);

var rowsAffected = cmd.ExecuteNonQuery();

using var rowidCmd = db.CreateCommand();
rowidCmd.Transaction = transaction;
rowidCmd.CommandText = "SELECT last_insert_rowid()";
var lastRowid = rowidCmd.ExecuteScalar();

return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
return new SessionFsSqliteResult
{
Columns = [],
Rows = [],
RowsAffected = rowsAffected,
LastInsertRowid = lastRowid is long l ? l : null,
});
};
}

throw new ArgumentException($"Unknown queryType: {queryType}");
Expand Down
2 changes: 2 additions & 0 deletions dotnet/test/E2E/RpcShellAndFleetE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public async Task Should_Kill_Shell_Process()
var killResult = await session.Rpc.Shell.KillAsync(execResult.ProcessId);

Assert.True(killResult.Killed);

await session.DisposeAsync();
}

[Fact]
Expand Down
3 changes: 3 additions & 0 deletions dotnet/test/E2E/SessionFsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,9 @@ protected override Task RenameAsync(string src, string dest, CancellationToken c
Task<SessionFsSqliteResult?> ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary<string, object?>? bindParams, CancellationToken cancellationToken) =>
Task.FromException<SessionFsSqliteResult?>(exception);

Task<IList<SessionFsSqliteResult>> ISessionFsSqliteProvider.TransactionAsync(IList<SessionFsSqliteStatement> statements, CancellationToken cancellationToken) =>
Task.FromException<IList<SessionFsSqliteResult>>(exception);

Task<bool> ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationToken) =>
Task.FromException<bool>(exception);
}
Expand Down
8 changes: 4 additions & 4 deletions go/internal/e2e/commands_and_elicitation_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestCommandsE2E(t *testing.T) {
var clientCommands *rpc.CommandList
waitForRPCCondition(t, 30*time.Second, "client commands to be listed", func() (bool, error) {
var err error
clientCommands, err = session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
clientCommands, err = session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
IncludeBuiltins: rpcPtr(false),
IncludeClientCommands: rpcPtr(true),
IncludeSkills: rpcPtr(false),
Expand All @@ -68,7 +68,7 @@ func TestCommandsE2E(t *testing.T) {
t.Fatalf("Expected client-command-only list to exclude builtins, got %+v", clientCommands.Commands)
}

builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
IncludeBuiltins: rpcPtr(true),
IncludeClientCommands: rpcPtr(false),
IncludeSkills: rpcPtr(false),
Expand All @@ -93,7 +93,7 @@ func TestCommandsE2E(t *testing.T) {
}
defer session.Disconnect()

builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
IncludeBuiltins: rpcPtr(true),
IncludeClientCommands: rpcPtr(false),
IncludeSkills: rpcPtr(false),
Expand Down Expand Up @@ -152,7 +152,7 @@ func TestCommandsE2E(t *testing.T) {
defer session.Disconnect()

waitForRPCCondition(t, 30*time.Second, "registered deploy command", func() (bool, error) {
commands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
commands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
IncludeBuiltins: rpcPtr(false),
IncludeClientCommands: rpcPtr(true),
IncludeSkills: rpcPtr(false),
Expand Down
2 changes: 1 addition & 1 deletion go/internal/e2e/rpc_session_state_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,7 @@ func TestRPCSessionStateE2E(t *testing.T) {
t.Errorf("Expected SetApproveAll(true) to succeed, got %+v", approve)
}

reset, err := session.RPC.Permissions.ResetSessionApprovals(t.Context())
reset, err := session.RPC.Permissions.ResetSessionApprovals(t.Context(), &rpc.PermissionsResetSessionApprovalsRequest{})
if err != nil {
t.Fatalf("Failed to call ResetSessionApprovals: %v", err)
}
Expand Down
4 changes: 4 additions & 0 deletions go/internal/e2e/rpc_shell_and_fleet_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ func TestRPCShellAndFleetE2E(t *testing.T) {
if !kill.Killed {
t.Errorf("Expected shell.kill to report Killed=true, got %+v", kill)
}

if err := session.Disconnect(); err != nil {
t.Fatalf("Failed to disconnect session: %v", err)
}
})

t.Run("should start fleet and complete custom tool task", func(t *testing.T) {
Expand Down
Loading
Loading