diff --git a/docs/auth/byok.md b/docs/auth/byok.md index d3d4e4106..4bb88f5aa 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -426,7 +426,7 @@ using GitHub.Copilot.SDK; var client = new CopilotClient(new CopilotClientOptions { - OnListModels = (ct) => Task.FromResult(new List + OnListModels = (ct) => Task.FromResult>(new List { new() { diff --git a/docs/setup/local-cli.md b/docs/setup/local-cli.md index 48092b735..845a20af5 100644 --- a/docs/setup/local-cli.md +++ b/docs/setup/local-cli.md @@ -99,7 +99,9 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) - fmt.Println(*response.Data.Content) + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } } ``` @@ -115,7 +117,9 @@ defer client.Stop() session, _ := client.CreateSession(ctx, &copilot.SessionConfig{Model: "gpt-4.1"}) response, _ := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "Hello!"}) -fmt.Println(*response.Data.Content) +if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) +} ``` diff --git a/dotnet/README.md b/dotnet/README.md index 4e6cd7c4e..3e6def504 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -131,7 +131,7 @@ Ping the server to check connectivity. Get current connection state. -##### `ListSessionsAsync(): Task>` +##### `ListSessionsAsync(): Task>` List all available sessions. diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 732c15447..29b49c294 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -75,7 +75,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable private int? _negotiatedProtocolVersion; private List? _modelsCache; private readonly SemaphoreSlim _modelsCacheLock = new(1, 1); - private readonly Func>>? _onListModels; + private readonly Func>>? _onListModels; private readonly List> _lifecycleHandlers = []; private readonly Dictionary>> _typedLifecycleHandlers = []; private readonly object _lifecycleHandlersLock = new(); @@ -735,7 +735,7 @@ public async Task GetAuthStatusAsync(CancellationToken ca /// The cache is cleared when the client disconnects. /// /// Thrown when the client is not connected or not authenticated. - public async Task> ListModelsAsync(CancellationToken cancellationToken = default) + public async Task> ListModelsAsync(CancellationToken cancellationToken = default) { await _modelsCacheLock.WaitAsync(cancellationToken); try @@ -746,7 +746,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio return [.. _modelsCache]; // Return a copy to prevent cache mutation } - List models; + IList models; if (_onListModels is not null) { // Use custom handler instead of CLI RPC @@ -847,7 +847,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell /// } /// /// - public async Task> ListSessionsAsync(SessionListFilter? filter = null, CancellationToken cancellationToken = default) + public async Task> ListSessionsAsync(SessionListFilter? filter = null, CancellationToken cancellationToken = default) { var connection = await EnsureConnectedAsync(cancellationToken); @@ -1467,7 +1467,7 @@ public void OnSessionLifecycle(string type, string sessionId, JsonElement? metad client.DispatchLifecycleEvent(evt); } - public async Task OnUserInputRequest(string sessionId, string question, List? choices = null, bool? allowFreeform = null) + public async Task OnUserInputRequest(string sessionId, string question, IList? choices = null, bool? allowFreeform = null) { var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); var request = new UserInputRequest @@ -1621,26 +1621,26 @@ internal record CreateSessionRequest( string? SessionId, string? ClientName, string? ReasoningEffort, - List? Tools, + IList? Tools, SystemMessageConfig? SystemMessage, - List? AvailableTools, - List? ExcludedTools, + IList? AvailableTools, + IList? ExcludedTools, ProviderConfig? Provider, bool? RequestPermission, bool? RequestUserInput, bool? Hooks, string? WorkingDirectory, bool? Streaming, - Dictionary? McpServers, + IDictionary? McpServers, string? EnvValueMode, - List? CustomAgents, + IList? CustomAgents, string? Agent, string? ConfigDir, bool? EnableConfigDiscovery, - List? SkillDirectories, - List? DisabledSkills, + IList? SkillDirectories, + IList? DisabledSkills, InfiniteSessionConfig? InfiniteSessions, - List? Commands = null, + IList? Commands = null, bool? RequestElicitation = null, string? Traceparent = null, string? Tracestate = null, @@ -1673,10 +1673,10 @@ internal record ResumeSessionRequest( string? ClientName, string? Model, string? ReasoningEffort, - List? Tools, + IList? Tools, SystemMessageConfig? SystemMessage, - List? AvailableTools, - List? ExcludedTools, + IList? AvailableTools, + IList? ExcludedTools, ProviderConfig? Provider, bool? RequestPermission, bool? RequestUserInput, @@ -1686,14 +1686,14 @@ internal record ResumeSessionRequest( bool? EnableConfigDiscovery, bool? DisableResume, bool? Streaming, - Dictionary? McpServers, + IDictionary? McpServers, string? EnvValueMode, - List? CustomAgents, + IList? CustomAgents, string? Agent, - List? SkillDirectories, - List? DisabledSkills, + IList? SkillDirectories, + IList? DisabledSkills, InfiniteSessionConfig? InfiniteSessions, - List? Commands = null, + IList? Commands = null, bool? RequestElicitation = null, string? Traceparent = null, string? Tracestate = null, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index b06b68676..387702685 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5,6 +5,7 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -60,7 +61,7 @@ public class ModelCapabilitiesLimitsVision { /// MIME types the model accepts. [JsonPropertyName("supported_media_types")] - public List SupportedMediaTypes { get => field ??= []; set; } + public IList SupportedMediaTypes { get => field ??= []; set; } /// Maximum number of images per prompt. [JsonPropertyName("max_prompt_images")] @@ -148,7 +149,7 @@ public class Model /// Supported reasoning effort levels (only present if model supports reasoning effort). [JsonPropertyName("supportedReasoningEfforts")] - public List? SupportedReasoningEfforts { get; set; } + public IList? SupportedReasoningEfforts { get; set; } /// Default reasoning effort level (only present if model supports reasoning effort). [JsonPropertyName("defaultReasoningEffort")] @@ -160,7 +161,7 @@ public class ModelsListResult { /// List of available models with full metadata. [JsonPropertyName("models")] - public List Models { get => field ??= []; set; } + public IList Models { get => field ??= []; set; } } /// RPC data type for Tool operations. @@ -180,7 +181,7 @@ public class Tool /// JSON Schema for the tool's input parameters. [JsonPropertyName("parameters")] - public Dictionary? Parameters { get; set; } + public IDictionary? Parameters { get; set; } /// Optional instructions for how to use this tool effectively. [JsonPropertyName("instructions")] @@ -192,7 +193,7 @@ public class ToolsListResult { /// List of available built-in tools with metadata. [JsonPropertyName("tools")] - public List Tools { get => field ??= []; set; } + public IList Tools { get => field ??= []; set; } } /// RPC data type for ToolsList operations. @@ -236,7 +237,7 @@ public class AccountGetQuotaResult { /// Quota snapshots keyed by type (e.g., chat, completions, premium_interactions). [JsonPropertyName("quotaSnapshots")] - public Dictionary QuotaSnapshots { get => field ??= []; set; } + public IDictionary QuotaSnapshots { get => field ??= new Dictionary(); set; } } /// RPC data type for SessionFsSetProvider operations. @@ -313,6 +314,8 @@ internal class SessionLogRequest public bool? Ephemeral { get; set; } /// Optional URL the user can open in their browser for more details. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] [JsonPropertyName("url")] public string? Url { get; set; } } @@ -358,7 +361,7 @@ public class ModelCapabilitiesOverrideLimitsVision { /// MIME types the model accepts. [JsonPropertyName("supported_media_types")] - public List? SupportedMediaTypes { get; set; } + public IList? SupportedMediaTypes { get; set; } /// Maximum number of images per prompt. [JsonPropertyName("max_prompt_images")] @@ -516,7 +519,7 @@ public class SessionWorkspaceListFilesResult { /// Relative file paths in the workspace files directory. [JsonPropertyName("files")] - public List Files { get => field ??= []; set; } + public IList Files { get => field ??= []; set; } } /// RPC data type for SessionWorkspaceListFiles operations. @@ -612,7 +615,7 @@ public class SessionAgentListResult { /// Available custom agents. [JsonPropertyName("agents")] - public List Agents { get => field ??= []; set; } + public IList Agents { get => field ??= []; set; } } /// RPC data type for SessionAgentList operations. @@ -717,7 +720,7 @@ public class SessionAgentReloadResult { /// Reloaded custom agents. [JsonPropertyName("agents")] - public List Agents { get => field ??= []; set; } + public IList Agents { get => field ??= []; set; } } /// RPC data type for SessionAgentReload operations. @@ -763,7 +766,7 @@ public class SessionSkillsListResult { /// Available skills. [JsonPropertyName("skills")] - public List Skills { get => field ??= []; set; } + public IList Skills { get => field ??= []; set; } } /// RPC data type for SessionSkillsList operations. @@ -854,7 +857,7 @@ public class SessionMcpListResult { /// Configured MCP servers. [JsonPropertyName("servers")] - public List Servers { get => field ??= []; set; } + public IList Servers { get => field ??= []; set; } } /// RPC data type for SessionMcpList operations. @@ -945,7 +948,7 @@ public class SessionPluginsListResult { /// Installed plugins. [JsonPropertyName("plugins")] - public List Plugins { get => field ??= []; set; } + public IList Plugins { get => field ??= []; set; } } /// RPC data type for SessionPluginsList operations. @@ -978,7 +981,7 @@ public class Extension /// Process ID if the extension is running. [JsonPropertyName("pid")] - public double? Pid { get; set; } + public long? Pid { get; set; } } /// RPC data type for SessionExtensionsList operations. @@ -987,7 +990,7 @@ public class SessionExtensionsListResult { /// Discovered extensions and their current status. [JsonPropertyName("extensions")] - public List Extensions { get => field ??= []; set; } + public IList Extensions { get => field ??= []; set; } } /// RPC data type for SessionExtensionsList operations. @@ -1113,7 +1116,7 @@ public class SessionUiElicitationResult /// The form values submitted by the user (present when action is 'accept'). [JsonPropertyName("content")] - public Dictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// JSON Schema describing the form fields to present to the user. @@ -1125,11 +1128,11 @@ public class SessionUiElicitationRequestRequestedSchema /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public Dictionary Properties { get => field ??= []; set; } + public IDictionary Properties { get => field ??= new Dictionary(); set; } /// List of required field names. [JsonPropertyName("required")] - public List? Required { get; set; } + public IList? Required { get; set; } } /// RPC data type for SessionUiElicitation operations. @@ -1165,7 +1168,7 @@ public class SessionUiHandlePendingElicitationRequestResult /// The form values submitted by the user (present when action is 'accept'). [JsonPropertyName("content")] - public Dictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// RPC data type for SessionUiHandlePendingElicitation operations. @@ -1449,7 +1452,7 @@ public class SessionFsReaddirResult { /// Entry names in the directory. [JsonPropertyName("entries")] - public List Entries { get => field ??= []; set; } + public IList Entries { get => field ??= []; set; } } /// RPC data type for SessionFsReaddir operations. @@ -1481,7 +1484,7 @@ public class SessionFsReaddirWithTypesResult { /// Directory entries with type information. [JsonPropertyName("entries")] - public List Entries { get => field ??= []; set; } + public IList Entries { get => field ??= []; set; } } /// RPC data type for SessionFsReaddirWithTypes operations. diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index dfd3b761f..c627aca4f 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -5,7 +5,9 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: session-events.schema.json +using System.ComponentModel.DataAnnotations; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; @@ -1196,7 +1198,7 @@ public partial class SessionErrorData /// HTTP status code from the upstream request, if applicable. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] - public double? StatusCode { get; set; } + public long? StatusCode { get; set; } /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1204,6 +1206,8 @@ public partial class SessionErrorData public string? ProviderCallId { get; set; } /// Optional URL associated with this error that the user can open in a browser. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("url")] public string? Url { get; set; } @@ -1238,6 +1242,8 @@ public partial class SessionInfoData public required string Message { get; set; } /// Optional URL associated with this message that the user can open in a browser. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("url")] public string? Url { get; set; } @@ -1255,6 +1261,8 @@ public partial class SessionWarningData public required string Message { get; set; } /// Optional URL associated with this warning that the user can open in a browser. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("url")] public string? Url { get; set; } @@ -1430,7 +1438,7 @@ public partial class SessionShutdownData /// Per-model usage breakdown, keyed by model identifier. [JsonPropertyName("modelMetrics")] - public required Dictionary ModelMetrics { get; set; } + public required IDictionary ModelMetrics { get; set; } /// Model that was selected at the time of shutdown. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -1886,7 +1894,7 @@ public partial class AssistantUsageData /// Per-quota resource usage snapshots, keyed by quota identifier. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("quotaSnapshots")] - public Dictionary? QuotaSnapshots { get; set; } + public IDictionary? QuotaSnapshots { get; set; } /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2019,7 +2027,7 @@ public partial class ToolExecutionCompleteData /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolTelemetry")] - public Dictionary? ToolTelemetry { get; set; } + public IDictionary? ToolTelemetry { get; set; } /// Tool call ID of the parent tool invocation when this event originates from a sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2383,7 +2391,7 @@ public partial class ElicitationCompletedData /// The submitted form data when action is 'accept'; keys match the requested schema fields. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("content")] - public Dictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// Sampling request from an MCP server; contains the server name and a requestId for correlation. @@ -3270,7 +3278,7 @@ public partial class SystemMessageDataMetadata /// Template variables used when constructing the prompt. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("variables")] - public Dictionary? Variables { get; set; } + public IDictionary? Variables { get; set; } } /// The agent_completed variant of . @@ -3678,7 +3686,7 @@ public partial class ElicitationRequestedDataRequestedSchema /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public required Dictionary Properties { get; set; } + public required IDictionary Properties { get; set; } /// List of required field names. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/dotnet/src/MillisecondsTimeSpanConverter.cs b/dotnet/src/MillisecondsTimeSpanConverter.cs new file mode 100644 index 000000000..696d053dd --- /dev/null +++ b/dotnet/src/MillisecondsTimeSpanConverter.cs @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHub.Copilot.SDK; + +/// Converts between JSON numeric milliseconds and . +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class MillisecondsTimeSpanConverter : JsonConverter +{ + /// + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + TimeSpan.FromMilliseconds(reader.GetDouble()); + + /// + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) => + writer.WriteNumberValue(value.TotalMilliseconds); +} diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 189cdfaff..2a2778b3c 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -1219,7 +1219,7 @@ internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; public string Prompt { get; init; } = string.Empty; - public List? Attachments { get; init; } + public IList? Attachments { get; init; } public string? Mode { get; init; } public string? Traceparent { get; init; } public string? Tracestate { get; init; } @@ -1237,7 +1237,7 @@ internal record GetMessagesRequest internal record GetMessagesResponse { - public List Events { get; init; } = []; + public IList Events { get => field ??= []; init; } } internal record SessionAbortRequest diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 8ee146dee..970d44f76 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -149,7 +149,7 @@ public string? GithubToken /// querying the CLI server. Useful in BYOK mode to return models /// available from your custom provider. /// - public Func>>? OnListModels { get; set; } + public Func>>? OnListModels { get; set; } /// /// Custom session filesystem provider configuration. @@ -293,7 +293,7 @@ public class ToolResultObject /// Binary results (e.g., images) to be consumed by the language model. /// [JsonPropertyName("binaryResultsForLlm")] - public List? BinaryResultsForLlm { get; set; } + public IList? BinaryResultsForLlm { get; set; } /// /// Result type indicator. @@ -323,7 +323,7 @@ public class ToolResultObject /// Custom telemetry data associated with the tool execution. /// [JsonPropertyName("toolTelemetry")] - public Dictionary? ToolTelemetry { get; set; } + public IDictionary? ToolTelemetry { get; set; } /// /// Converts the result of an invocation into a @@ -540,7 +540,7 @@ public class PermissionRequestResult /// Permission rules to apply for the decision. /// [JsonPropertyName("rules")] - public List? Rules { get; set; } + public IList? Rules { get; set; } } /// @@ -578,7 +578,7 @@ public class UserInputRequest /// Optional choices for multiple choice questions. /// [JsonPropertyName("choices")] - public List? Choices { get; set; } + public IList? Choices { get; set; } /// /// Whether freeform text input is allowed. @@ -696,13 +696,13 @@ public class ElicitationSchema /// Form field definitions, keyed by field name. /// [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; + public IDictionary Properties { get => field ??= new Dictionary(); set; } /// /// List of required field names. /// [JsonPropertyName("required")] - public List? Required { get; set; } + public IList? Required { get; set; } } /// @@ -734,7 +734,7 @@ public class ElicitationResult /// /// Form values submitted by the user (present when is Accept). /// - public Dictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// @@ -1127,7 +1127,7 @@ public class SessionStartHookOutput /// Modified session configuration to apply at startup. /// [JsonPropertyName("modifiedConfig")] - public Dictionary? ModifiedConfig { get; set; } + public IDictionary? ModifiedConfig { get; set; } } /// @@ -1193,7 +1193,7 @@ public class SessionEndHookOutput /// List of cleanup action identifiers to execute after the session ends. /// [JsonPropertyName("cleanupActions")] - public List? CleanupActions { get; set; } + public IList? CleanupActions { get; set; } /// /// Summary of the session to persist for future reference. @@ -1438,7 +1438,7 @@ public class SystemMessageConfig /// Section-level overrides for customize mode. /// Keys are section identifiers (see ). /// - public Dictionary? Sections { get; set; } + public IDictionary? Sections { get; set; } } /// @@ -1517,7 +1517,7 @@ private protected McpServerConfig() { } /// List of tools to include from this server. Empty list means none. Use "*" for all. /// [JsonPropertyName("tools")] - public List Tools { get; set; } = []; + public IList Tools { get => field ??= []; set; } /// /// The server type discriminator. @@ -1551,13 +1551,13 @@ public sealed class McpStdioServerConfig : McpServerConfig /// Arguments to pass to the command. /// [JsonPropertyName("args")] - public List Args { get; set; } = []; + public IList Args { get => field ??= []; set; } /// /// Environment variables to pass to the server. /// [JsonPropertyName("env")] - public Dictionary? Env { get; set; } + public IDictionary? Env { get; set; } /// /// Working directory for the server process. @@ -1585,7 +1585,7 @@ public sealed class McpHttpServerConfig : McpServerConfig /// Optional HTTP headers to include in requests. /// [JsonPropertyName("headers")] - public Dictionary? Headers { get; set; } + public IDictionary? Headers { get; set; } } // ============================================================================ @@ -1619,7 +1619,7 @@ public class CustomAgentConfig /// List of tool names the agent can use. Null for all tools. /// [JsonPropertyName("tools")] - public List? Tools { get; set; } + public IList? Tools { get; set; } /// /// The prompt content for the agent. @@ -1631,7 +1631,7 @@ public class CustomAgentConfig /// MCP servers specific to this agent. /// [JsonPropertyName("mcpServers")] - public Dictionary? McpServers { get; set; } + public IDictionary? McpServers { get; set; } /// /// Whether the agent should be available for model inference. @@ -1700,7 +1700,9 @@ protected SessionConfig(SessionConfig? other) Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; McpServers = other.McpServers is not null - ? new Dictionary(other.McpServers, other.McpServers.Comparer) + ? (other.McpServers is Dictionary dict + ? new Dictionary(dict, dict.Comparer) + : new Dictionary(other.McpServers)) : null; Model = other.Model; ModelCapabilities = other.ModelCapabilities; @@ -1777,11 +1779,11 @@ protected SessionConfig(SessionConfig? other) /// /// List of tool names to allow; only these tools will be available when specified. /// - public List? AvailableTools { get; set; } + public IList? AvailableTools { get; set; } /// /// List of tool names to exclude from the session. /// - public List? ExcludedTools { get; set; } + public IList? ExcludedTools { get; set; } /// /// Custom model provider configuration for the session. /// @@ -1804,7 +1806,7 @@ protected SessionConfig(SessionConfig? other) /// When the CLI has a TUI, each command appears as /name for the user to invoke. /// The handler is called when the user executes the command. /// - public List? Commands { get; set; } + public IList? Commands { get; set; } /// /// Handler for elicitation requests from the server or MCP tools. @@ -1834,12 +1836,12 @@ protected SessionConfig(SessionConfig? other) /// MCP server configurations for the session. /// Keys are server names, values are server configurations ( or ). /// - public Dictionary? McpServers { get; set; } + public IDictionary? McpServers { get; set; } /// /// Custom agent configurations for the session. /// - public List? CustomAgents { get; set; } + public IList? CustomAgents { get; set; } /// /// Name of the custom agent to activate when the session starts. @@ -1850,12 +1852,12 @@ protected SessionConfig(SessionConfig? other) /// /// Directories to load skills from. /// - public List? SkillDirectories { get; set; } + public IList? SkillDirectories { get; set; } /// /// List of skill names to disable. /// - public List? DisabledSkills { get; set; } + public IList? DisabledSkills { get; set; } /// /// Infinite session configuration for persistent workspaces and automatic compaction. @@ -1928,7 +1930,9 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) Hooks = other.Hooks; InfiniteSessions = other.InfiniteSessions; McpServers = other.McpServers is not null - ? new Dictionary(other.McpServers, other.McpServers.Comparer) + ? (other.McpServers is Dictionary dict + ? new Dictionary(dict, dict.Comparer) + : new Dictionary(other.McpServers)) : null; Model = other.Model; ModelCapabilities = other.ModelCapabilities; @@ -1971,13 +1975,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// List of tool names to allow. When specified, only these tools will be available. /// Takes precedence over ExcludedTools. /// - public List? AvailableTools { get; set; } + public IList? AvailableTools { get; set; } /// /// List of tool names to disable. All other tools remain available. /// Ignored if AvailableTools is specified. /// - public List? ExcludedTools { get; set; } + public IList? ExcludedTools { get; set; } /// /// Custom model provider configuration for the resumed session. @@ -2012,7 +2016,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// When the CLI has a TUI, each command appears as /name for the user to invoke. /// The handler is called when the user executes the command. /// - public List? Commands { get; set; } + public IList? Commands { get; set; } /// /// Handler for elicitation requests from the server or MCP tools. @@ -2066,12 +2070,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// MCP server configurations for the session. /// Keys are server names, values are server configurations ( or ). /// - public Dictionary? McpServers { get; set; } + public IDictionary? McpServers { get; set; } /// /// Custom agent configurations for the session. /// - public List? CustomAgents { get; set; } + public IList? CustomAgents { get; set; } /// /// Name of the custom agent to activate when the session starts. @@ -2082,12 +2086,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// /// Directories to load skills from. /// - public List? SkillDirectories { get; set; } + public IList? SkillDirectories { get; set; } /// /// List of skill names to disable. /// - public List? DisabledSkills { get; set; } + public IList? DisabledSkills { get; set; } /// /// Infinite session configuration for persistent workspaces and automatic compaction. @@ -2152,7 +2156,7 @@ protected MessageOptions(MessageOptions? other) /// /// File or data attachments to include with the message. /// - public List? Attachments { get; set; } + public IList? Attachments { get; set; } /// /// Interaction mode for the message (e.g., "plan", "edit"). /// @@ -2320,7 +2324,7 @@ public class ModelVisionLimits /// List of supported image MIME types (e.g., "image/png", "image/jpeg"). /// [JsonPropertyName("supported_media_types")] - public List SupportedMediaTypes { get; set; } = []; + public IList SupportedMediaTypes { get => field ??= []; set; } /// /// Maximum number of images allowed in a single prompt. @@ -2452,7 +2456,7 @@ public class ModelInfo /// Supported reasoning effort levels (only present if model supports reasoning effort) [JsonPropertyName("supportedReasoningEfforts")] - public List? SupportedReasoningEfforts { get; set; } + public IList? SupportedReasoningEfforts { get; set; } /// Default reasoning effort level (only present if model supports reasoning effort) [JsonPropertyName("defaultReasoningEffort")] @@ -2468,7 +2472,7 @@ public class GetModelsResponse /// List of available models. /// [JsonPropertyName("models")] - public List Models { get; set; } = []; + public IList Models { get => field ??= []; set; } } // ============================================================================ @@ -2597,7 +2601,7 @@ public class SystemMessageTransformRpcResponse /// The transformed sections keyed by section identifier. /// [JsonPropertyName("sections")] - public Dictionary? Sections { get; set; } + public IDictionary? Sections { get; set; } } [JsonSourceGenerationOptions( diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs index 6c70ffaa3..c62c5bc3f 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -278,7 +278,7 @@ public async Task Should_Throw_When_ResumeSession_Called_Without_PermissionHandl [Fact] public async Task ListModels_WithCustomHandler_CallsHandler() { - var customModels = new List + IList customModels = new List { new() { @@ -312,7 +312,7 @@ public async Task ListModels_WithCustomHandler_CallsHandler() [Fact] public async Task ListModels_WithCustomHandler_CachesResults() { - var customModels = new List + IList customModels = new List { new() { @@ -345,7 +345,7 @@ public async Task ListModels_WithCustomHandler_CachesResults() [Fact] public async Task ListModels_WithCustomHandler_WorksWithoutStart() { - var customModels = new List + IList customModels = new List { new() { diff --git a/dotnet/test/ElicitationTests.cs b/dotnet/test/ElicitationTests.cs index e3048e4c9..f91fe2d19 100644 --- a/dotnet/test/ElicitationTests.cs +++ b/dotnet/test/ElicitationTests.cs @@ -62,7 +62,7 @@ await session.Ui.ElicitationAsync(new ElicitationParams Message = "Enter name", RequestedSchema = new ElicitationSchema { - Properties = new() { ["name"] = new Dictionary { ["type"] = "string" } }, + Properties = new Dictionary() { ["name"] = new Dictionary { ["type"] = "string" } }, Required = ["name"], }, }); diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index 9bd03f186..5200d6de5 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -397,7 +397,7 @@ public async Task Should_List_Sessions_With_Context() var sessions = await Client.ListSessionsAsync(); Assert.NotEmpty(sessions); - var ourSession = sessions.Find(s => s.SessionId == session.SessionId); + var ourSession = sessions.FirstOrDefault(s => s.SessionId == session.SessionId); Assert.NotNull(ourSession); // Context may be present on sessions that have been persisted with workspace.yaml diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 9049cb38c..63968077e 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -158,13 +158,25 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: if (format === "date-time") return "DateTimeOffset?"; return "string?"; } + if (nonNullTypes.length === 1 && (nonNullTypes[0] === "number" || nonNullTypes[0] === "integer")) { + if (format === "duration") { + return "TimeSpan?"; + } + return nonNullTypes[0] === "integer" ? "long?" : "double?"; + } } if (type === "string") { if (format === "uuid") return required ? "Guid" : "Guid?"; if (format === "date-time") return required ? "DateTimeOffset" : "DateTimeOffset?"; return required ? "string" : "string?"; } - if (type === "number" || type === "integer") return required ? "double" : "double?"; + if (type === "number" || type === "integer") { + if (format === "duration") { + return required ? "TimeSpan" : "TimeSpan?"; + } + if (type === "integer") return required ? "long" : "long?"; + return required ? "double" : "double?"; + } if (type === "boolean") return required ? "bool" : "bool?"; if (type === "array") { const items = schema.items as JSONSchema7 | undefined; @@ -174,13 +186,99 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: if (type === "object") { if (schema.additionalProperties && typeof schema.additionalProperties === "object") { const valueType = schemaTypeToCSharp(schema.additionalProperties as JSONSchema7, true, knownTypes); - return required ? `Dictionary` : `Dictionary?`; + return required ? `IDictionary` : `IDictionary?`; } return required ? "object" : "object?"; } return required ? "object" : "object?"; } +/** Tracks whether any TimeSpan property was emitted so the converter can be generated. */ + + +/** + * Emit C# data-annotation attributes for a JSON Schema property. + * Returns an array of attribute lines (without trailing newlines). + */ +function emitDataAnnotations(schema: JSONSchema7, indent: string): string[] { + const attrs: string[] = []; + const format = schema.format; + + // [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] for format: "uri" + if (format === "uri") { + attrs.push(`${indent}[Url]`); + attrs.push(`${indent}[StringSyntax(StringSyntaxAttribute.Uri)]`); + } + + // [StringSyntax(StringSyntaxAttribute.Regex)] for format: "regex" + if (format === "regex") { + attrs.push(`${indent}[StringSyntax(StringSyntaxAttribute.Regex)]`); + } + + // [Base64String] for base64-encoded string properties + if (format === "byte" || (schema as Record).contentEncoding === "base64") { + attrs.push(`${indent}[Base64String]`); + } + + // [Range] for minimum/maximum + const hasMin = typeof schema.minimum === "number"; + const hasMax = typeof schema.maximum === "number"; + if (hasMin || hasMax) { + const namedArgs: string[] = []; + if (schema.exclusiveMinimum === true) namedArgs.push("MinimumIsExclusive = true"); + if (schema.exclusiveMaximum === true) namedArgs.push("MaximumIsExclusive = true"); + const namedSuffix = namedArgs.length > 0 ? `, ${namedArgs.join(", ")}` : ""; + if (schema.type === "integer") { + // Use Range(Type, string, string) overload since RangeAttribute has no long constructor + const min = hasMin ? String(schema.minimum) : "long.MinValue"; + const max = hasMax ? String(schema.maximum) : "long.MaxValue"; + attrs.push(`${indent}[Range(typeof(long), "${min}", "${max}"${namedSuffix})]`); + } else { + const min = hasMin ? String(schema.minimum) : "double.MinValue"; + const max = hasMax ? String(schema.maximum) : "double.MaxValue"; + attrs.push(`${indent}[Range(${min}, ${max}${namedSuffix})]`); + } + } + + // [RegularExpression] for pattern + if (typeof schema.pattern === "string") { + const escaped = schema.pattern.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + attrs.push(`${indent}[RegularExpression("${escaped}")]`); + } + + // [MinLength] / [MaxLength] for string constraints + if (typeof schema.minLength === "number") { + attrs.push(`${indent}[MinLength(${schema.minLength})]`); + } + if (typeof schema.maxLength === "number") { + attrs.push(`${indent}[MaxLength(${schema.maxLength})]`); + } + + return attrs; +} + +/** + * Returns true when a TimeSpan-typed property needs a [JsonConverter] attribute. + * + * NOTE: The runtime schema uses `format: "duration"` on numeric (integer/number) fields + * to mean "a duration value expressed in milliseconds". This differs from the JSON Schema + * spec, where `format: "duration"` denotes an ISO 8601 duration string (e.g. "PT1H30M"). + * The generator and runtime agree on this convention, so we map these to TimeSpan with a + * milliseconds-based JSON converter rather than expecting ISO 8601 strings. + */ +function isDurationProperty(schema: JSONSchema7): boolean { + if (schema.format === "duration") { + const t = schema.type; + if (t === "number" || t === "integer") return true; + if (Array.isArray(t)) { + const nonNull = (t as string[]).filter((x) => x !== "null"); + if (nonNull.length === 1 && (nonNull[0] === "number" || nonNull[0] === "integer")) return true; + } + } + return false; +} + + const COPYRIGHT = `/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/`; @@ -351,6 +449,8 @@ function generateDerivedClass( const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); + lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " ")); + if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -383,6 +483,8 @@ function generateNestedClass( const csharpType = resolveSessionPropertyType(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); + lines.push(...emitDataAnnotations(prop, " ")); + if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -479,6 +581,8 @@ function generateDataClass(variant: EventVariant, knownTypes: Map` : `List<${itemClass}>?`; + return isRequired ? `IList<${itemClass}>` : `IList<${itemClass}>?`; } const itemType = schemaTypeToCSharp(items, true, rpcKnownTypes); - return isRequired ? `List<${itemType}>` : `List<${itemType}>?`; + return isRequired ? `IList<${itemType}>` : `IList<${itemType}>?`; } if (schema.type === "object" && schema.additionalProperties && typeof schema.additionalProperties === "object") { const vs = schema.additionalProperties as JSONSchema7; if (vs.type === "object" && vs.properties) { const valClass = `${parentClassName}${propName}Value`; classes.push(emitRpcClass(valClass, vs, "public", classes)); - return isRequired ? `Dictionary` : `Dictionary?`; + return isRequired ? `IDictionary` : `IDictionary?`; } const valueType = schemaTypeToCSharp(vs, true, rpcKnownTypes); - return isRequired ? `Dictionary` : `Dictionary?`; + return isRequired ? `IDictionary` : `IDictionary?`; } return schemaTypeToCSharp(schema, isRequired, rpcKnownTypes); } @@ -680,6 +786,8 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi const csharpType = resolveRpcType(prop, isReq, className, csharpName, extraClasses); lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); + lines.push(...emitDataAnnotations(prop, " ")); + if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); lines.push(` [JsonPropertyName("${propName}")]`); let defaultVal = ""; @@ -687,8 +795,11 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; else if (csharpType === "object") defaultVal = " = null!;"; - else if (csharpType.startsWith("List<") || csharpType.startsWith("Dictionary<")) { + else if (csharpType.startsWith("IList<")) { propAccessors = "{ get => field ??= []; set; }"; + } else if (csharpType.startsWith("IDictionary<")) { + const concreteType = csharpType.replace("IDictionary<", "Dictionary<"); + propAccessors = `{ get => field ??= new ${concreteType}(); set; }`; } else if (emittedRpcClasses.has(csharpType)) { propAccessors = "{ get => field ??= new(); set; }"; } @@ -1082,6 +1193,7 @@ function generateRpcCode(schema: ApiSchema): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization;