Skip to content

Commit ad51d74

Browse files
Fix codegen for discriminated unions nested within other types (#736)
* Fix C# codegen for discriminated unions nested within other types. Result is strongly-typed APIs for permission requests * Fix permissions scenario to use strongly-typed PermissionRequest variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5c38b90 commit ad51d74

File tree

7 files changed

+253
-65
lines changed

7 files changed

+253
-65
lines changed

dotnet/src/Generated/SessionEvents.cs

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,7 @@ public partial class PermissionRequestedData
15281528
public required string RequestId { get; set; }
15291529

15301530
[JsonPropertyName("permissionRequest")]
1531-
public required object PermissionRequest { get; set; }
1531+
public required PermissionRequest PermissionRequest { get; set; }
15321532
}
15331533

15341534
public partial class PermissionCompletedData
@@ -2095,6 +2095,193 @@ public partial class SystemMessageDataMetadata
20952095
public Dictionary<string, object>? Variables { get; set; }
20962096
}
20972097

2098+
public partial class PermissionRequestShellCommandsItem
2099+
{
2100+
[JsonPropertyName("identifier")]
2101+
public required string Identifier { get; set; }
2102+
2103+
[JsonPropertyName("readOnly")]
2104+
public required bool ReadOnly { get; set; }
2105+
}
2106+
2107+
public partial class PermissionRequestShellPossibleUrlsItem
2108+
{
2109+
[JsonPropertyName("url")]
2110+
public required string Url { get; set; }
2111+
}
2112+
2113+
public partial class PermissionRequestShell : PermissionRequest
2114+
{
2115+
[JsonIgnore]
2116+
public override string Kind => "shell";
2117+
2118+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2119+
[JsonPropertyName("toolCallId")]
2120+
public string? ToolCallId { get; set; }
2121+
2122+
[JsonPropertyName("fullCommandText")]
2123+
public required string FullCommandText { get; set; }
2124+
2125+
[JsonPropertyName("intention")]
2126+
public required string Intention { get; set; }
2127+
2128+
[JsonPropertyName("commands")]
2129+
public required PermissionRequestShellCommandsItem[] Commands { get; set; }
2130+
2131+
[JsonPropertyName("possiblePaths")]
2132+
public required string[] PossiblePaths { get; set; }
2133+
2134+
[JsonPropertyName("possibleUrls")]
2135+
public required PermissionRequestShellPossibleUrlsItem[] PossibleUrls { get; set; }
2136+
2137+
[JsonPropertyName("hasWriteFileRedirection")]
2138+
public required bool HasWriteFileRedirection { get; set; }
2139+
2140+
[JsonPropertyName("canOfferSessionApproval")]
2141+
public required bool CanOfferSessionApproval { get; set; }
2142+
2143+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2144+
[JsonPropertyName("warning")]
2145+
public string? Warning { get; set; }
2146+
}
2147+
2148+
public partial class PermissionRequestWrite : PermissionRequest
2149+
{
2150+
[JsonIgnore]
2151+
public override string Kind => "write";
2152+
2153+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2154+
[JsonPropertyName("toolCallId")]
2155+
public string? ToolCallId { get; set; }
2156+
2157+
[JsonPropertyName("intention")]
2158+
public required string Intention { get; set; }
2159+
2160+
[JsonPropertyName("fileName")]
2161+
public required string FileName { get; set; }
2162+
2163+
[JsonPropertyName("diff")]
2164+
public required string Diff { get; set; }
2165+
2166+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2167+
[JsonPropertyName("newFileContents")]
2168+
public string? NewFileContents { get; set; }
2169+
}
2170+
2171+
public partial class PermissionRequestRead : PermissionRequest
2172+
{
2173+
[JsonIgnore]
2174+
public override string Kind => "read";
2175+
2176+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2177+
[JsonPropertyName("toolCallId")]
2178+
public string? ToolCallId { get; set; }
2179+
2180+
[JsonPropertyName("intention")]
2181+
public required string Intention { get; set; }
2182+
2183+
[JsonPropertyName("path")]
2184+
public required string Path { get; set; }
2185+
}
2186+
2187+
public partial class PermissionRequestMcp : PermissionRequest
2188+
{
2189+
[JsonIgnore]
2190+
public override string Kind => "mcp";
2191+
2192+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2193+
[JsonPropertyName("toolCallId")]
2194+
public string? ToolCallId { get; set; }
2195+
2196+
[JsonPropertyName("serverName")]
2197+
public required string ServerName { get; set; }
2198+
2199+
[JsonPropertyName("toolName")]
2200+
public required string ToolName { get; set; }
2201+
2202+
[JsonPropertyName("toolTitle")]
2203+
public required string ToolTitle { get; set; }
2204+
2205+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2206+
[JsonPropertyName("args")]
2207+
public object? Args { get; set; }
2208+
2209+
[JsonPropertyName("readOnly")]
2210+
public required bool ReadOnly { get; set; }
2211+
}
2212+
2213+
public partial class PermissionRequestUrl : PermissionRequest
2214+
{
2215+
[JsonIgnore]
2216+
public override string Kind => "url";
2217+
2218+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2219+
[JsonPropertyName("toolCallId")]
2220+
public string? ToolCallId { get; set; }
2221+
2222+
[JsonPropertyName("intention")]
2223+
public required string Intention { get; set; }
2224+
2225+
[JsonPropertyName("url")]
2226+
public required string Url { get; set; }
2227+
}
2228+
2229+
public partial class PermissionRequestMemory : PermissionRequest
2230+
{
2231+
[JsonIgnore]
2232+
public override string Kind => "memory";
2233+
2234+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2235+
[JsonPropertyName("toolCallId")]
2236+
public string? ToolCallId { get; set; }
2237+
2238+
[JsonPropertyName("subject")]
2239+
public required string Subject { get; set; }
2240+
2241+
[JsonPropertyName("fact")]
2242+
public required string Fact { get; set; }
2243+
2244+
[JsonPropertyName("citations")]
2245+
public required string Citations { get; set; }
2246+
}
2247+
2248+
public partial class PermissionRequestCustomTool : PermissionRequest
2249+
{
2250+
[JsonIgnore]
2251+
public override string Kind => "custom-tool";
2252+
2253+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2254+
[JsonPropertyName("toolCallId")]
2255+
public string? ToolCallId { get; set; }
2256+
2257+
[JsonPropertyName("toolName")]
2258+
public required string ToolName { get; set; }
2259+
2260+
[JsonPropertyName("toolDescription")]
2261+
public required string ToolDescription { get; set; }
2262+
2263+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2264+
[JsonPropertyName("args")]
2265+
public object? Args { get; set; }
2266+
}
2267+
2268+
[JsonPolymorphic(
2269+
TypeDiscriminatorPropertyName = "kind",
2270+
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
2271+
[JsonDerivedType(typeof(PermissionRequestShell), "shell")]
2272+
[JsonDerivedType(typeof(PermissionRequestWrite), "write")]
2273+
[JsonDerivedType(typeof(PermissionRequestRead), "read")]
2274+
[JsonDerivedType(typeof(PermissionRequestMcp), "mcp")]
2275+
[JsonDerivedType(typeof(PermissionRequestUrl), "url")]
2276+
[JsonDerivedType(typeof(PermissionRequestMemory), "memory")]
2277+
[JsonDerivedType(typeof(PermissionRequestCustomTool), "custom-tool")]
2278+
public partial class PermissionRequest
2279+
{
2280+
[JsonPropertyName("kind")]
2281+
public virtual string Kind { get; set; } = string.Empty;
2282+
}
2283+
2284+
20982285
public partial class PermissionCompletedDataResult
20992286
{
21002287
[JsonPropertyName("kind")]
@@ -2273,6 +2460,16 @@ public enum PermissionCompletedDataResultKind
22732460
[JsonSerializable(typeof(PermissionCompletedData))]
22742461
[JsonSerializable(typeof(PermissionCompletedDataResult))]
22752462
[JsonSerializable(typeof(PermissionCompletedEvent))]
2463+
[JsonSerializable(typeof(PermissionRequest))]
2464+
[JsonSerializable(typeof(PermissionRequestCustomTool))]
2465+
[JsonSerializable(typeof(PermissionRequestMcp))]
2466+
[JsonSerializable(typeof(PermissionRequestMemory))]
2467+
[JsonSerializable(typeof(PermissionRequestRead))]
2468+
[JsonSerializable(typeof(PermissionRequestShell))]
2469+
[JsonSerializable(typeof(PermissionRequestShellCommandsItem))]
2470+
[JsonSerializable(typeof(PermissionRequestShellPossibleUrlsItem))]
2471+
[JsonSerializable(typeof(PermissionRequestUrl))]
2472+
[JsonSerializable(typeof(PermissionRequestWrite))]
22762473
[JsonSerializable(typeof(PermissionRequestedData))]
22772474
[JsonSerializable(typeof(PermissionRequestedEvent))]
22782475
[JsonSerializable(typeof(SessionCompactionCompleteData))]

dotnet/src/Session.cs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonEl
339339
};
340340
}
341341

342-
var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionJsonContext.Default.PermissionRequest)
342+
var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionEventsJsonContext.Default.PermissionRequest)
343343
?? throw new InvalidOperationException("Failed to deserialize permission request");
344344

345345
var invocation = new PermissionInvocation
@@ -457,27 +457,16 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName,
457457
/// <summary>
458458
/// Executes a permission handler and sends the result back via the HandlePendingPermissionRequest RPC.
459459
/// </summary>
460-
private async Task ExecutePermissionAndRespondAsync(string requestId, object permissionRequestData, PermissionRequestHandler handler)
460+
private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, PermissionRequestHandler handler)
461461
{
462462
try
463463
{
464-
// PermissionRequestedData.PermissionRequest is typed as `object` in generated code,
465-
// but StreamJsonRpc deserializes it as a JsonElement.
466-
if (permissionRequestData is not JsonElement permJsonElement)
467-
{
468-
throw new InvalidOperationException(
469-
$"Permission request data must be a {nameof(JsonElement)}; received {permissionRequestData.GetType().Name}");
470-
}
471-
472-
var request = JsonSerializer.Deserialize(permJsonElement.GetRawText(), SessionJsonContext.Default.PermissionRequest)
473-
?? throw new InvalidOperationException("Failed to deserialize permission request");
474-
475464
var invocation = new PermissionInvocation
476465
{
477466
SessionId = SessionId
478467
};
479468

480-
var result = await handler(request, invocation);
469+
var result = await handler(permissionRequest, invocation);
481470
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, result);
482471
}
483472
catch (Exception)
@@ -780,7 +769,6 @@ internal record SessionDestroyRequest
780769
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
781770
[JsonSerializable(typeof(GetMessagesRequest))]
782771
[JsonSerializable(typeof(GetMessagesResponse))]
783-
[JsonSerializable(typeof(PermissionRequest))]
784772
[JsonSerializable(typeof(SendMessageRequest))]
785773
[JsonSerializable(typeof(SendMessageResponse))]
786774
[JsonSerializable(typeof(SessionAbortRequest))]

dotnet/src/Types.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -266,38 +266,6 @@ public class ToolInvocation
266266
/// </summary>
267267
public delegate Task<object?> ToolHandler(ToolInvocation invocation);
268268

269-
/// <summary>
270-
/// Represents a permission request from the server for a tool operation.
271-
/// </summary>
272-
public class PermissionRequest
273-
{
274-
/// <summary>
275-
/// Kind of permission being requested.
276-
/// <list type="bullet">
277-
/// <item><description><c>"shell"</c> — execute a shell command.</description></item>
278-
/// <item><description><c>"write"</c> — write to a file.</description></item>
279-
/// <item><description><c>"read"</c> — read a file.</description></item>
280-
/// <item><description><c>"mcp"</c> — invoke an MCP server tool.</description></item>
281-
/// <item><description><c>"url"</c> — access a URL.</description></item>
282-
/// <item><description><c>"custom-tool"</c> — invoke a custom tool.</description></item>
283-
/// </list>
284-
/// </summary>
285-
[JsonPropertyName("kind")]
286-
public string Kind { get; set; } = string.Empty;
287-
288-
/// <summary>
289-
/// Identifier of the tool call that triggered the permission request.
290-
/// </summary>
291-
[JsonPropertyName("toolCallId")]
292-
public string? ToolCallId { get; set; }
293-
294-
/// <summary>
295-
/// Additional properties not explicitly modeled.
296-
/// </summary>
297-
[JsonExtensionData]
298-
public Dictionary<string, object>? ExtensionData { get; set; }
299-
}
300-
301269
/// <summary>Describes the kind of a permission request result.</summary>
302270
[JsonConverter(typeof(PermissionRequestResultKind.Converter))]
303271
[DebuggerDisplay("{Value,nq}")]
@@ -2005,7 +1973,6 @@ public class SetForegroundSessionResponse
20051973
[JsonSerializable(typeof(ModelPolicy))]
20061974
[JsonSerializable(typeof(ModelSupports))]
20071975
[JsonSerializable(typeof(ModelVisionLimits))]
2008-
[JsonSerializable(typeof(PermissionRequest))]
20091976
[JsonSerializable(typeof(PermissionRequestResult))]
20101977
[JsonSerializable(typeof(PingRequest))]
20111978
[JsonSerializable(typeof(PingResponse))]

dotnet/test/PermissionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests()
231231
{
232232
OnPermissionRequest = (request, invocation) =>
233233
{
234-
if (!string.IsNullOrEmpty(request.ToolCallId))
234+
if (request is PermissionRequestShell shell && !string.IsNullOrEmpty(shell.ToolCallId))
235235
{
236236
receivedToolCallId = true;
237237
}

dotnet/test/ToolsTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,9 @@ await session.SendAsync(new MessageOptions
237237
Assert.Contains("HELLO", assistantMessage!.Data.Content ?? string.Empty);
238238

239239
// Should have received a custom-tool permission request with the correct tool name
240-
var customToolRequest = permissionRequests.FirstOrDefault(r => r.Kind == "custom-tool");
240+
var customToolRequest = permissionRequests.OfType<PermissionRequestCustomTool>().FirstOrDefault();
241241
Assert.NotNull(customToolRequest);
242-
Assert.True(customToolRequest!.ExtensionData?.ContainsKey("toolName") ?? false);
243-
var toolName = ((JsonElement)customToolRequest.ExtensionData!["toolName"]).GetString();
244-
Assert.Equal("encrypt_string", toolName);
242+
Assert.Equal("encrypt_string", customToolRequest!.ToolName);
245243

246244
[Description("Encrypts a string")]
247245
static string EncryptStringForPermission([Description("String to encrypt")] string input)

0 commit comments

Comments
 (0)