Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d726f4d
Reject unmappable shapes; require .asOpaqueJson() opt-in for JsonElement
SteveSandersonMS May 21, 2026
be97a4d
C# codegen: accept object at RPC boundary for opaque-JSON params
SteveSandersonMS May 21, 2026
a81706d
C# codegen: honor property-level visibility=internal
SteveSandersonMS May 21, 2026
d9681d5
C# codegen: honor property-level visibility=experimental
SteveSandersonMS May 21, 2026
007434c
Revert required-drop hack now that runtime rejects bad shapes
SteveSandersonMS May 21, 2026
4e96e07
Use chained SerializerOptions for opaque-JSON wire conversion
SteveSandersonMS May 21, 2026
cf4cebd
Rust codegen: honor property-level visibility=internal and stability=…
SteveSandersonMS May 21, 2026
fca140a
TypeScript codegen: honor property-level visibility=internal and sta…
SteveSandersonMS May 21, 2026
21ffb14
Python codegen: honor property-level visibility=internal and stabilit…
SteveSandersonMS May 21, 2026
4759835
Go codegen: emit property-level Internal/Experimental doc comments
SteveSandersonMS May 21, 2026
1469a23
Fix mojibake in csharp.ts and factor [JsonInclude] helper
SteveSandersonMS May 21, 2026
12d3837
Regenerate SDK code after rebase on main
SteveSandersonMS May 21, 2026
8c5567e
Use JsonTypeInfo in ToJsonElementForWire to drop trim/AOT suppressions
SteveSandersonMS May 21, 2026
f946952
TS codegen: honor x-opaque-json; share isOpaqueJson helper
SteveSandersonMS May 21, 2026
e7b77d0
Preserve TS shape for x-opaque-json fields
SteveSandersonMS May 21, 2026
c706604
Factor stripOpaqueJsonMarker into utils.ts
SteveSandersonMS May 21, 2026
8baafad
Regenerate SDK from latest runtime schemas
SteveSandersonMS May 21, 2026
bb0e481
Regenerate SDK: MarketplaceSource is experimental
SteveSandersonMS May 21, 2026
de221da
Regenerate SDK: anthropicAdvisorBlocks is experimental
SteveSandersonMS May 21, 2026
55eec83
Regenerate SDK: optional internal/experimental metrics fields
SteveSandersonMS May 21, 2026
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
23 changes: 16 additions & 7 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1636,6 +1636,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 Expand Up @@ -1803,7 +1817,7 @@ public async ValueTask<SystemMessageTransformRpcResponse> OnSystemMessageTransfo
public async ValueTask<ToolCallResponseV2> OnToolCallV2(string sessionId,
string toolCallId,
string toolName,
object? arguments,
JsonElement? arguments,
string? traceparent = null,
string? tracestate = null)
{
Expand Down Expand Up @@ -1840,13 +1854,8 @@ public async ValueTask<ToolCallResponseV2> OnToolCallV2(string sessionId,
}
};

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
94 changes: 59 additions & 35 deletions dotnet/src/Generated/Rpc.cs

Large diffs are not rendered by default.

91 changes: 58 additions & 33 deletions dotnet/src/Generated/SessionEvents.cs

Large diffs are not rendered by default.

74 changes: 35 additions & 39 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,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 @@ -690,7 +690,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 @@ -710,13 +710,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 @@ -957,7 +952,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 @@ -1009,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),
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 @@ -1026,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"] = JsonDocument.Parse("""{"type":"boolean","default":true}""").RootElement.Clone()
},
Required = ["confirmed"]
};
Expand All @@ -1038,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 @@ -1057,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"] = JsonDocument.Parse($$"""{"type":"string","enum":{{enumJson}}}""").RootElement.Clone()
},
Required = ["selection"]
};
Expand All @@ -1072,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 @@ -1089,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"] = JsonDocument.Parse(fieldNode.ToJsonString()).RootElement.Clone()
},
Required = ["value"]
};

Expand All @@ -1109,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() ?? [],
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 @@ -671,7 +671,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>Describes the kind of a permission request result.</summary>
Expand Down Expand Up @@ -1227,7 +1227,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 @@ -1305,13 +1305,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 @@ -5,6 +5,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 @@ -141,7 +142,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());
Assert.True(toolResult.Success);

var answer = await TestHelper.GetFinalAssistantMessageAsync(session2, PendingWorkTimeout);
Expand Down Expand Up @@ -210,7 +211,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);

// continuePendingWork=false may interrupt agent continuation before this response,
Expand Down Expand Up @@ -287,11 +288,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());
Assert.True(resultA.Success);

await session2.DisposeAsync();
Expand Down
8 changes: 4 additions & 4 deletions dotnet/test/E2E/RpcMcpConfigE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public async Task Should_Call_Server_Mcp_Config_Rpcs()

try
{
await Client.Rpc.Mcp.Config.AddAsync(serverName, config);
await Client.Rpc.Mcp.Config.AddAsync(serverName, JsonSerializer.SerializeToElement(config, TestSharedJsonContext.Default.DictionaryStringObject));
var afterAdd = await Client.Rpc.Mcp.Config.ListAsync();
Assert.Contains(serverName, afterAdd.Servers.Keys);

await Client.Rpc.Mcp.Config.UpdateAsync(serverName, updatedConfig);
await Client.Rpc.Mcp.Config.UpdateAsync(serverName, JsonSerializer.SerializeToElement(updatedConfig, TestSharedJsonContext.Default.DictionaryStringObject));
var afterUpdate = await Client.Rpc.Mcp.Config.ListAsync();
var updated = GetServerConfig(afterUpdate, serverName);
Assert.Equal("node", updated.GetProperty("command").GetString());
Expand Down Expand Up @@ -84,7 +84,7 @@ public async Task Should_RoundTrip_Http_Mcp_Oauth_Config_Rpc()

try
{
await Client.Rpc.Mcp.Config.AddAsync(serverName, config);
await Client.Rpc.Mcp.Config.AddAsync(serverName, JsonSerializer.SerializeToElement<McpServerConfig>(config, TestSharedJsonContext.Default.McpServerConfig));
var afterAdd = await Client.Rpc.Mcp.Config.ListAsync();
var added = GetServerConfig(afterAdd, serverName);
Assert.Equal("http", added.GetProperty("type").GetString());
Expand All @@ -94,7 +94,7 @@ public async Task Should_RoundTrip_Http_Mcp_Oauth_Config_Rpc()
Assert.False(added.GetProperty("oauthPublicClient").GetBoolean());
Assert.Equal("client_credentials", added.GetProperty("oauthGrantType").GetString());

await Client.Rpc.Mcp.Config.UpdateAsync(serverName, updatedConfig);
await Client.Rpc.Mcp.Config.UpdateAsync(serverName, JsonSerializer.SerializeToElement<McpServerConfig>(updatedConfig, TestSharedJsonContext.Default.McpServerConfig));
var afterUpdate = await Client.Rpc.Mcp.Config.ListAsync();
var updated = GetServerConfig(afterUpdate, serverName);
Assert.Equal("https://example.com/updated-mcp", updated.GetProperty("url").GetString());
Expand Down
Loading
Loading