Skip to content
Merged
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
14 changes: 14 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,20 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?

private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions();

/// <summary>
/// Converts an arbitrary value into the <see cref="JsonElement"/> representation that wire
/// DTOs use for opaque-JSON fields. Pass-through for <see cref="JsonElement"/>, otherwise
/// serializes the runtime type using the shared JSON-RPC serializer options so that any
/// type registered in the SDK's source-generated contexts (e.g. primitives,
/// <c>Dictionary&lt;string, object&gt;</c>, generated DTOs) is supported.
/// </summary>
public static JsonElement? ToJsonElementForWire(object? value) => value switch
{
null => null,
JsonElement je => je,
_ => JsonSerializer.SerializeToElement(value, SerializerOptionsForMessageFormatter.GetTypeInfo(value.GetType()))
};

private static JsonSerializerOptions CreateSerializerOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
Expand Down
62 changes: 32 additions & 30 deletions dotnet/src/Generated/Rpc.cs

Large diffs are not rendered by default.

87 changes: 53 additions & 34 deletions dotnet/src/Generated/SessionEvents.cs

Large diffs are not rendered by default.

83 changes: 44 additions & 39 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent)
? new ElicitationSchema
{
Type = data.RequestedSchema.Type,
Properties = data.RequestedSchema.Properties,
Properties = data.RequestedSchema.Properties.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value),
Required = data.RequestedSchema.Required?.ToList()
}
: null;
Expand Down Expand Up @@ -687,7 +687,7 @@ await HandleElicitationRequestAsync(
/// <summary>
/// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC.
/// </summary>
private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, string toolCallId, object? arguments, AIFunction tool)
private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, string toolCallId, JsonElement? arguments, AIFunction tool)
{
try
{
Expand All @@ -707,13 +707,8 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName,
}
};

if (arguments is not null)
if (arguments is JsonElement incomingJsonArgs)
{
if (arguments is not JsonElement incomingJsonArgs)
{
throw new InvalidOperationException($"Incoming arguments must be a {nameof(JsonElement)}; received {arguments.GetType().Name}");
}

foreach (var prop in incomingJsonArgs.EnumerateObject())
{
aiFunctionArgs[prop.Name] = prop.Value;
Expand Down Expand Up @@ -948,7 +943,9 @@ private async Task HandleElicitationRequestAsync(ElicitationContext context, str
await Rpc.Ui.HandlePendingElicitationAsync(requestId, new UIElicitationResponse
{
Action = result.Action,
Content = result.Content
Content = result.Content?.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)
});
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
"CopilotSession.HandleElicitationRequestAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}",
Expand Down Expand Up @@ -991,6 +988,15 @@ private void AssertElicitation()
/// </summary>
private sealed class SessionUiApiImpl(CopilotSession session) : ISessionUiApi
{
// Parses a JSON string and returns a detached JsonElement. Using `using`
// ensures the pooled buffers backing the JsonDocument are released
// promptly; the cloned RootElement is independent of the document.
private static JsonElement ParseJsonElement(string json)
{
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}

public async Task<ElicitationResult> ElicitAsync(ElicitationParams elicitationParams, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(elicitationParams);
Expand All @@ -1000,12 +1006,18 @@ public async Task<ElicitationResult> ElicitAsync(ElicitationParams elicitationPa
var schema = new UIElicitationSchema
{
Type = elicitationParams.RequestedSchema.Type,
Properties = elicitationParams.RequestedSchema.Properties,
Properties = elicitationParams.RequestedSchema.Properties.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value),
Comment thread
SteveSandersonMS marked this conversation as resolved.
Required = elicitationParams.RequestedSchema.Required
};

var result = await session.Rpc.Ui.ElicitationAsync(elicitationParams.Message, schema, cancellationToken);
return new ElicitationResult { Action = result.Action, Content = result.Content };
return new ElicitationResult
{
Action = result.Action,
Content = result.Content?.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value)
};
}

public async Task<bool> ConfirmAsync(string message, CancellationToken cancellationToken)
Expand All @@ -1017,9 +1029,9 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
var schema = new UIElicitationSchema
{
Type = "object",
Properties = new Dictionary<string, object>
Properties = new Dictionary<string, JsonElement>
{
["confirmed"] = new Dictionary<string, object> { ["type"] = "boolean", ["default"] = true }
["confirmed"] = ParseJsonElement("""{"type":"boolean","default":true}""")
},
Comment thread
SteveSandersonMS marked this conversation as resolved.
Required = ["confirmed"]
};
Expand All @@ -1029,11 +1041,10 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
&& result.Content != null
&& result.Content.TryGetValue("confirmed", out var val))
{
return val switch
return val.ValueKind switch
{
bool b => b,
JsonElement { ValueKind: JsonValueKind.True } => true,
JsonElement { ValueKind: JsonValueKind.False } => false,
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => false
};
}
Expand All @@ -1048,12 +1059,13 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
session.ThrowIfDisposed();
session.AssertElicitation();

var enumJson = JsonSerializer.Serialize(options, TypesJsonContext.Default.StringArray);
var schema = new UIElicitationSchema
{
Type = "object",
Properties = new Dictionary<string, object>
Properties = new Dictionary<string, JsonElement>
{
["selection"] = new Dictionary<string, object> { ["type"] = "string", ["enum"] = options }
["selection"] = ParseJsonElement($$"""{"type":"string","enum":{{enumJson}}}""")
},
Comment thread
SteveSandersonMS marked this conversation as resolved.
Required = ["selection"]
};
Expand All @@ -1063,12 +1075,7 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
&& result.Content != null
&& result.Content.TryGetValue("selection", out var val))
{
return val switch
{
string s => s,
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
_ => val.ToString()
};
return val.ValueKind == JsonValueKind.String ? val.GetString() : val.ToString();
}

return null;
Expand All @@ -1080,18 +1087,21 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
session.ThrowIfDisposed();
session.AssertElicitation();

var field = new Dictionary<string, object> { ["type"] = "string" };
if (options?.Title != null) field["title"] = options.Title;
if (options?.Description != null) field["description"] = options.Description;
if (options?.MinLength != null) field["minLength"] = options.MinLength;
if (options?.MaxLength != null) field["maxLength"] = options.MaxLength;
if (options?.Format != null) field["format"] = options.Format;
if (options?.Default != null) field["default"] = options.Default;
var fieldNode = new System.Text.Json.Nodes.JsonObject { ["type"] = "string" };
if (options?.Title != null) fieldNode["title"] = options.Title;
if (options?.Description != null) fieldNode["description"] = options.Description;
if (options?.MinLength != null) fieldNode["minLength"] = options.MinLength;
if (options?.MaxLength != null) fieldNode["maxLength"] = options.MaxLength;
if (options?.Format != null) fieldNode["format"] = options.Format;
if (options?.Default != null) fieldNode["default"] = options.Default;

var schema = new UIElicitationSchema
{
Type = "object",
Properties = new Dictionary<string, object> { ["value"] = field },
Properties = new Dictionary<string, JsonElement>
{
["value"] = ParseJsonElement(fieldNode.ToJsonString())
},
Comment thread
SteveSandersonMS marked this conversation as resolved.
Required = ["value"]
};

Expand All @@ -1100,12 +1110,7 @@ public async Task<bool> ConfirmAsync(string message, CancellationToken cancellat
&& result.Content != null
&& result.Content.TryGetValue("value", out var val))
{
return val switch
{
string s => s,
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
_ => val.ToString()
};
return val.ValueKind == JsonValueKind.String ? val.GetString() : val.ToString();
}

return null;
Expand Down
22 changes: 19 additions & 3 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.Text.Json;

namespace GitHub.Copilot;

Expand Down Expand Up @@ -44,7 +45,7 @@ public interface ISessionFsSqliteProvider
Task<SessionFsSqliteResult?> QueryAsync(
SessionFsSqliteQueryType queryType,
string query,
IDictionary<string, object>? bindParams,
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken);

/// <summary>
Expand Down Expand Up @@ -287,11 +288,16 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio

try
{
var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, request.Params, cancellationToken).ConfigureAwait(false);
var bindParams = request.Params?.ToDictionary(
kvp => kvp.Key,
kvp => JsonElementToValue(kvp.Value));
var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false);

return new SessionFsSqliteQueryResult
{
Rows = result?.Rows ?? [],
Rows = result?.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
Comment thread
SteveSandersonMS marked this conversation as resolved.
Columns = result?.Columns ?? [],
RowsAffected = result?.RowsAffected ?? 0,
LastInsertRowid = result?.LastInsertRowid,
Expand Down Expand Up @@ -329,4 +335,14 @@ private static SessionFsError ToSessionFsError(Exception ex)
: SessionFsErrorCode.UNKNOWN;
return new SessionFsError { Code = code, Message = ex.Message };
}

private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Null => null,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
_ => element.GetRawText(),
};
}
8 changes: 4 additions & 4 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ public sealed class ToolInvocation
/// <summary>
/// Arguments passed to the tool by the language model.
/// </summary>
public object? Arguments { get; set; }
public JsonElement? Arguments { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -1123,7 +1123,7 @@ public sealed class PreToolUseHookInput
/// Arguments that will be passed to the tool.
/// </summary>
[JsonPropertyName("toolArgs")]
public object? ToolArgs { get; set; }
public JsonElement? ToolArgs { get; set; }
}

/// <summary>
Expand Down Expand Up @@ -1278,13 +1278,13 @@ public sealed class PostToolUseHookInput
/// Arguments that were passed to the tool.
/// </summary>
[JsonPropertyName("toolArgs")]
public object? ToolArgs { get; set; }
public JsonElement? ToolArgs { get; set; }

/// <summary>
/// Result returned by the tool execution.
/// </summary>
[JsonPropertyName("toolResult")]
public object? ToolResult { get; set; }
public JsonElement? ToolResult { get; set; }
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private SqliteConnection GetOrCreateDb()
public Task<SessionFsSqliteResult?> QueryAsync(
SessionFsSqliteQueryType queryType,
string query,
IDictionary<string, object>? bindParams,
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken)
{
sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query));
Expand Down Expand Up @@ -125,7 +125,7 @@ public Task<bool> ExistsAsync(CancellationToken cancellationToken)
return Task.FromResult(_db is not null);
}

private static void AddParams(SqliteCommand cmd, IDictionary<string, object>? bindParams)
private static void AddParams(SqliteCommand cmd, IDictionary<string, object?>? bindParams)
{
if (bindParams is null) return;
foreach (var (key, value) in bindParams)
Expand Down
9 changes: 5 additions & 4 deletions dotnet/test/E2E/PendingWorkResumeE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using GitHub.Copilot.Test.Harness;
using Microsoft.Extensions.AI;
using System.ComponentModel;
using System.Text.Json;
using Xunit;
using Xunit.Abstractions;
using RpcPermissionDecisionApproveOnce = GitHub.Copilot.Rpc.PermissionDecisionApproveOnce;
Expand Down Expand Up @@ -136,7 +137,7 @@ await session1.SendAsync(new MessageOptions

var toolResult = await session2.Rpc.Tools.HandlePendingToolCallAsync(
toolEvent.Data.RequestId,
result: "EXTERNAL_RESUMED_BETA");
result: JsonDocument.Parse("\"EXTERNAL_RESUMED_BETA\"").RootElement.Clone());
Comment thread
SteveSandersonMS marked this conversation as resolved.
Assert.True(toolResult.Success);

var answer = await TestHelper.GetFinalAssistantMessageAsync(session2, PendingWorkTimeout);
Expand Down Expand Up @@ -205,7 +206,7 @@ await session1.SendAsync(new MessageOptions

var resumedResult = await session2.Rpc.Tools.HandlePendingToolCallAsync(
toolEvent.Data.RequestId,
result: "EXTERNAL_RESUMED_BETA");
result: JsonDocument.Parse("\"EXTERNAL_RESUMED_BETA\"").RootElement.Clone());
Assert.True(resumedResult.Success);
Comment thread
SteveSandersonMS marked this conversation as resolved.

// continuePendingWork=false may interrupt agent continuation before this response,
Expand Down Expand Up @@ -282,11 +283,11 @@ await Task.WhenAll(
var toolB = toolEvents["pending_lookup_b"];
var resultB = await session2.Rpc.Tools.HandlePendingToolCallAsync(
toolB.Data.RequestId,
result: "PARALLEL_B_BETA");
result: JsonDocument.Parse("\"PARALLEL_B_BETA\"").RootElement.Clone());
Assert.True(resultB.Success);
var resultA = await session2.Rpc.Tools.HandlePendingToolCallAsync(
toolA.Data.RequestId,
result: "PARALLEL_A_ALPHA");
result: JsonDocument.Parse("\"PARALLEL_A_ALPHA\"").RootElement.Clone());
Comment thread
SteveSandersonMS marked this conversation as resolved.
Assert.True(resultA.Success);

await session2.DisposeAsync();
Expand Down
3 changes: 2 additions & 1 deletion dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using GitHub.Copilot.Rpc;
using GitHub.Copilot.Test.Harness;
using System.Text.Json;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -143,7 +144,7 @@ public async Task Should_Return_Expected_Results_For_Missing_Pending_Handler_Req

var tool = await session.Rpc.Tools.HandlePendingToolCallAsync(
requestId: "missing-tool-request",
result: "tool result");
result: JsonDocument.Parse("\"tool result\"").RootElement.Clone());
Comment thread
SteveSandersonMS marked this conversation as resolved.
Assert.False(tool.Success);

var command = await session.Rpc.Commands.HandlePendingCommandAsync(
Expand Down
2 changes: 1 addition & 1 deletion dotnet/test/E2E/SessionFsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ protected override Task RemoveAsync(string path, bool recursive, bool force, Can
protected override Task RenameAsync(string src, string dest, CancellationToken cancellationToken) =>
Task.FromException(exception);

Task<SessionFsSqliteResult?> ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary<string, object>? bindParams, CancellationToken cancellationToken) =>
Task<SessionFsSqliteResult?> ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary<string, object?>? bindParams, CancellationToken cancellationToken) =>
Task.FromException<SessionFsSqliteResult?>(exception);

Task<bool> ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationToken) =>
Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/E2E/ToolResultsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ static ToolResultAIContent AnalyzeCode([Description("File to analyze")] string f
ResultType = "success",
ToolTelemetry = new Dictionary<string, object>
{
["metrics"] = new Dictionary<string, object> { ["analysisTimeMs"] = 150 },
["properties"] = new Dictionary<string, object> { ["analyzer"] = "eslint" },
["metrics"] = JsonDocument.Parse("""{"analysisTimeMs":150}""").RootElement.Clone(),
["properties"] = JsonDocument.Parse("""{"analyzer":"eslint"}""").RootElement.Clone(),
Comment thread
SteveSandersonMS marked this conversation as resolved.
},
});
}
Expand Down
3 changes: 1 addition & 2 deletions dotnet/test/Unit/SessionEventSerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class SessionEventSerializationTests
Content = "ok",
DetailedContent = "ok",
},
ToolTelemetry = new Dictionary<string, object>
ToolTelemetry = new Dictionary<string, JsonElement>
{
["properties"] = ParseJsonElement("""{"command":"view"}"""),
["metrics"] = ParseJsonElement("""{"resultLength":2}"""),
Expand All @@ -84,7 +84,6 @@ public class SessionEventSerializationTests
Data = new SessionShutdownData
{
ShutdownType = ShutdownType.Routine,
TotalPremiumRequests = 1,
TotalApiDuration = TimeSpan.FromMilliseconds(100),
SessionStartTime = 1773609948932,
CodeChanges = new ShutdownCodeChanges
Expand Down
Loading
Loading