From 83f7bc47eefeca8f6b2ff10ee9ec049354eb41ae Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 17:52:49 +0100 Subject: [PATCH] Re-land x-opaque-json -> JsonElement mapping with object boundary at RPC params Also makes visibility: internal handling consistent across all codegens: - C# session-events now honors type-level visibility=internal (was missing; caused CS9032 build failures with required internal members on public types). - New transitive-visibility pass propagates internal from a referenced type to its referencing fields (fixes CS0053-style inconsistencies). - Rust honors type-level visibility=internal via pub(crate) on struct + fields (replaces the previous doc-hidden-only on fields). Internal RPC methods are also pub(crate). - Python prefixes internal types and fields with underscore (the universal Python no-stability-guarantee convention); replaces the no-op # Internal: comment that consumers never saw. - Go skips internal types from the copilot package re-exports so consumers using the canonical copilot.* namespace never see them. Lowercase types in the rpc package was evaluated but rejected (requires non-trivial runtime refactoring in go/client.go). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 14 + dotnet/src/Generated/Rpc.cs | 62 ++-- dotnet/src/Generated/SessionEvents.cs | 87 +++-- dotnet/src/Session.cs | 74 ++-- dotnet/src/SessionFsProvider.cs | 22 +- dotnet/src/Types.cs | 8 +- .../E2E/InMemorySessionFsSqliteHandler.cs | 4 +- dotnet/test/E2E/PendingWorkResumeE2ETests.cs | 9 +- dotnet/test/E2E/RpcMcpConfigE2ETests.cs | 8 +- .../test/E2E/RpcTasksAndHandlersE2ETests.cs | 3 +- dotnet/test/E2E/SessionFsE2ETests.cs | 2 +- dotnet/test/E2E/ToolResultsE2ETests.cs | 4 +- dotnet/test/TestJsonContext.cs | 16 + .../Unit/SessionEventSerializationTests.cs | 3 +- go/rpc/zrpc.go | 7 + go/rpc/zsession_events.go | 19 + go/zsession_events.go | 3 - nodejs/src/generated/rpc.ts | 28 +- nodejs/src/generated/session-events.ts | 22 +- python/copilot/client.py | 6 +- python/copilot/generated/rpc.py | 29 +- python/copilot/generated/session_events.py | 145 ++++---- rust/src/generated/api_types.rs | 38 +- rust/src/generated/rpc.rs | 2 +- rust/src/generated/session_events.rs | 91 ++++- scripts/codegen/csharp.ts | 140 ++++++-- scripts/codegen/go.ts | 70 +++- scripts/codegen/package-lock.json | 1 - scripts/codegen/python.ts | 90 ++++- scripts/codegen/rust.ts | 31 +- scripts/codegen/typescript.ts | 13 +- scripts/codegen/utils.ts | 329 ++++++++++++++++++ 32 files changed, 1039 insertions(+), 341 deletions(-) create mode 100644 dotnet/test/TestJsonContext.cs diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 8c4204ccb..a5cc62354 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1629,6 +1629,20 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions(); + /// + /// Converts an arbitrary value into the representation that wire + /// DTOs use for opaque-JSON fields. Pass-through for , 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, + /// Dictionary<string, object>, generated DTOs) is supported. + /// + 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) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 09b16e0bf..a756c6bff 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -247,7 +247,7 @@ public sealed class Tool /// JSON Schema for the tool's input parameters. [JsonPropertyName("parameters")] - public IDictionary? Parameters { get; set; } + public IDictionary? Parameters { get; set; } } /// Built-in tools available for the requested model, with their parameters and instructions. @@ -378,7 +378,7 @@ public sealed class McpConfigList { /// All MCP servers from user config, keyed by name. [JsonPropertyName("servers")] - public IDictionary Servers { get => field ??= new Dictionary(); set; } + public IDictionary Servers { get => field ??= new Dictionary(); set; } } /// MCP server name and configuration to add to user configuration. @@ -386,7 +386,7 @@ internal sealed class McpConfigAddRequest { /// MCP server configuration (stdio process or remote HTTP/SSE). [JsonPropertyName("config")] - public object Config { get; set; } = null!; + public JsonElement Config { get; set; } /// Unique name for the MCP server. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] @@ -401,7 +401,7 @@ internal sealed class McpConfigUpdateRequest { /// MCP server configuration (stdio process or remote HTTP/SSE). [JsonPropertyName("config")] - public object Config { get; set; } = null!; + public JsonElement Config { get; set; } /// Name of the MCP server to update. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] @@ -1074,7 +1074,7 @@ public sealed class InstalledPlugin /// Source for direct repo installs (when marketplace is empty). [JsonPropertyName("source")] - public object? Source { get; set; } + public JsonElement? Source { get; set; } /// Version installed (if available). [JsonPropertyName("version")] @@ -1345,8 +1345,9 @@ internal sealed class SendRequest public string SessionId { get; set; } = string.Empty; /// Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. + [JsonInclude] [JsonPropertyName("source")] - public object? Source { get; set; } + internal JsonElement? Source { get; set; } /// W3C Trace Context traceparent header for distributed tracing of this agent turn. [JsonPropertyName("traceparent")] @@ -2617,8 +2618,9 @@ public sealed class AgentInfo public string Id { get; set; } = string.Empty; /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("mcpServers")] - public IDictionary? McpServers { get; set; } + public IDictionary? McpServers { get; set; } /// Preferred model id for this agent. When omitted, inherits the outer agent's model. [JsonPropertyName("model")] @@ -3474,7 +3476,7 @@ internal sealed class McpExecuteSamplingParams { /// The original MCP JSON-RPC request ID (string or number). Used by the runtime to correlate the inference with the originating MCP request for telemetry; this is distinct from `requestId` (which is the schema-level cancellation handle). [JsonPropertyName("mcpRequestId")] - public object McpRequestId { get; set; } = null!; + public JsonElement McpRequestId { get; set; } /// Raw MCP CreateMessageRequest params, as received in the `sampling.requested` event. Treated as opaque at the schema layer; the runtime converts the embedded MCP messages into the OpenAI chat-completion shape internally. [JsonPropertyName("request")] @@ -3668,7 +3670,7 @@ public sealed class SessionInstalledPlugin /// Source descriptor for direct repo installs (when marketplace is empty). [JsonPropertyName("source")] - public object? Source { get; set; } + public JsonElement? Source { get; set; } /// Installed version, if known. [JsonPropertyName("version")] @@ -3680,8 +3682,9 @@ public sealed class SessionInstalledPlugin internal sealed class SessionUpdateOptionsParams { /// Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("additionalContentExclusionPolicies")] - public IList? AdditionalContentExclusionPolicies { get; set; } + public IList? AdditionalContentExclusionPolicies { get; set; } /// Runtime context discriminator (e.g., `cli`, `actions`). [JsonPropertyName("agentContext")] @@ -3784,8 +3787,9 @@ internal sealed class SessionUpdateOptionsParams public string? Model { get; set; } /// Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("provider")] - public object? Provider { get; set; } + public JsonElement? Provider { get; set; } /// Reasoning effort for the selected model (model-defined enum). [JsonPropertyName("reasoningEffort")] @@ -3796,8 +3800,9 @@ internal sealed class SessionUpdateOptionsParams public bool? RunningInInteractiveMode { get; set; } /// Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + [Experimental(Diagnostics.Experimental)] [JsonPropertyName("sandboxConfig")] - public object? SandboxConfig { get; set; } + public JsonElement? SandboxConfig { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] @@ -3950,7 +3955,7 @@ internal sealed class HandlePendingToolCallRequest /// Tool call result (string or expanded result object). [JsonPropertyName("result")] - public object? Result { get; set; } + public JsonElement? Result { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] @@ -4367,7 +4372,7 @@ public sealed class UIElicitationResponse /// The form values submitted by the user (present when action is 'accept'). [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + public IDictionary? Content { get; set; } } /// JSON Schema describing the form fields to present to the user. @@ -4376,7 +4381,7 @@ public sealed class UIElicitationSchema { /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + public IDictionary Properties { get => field ??= new Dictionary(); set; } /// List of required field names. [JsonPropertyName("required")] @@ -4636,7 +4641,7 @@ public sealed class PermissionsConfigureAdditionalContentExclusionPolicy { /// Gets or sets the last_updated_at value. [JsonPropertyName("last_updated_at")] - public object LastUpdatedAt { get; set; } = null!; + public JsonElement LastUpdatedAt { get; set; } /// Gets or sets the rules value. [JsonPropertyName("rules")] @@ -6517,7 +6522,7 @@ internal sealed class EventLogReadRequest /// Either '*' to receive all event types, or a non-empty list of event types to receive. [JsonPropertyName("types")] - public object? Types { get; set; } + public JsonElement? Types { get; set; } /// Milliseconds to wait for new events when the cursor is at the tail of history. 0 (default) returns immediately even if no events are available. Capped at 30000ms. Ephemeral events that arrive during the wait are delivered in this batch but are NOT replayable on a subsequent read (use a non-zero waitMs in your next call to capture future ephemerals as they happen). [JsonConverter(typeof(MillisecondsTimeSpanConverter))] @@ -7139,7 +7144,7 @@ public sealed class SessionFsSqliteQueryResult /// For SELECT: array of row objects. For others: empty array. [JsonPropertyName("rows")] - public IList> Rows { get => field ??= []; set; } + public IList> Rows { get => field ??= []; set; } /// Number of rows affected (for INSERT/UPDATE/DELETE). [JsonPropertyName("rowsAffected")] @@ -7151,7 +7156,7 @@ public sealed class SessionFsSqliteQueryRequest { /// Optional named bind parameters. [JsonPropertyName("params")] - public IDictionary? Params { get; set; } + public IDictionary? Params { get; set; } /// SQL query to execute. [JsonPropertyName("query")] @@ -10354,7 +10359,7 @@ public async Task AddAsync(string name, object config, CancellationToken cancell ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(config); - var request = new McpConfigAddRequest { Name = name, Config = config }; + var request = new McpConfigAddRequest { Name = name, Config = CopilotClient.ToJsonElementForWire(config)!.Value }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.add", [request], cancellationToken); } @@ -10367,7 +10372,7 @@ public async Task UpdateAsync(string name, object config, CancellationToken canc ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(config); - var request = new McpConfigUpdateRequest { Name = name, Config = config }; + var request = new McpConfigUpdateRequest { Name = name, Config = CopilotClient.ToJsonElementForWire(config)!.Value }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.update", [request], cancellationToken); } @@ -10938,7 +10943,7 @@ public async Task SendAsync(string prompt, string? displayPrompt = n ArgumentNullException.ThrowIfNull(prompt); _session.ThrowIfDisposed(); - var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = source, AgentMode = agentMode, RequestHeaders = requestHeaders, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; + var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = CopilotClient.ToJsonElementForWire(source), AgentMode = agentMode, RequestHeaders = requestHeaders, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.send", [request], cancellationToken); } @@ -11718,7 +11723,7 @@ public async Task ExecuteSamplingAsync(string reques ArgumentNullException.ThrowIfNull(request); _session.ThrowIfDisposed(); - var rpcRequest = new McpExecuteSamplingParams { SessionId = _session.SessionId, RequestId = requestId, ServerName = serverName, McpRequestId = mcpRequestId, Request = request }; + var rpcRequest = new McpExecuteSamplingParams { SessionId = _session.SessionId, RequestId = requestId, ServerName = serverName, McpRequestId = CopilotClient.ToJsonElementForWire(mcpRequestId)!.Value, Request = request }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.executeSampling", [rpcRequest], cancellationToken); } @@ -11866,11 +11871,11 @@ internal OptionsApi(CopilotSession session) /// Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). /// The to monitor for cancellation requests. The default is . /// Indicates whether the session options patch was applied successfully. - public async Task UpdateAsync(string? model = null, string? reasoningEffort = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, object? provider = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, object? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, CancellationToken cancellationToken = default) + public async Task UpdateAsync(string? model = null, string? reasoningEffort = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, object? provider = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, object? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ReasoningEffort = reasoningEffort, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled }; + var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ReasoningEffort = reasoningEffort, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = CopilotClient.ToJsonElementForWire(provider), WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = CopilotClient.ToJsonElementForWire(sandboxConfig), LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.options.update", [request], cancellationToken); } } @@ -11979,7 +11984,7 @@ public async Task HandlePendingToolCallAsync(string ArgumentNullException.ThrowIfNull(requestId); _session.ThrowIfDisposed(); - var request = new HandlePendingToolCallRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result, Error = error }; + var request = new HandlePendingToolCallRequest { SessionId = _session.SessionId, RequestId = requestId, Result = CopilotClient.ToJsonElementForWire(result), Error = error }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tools.handlePendingToolCall", [request], cancellationToken); } @@ -12836,7 +12841,7 @@ public async Task ReadAsync(string? cursor = null, int? max = { _session.ThrowIfDisposed(); - var request = new EventLogReadRequest { SessionId = _session.SessionId, Cursor = cursor, Max = max, Wait = waitMs, Types = types, AgentScope = agentScope }; + var request = new EventLogReadRequest { SessionId = _session.SessionId, Cursor = cursor, Max = max, Wait = waitMs, Types = CopilotClient.ToJsonElementForWire(types), AgentScope = agentScope }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.eventLog.read", [request], cancellationToken); } @@ -13172,11 +13177,9 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncSession-wide accumulated nano-AI units cost. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("totalNanoAiu")] public double? TotalNanoAiu { get; set; } /// Total number of premium API requests used during the session. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("totalPremiumRequests")] - public double? TotalPremiumRequests { get; set; } + internal double? TotalPremiumRequests { get; set; } } /// Working directory and git context at session start. @@ -1987,11 +1989,13 @@ public sealed partial class AssistantStreamingDeltaData public sealed partial class AssistantMessageData { /// Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("anthropicAdvisorBlocks")] - public object[]? AnthropicAdvisorBlocks { get; set; } + public JsonElement[]? AnthropicAdvisorBlocks { get; set; } /// Anthropic advisor model ID used for this response, for timeline display on replay. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("anthropicAdvisorModel")] public string? AnthropicAdvisorModel { get; set; } @@ -2127,10 +2131,12 @@ public sealed partial class AssistantUsageData /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("copilotUsage")] - public AssistantUsageCopilotUsage? CopilotUsage { get; set; } + internal AssistantUsageCopilotUsage? CopilotUsage { get; set; } /// Model multiplier cost for billing purposes. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cost")] public double? Cost { get; set; } @@ -2180,8 +2186,9 @@ public sealed partial class AssistantUsageData /// Per-quota resource usage snapshots, keyed by quota identifier. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("quotaSnapshots")] - public IDictionary? QuotaSnapshots { get; set; } + internal IDictionary? QuotaSnapshots { get; set; } /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max"). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2258,7 +2265,7 @@ public sealed partial class ToolUserRequestedData /// Arguments for the tool invocation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] @@ -2275,7 +2282,7 @@ public sealed partial class ToolExecutionStartData /// Arguments passed to the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Name of the MCP server hosting this tool, when the tool is an MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2383,7 +2390,7 @@ public sealed partial class ToolExecutionCompleteData /// Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolTelemetry")] - public IDictionary? ToolTelemetry { get; set; } + public IDictionary? ToolTelemetry { get; set; } /// Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2565,7 +2572,7 @@ public sealed partial class HookStartData /// Input data passed to the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("input")] - public object? Input { get; set; } + public JsonElement? Input { get; set; } } /// Hook invocation completion details including output, success status, and error information. @@ -2587,7 +2594,7 @@ public sealed partial class HookEndData /// Output data produced by the hook. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("output")] - public object? Output { get; set; } + public JsonElement? Output { get; set; } /// Whether the hook completed successfully. [JsonPropertyName("success")] @@ -2760,7 +2767,7 @@ public sealed partial class ElicitationCompletedData /// The submitted form data when action is 'accept'; keys match the requested schema fields. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + public IDictionary? Content { get; set; } /// Request ID of the resolved elicitation request; clients should dismiss any UI for this request. [JsonPropertyName("requestId")] @@ -2772,7 +2779,7 @@ public sealed partial class SamplingRequestedData { /// The JSON-RPC request ID from the MCP protocol. [JsonPropertyName("mcpRequestId")] - public required object McpRequestId { get; set; } + public required JsonElement McpRequestId { get; set; } /// Unique identifier for this sampling request; used to respond via session.respondToSampling(). [JsonPropertyName("requestId")] @@ -2831,7 +2838,7 @@ public sealed partial class SessionCustomNotificationData /// Source-defined JSON payload for the custom notification. [JsonPropertyName("payload")] - public required object Payload { get; set; } + public required JsonElement Payload { get; set; } /// Namespace for the custom notification producer. [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -2856,7 +2863,7 @@ public sealed partial class ExternalToolRequestedData /// Arguments to pass to the external tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Unique identifier for this request; used to respond via session.respondToExternalTool(). [JsonPropertyName("requestId")] @@ -3181,11 +3188,13 @@ public sealed partial class ShutdownCodeChanges public sealed partial class ShutdownModelMetricRequests { /// Cumulative cost multiplier for requests to this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cost")] public double? Cost { get; set; } /// Total number of API requests made to this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("count")] public long? Count { get; set; } @@ -3240,6 +3249,7 @@ public sealed partial class ShutdownModelMetric public IDictionary? TokenDetails { get; set; } /// Accumulated nano-AI units cost for this model. + [Experimental(Diagnostics.Experimental)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("totalNanoAiu")] public double? TotalNanoAiu { get; set; } @@ -3281,7 +3291,7 @@ public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsageTo /// Per-request cost and usage data from the CAPI copilot_usage response field. /// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsage. -public sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsage +internal sealed partial class CompactionCompleteCompactionTokensUsedCopilotUsage { /// Itemized token usage breakdown. [JsonPropertyName("tokenDetails")] @@ -3308,8 +3318,9 @@ public sealed partial class CompactionCompleteCompactionTokensUsed /// Per-request cost and usage data from the CAPI copilot_usage response field. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("copilotUsage")] - public CompactionCompleteCompactionTokensUsedCopilotUsage? CopilotUsage { get; set; } + internal CompactionCompleteCompactionTokensUsedCopilotUsage? CopilotUsage { get; set; } /// Duration of the compaction LLM call in milliseconds. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] @@ -3526,7 +3537,7 @@ public sealed partial class AssistantMessageToolRequest /// Arguments to pass to the tool, format depends on the tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("arguments")] - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } /// Resolved intention summary describing what this specific call does. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -3585,7 +3596,7 @@ public sealed partial class AssistantUsageCopilotUsageTokenDetail /// Per-request cost and usage data from the CAPI copilot_usage response field. /// Nested data type for AssistantUsageCopilotUsage. -public sealed partial class AssistantUsageCopilotUsage +internal sealed partial class AssistantUsageCopilotUsage { /// Itemized token usage breakdown. [JsonPropertyName("tokenDetails")] @@ -3598,40 +3609,48 @@ public sealed partial class AssistantUsageCopilotUsage /// Schema for the `AssistantUsageQuotaSnapshot` type. /// Nested data type for AssistantUsageQuotaSnapshot. -public sealed partial class AssistantUsageQuotaSnapshot +internal sealed partial class AssistantUsageQuotaSnapshot { /// Total requests allowed by the entitlement. + [JsonInclude] [JsonPropertyName("entitlementRequests")] - public required long EntitlementRequests { get; set; } + internal required long EntitlementRequests { get; set; } /// Whether the user has an unlimited usage entitlement. + [JsonInclude] [JsonPropertyName("isUnlimitedEntitlement")] - public required bool IsUnlimitedEntitlement { get; set; } + internal required bool IsUnlimitedEntitlement { get; set; } /// Number of additional usage requests made this period. + [JsonInclude] [JsonPropertyName("overage")] - public required double Overage { get; set; } + internal required double Overage { get; set; } /// Whether additional usage is allowed when quota is exhausted. + [JsonInclude] [JsonPropertyName("overageAllowedWithExhaustedQuota")] - public required bool OverageAllowedWithExhaustedQuota { get; set; } + internal required bool OverageAllowedWithExhaustedQuota { get; set; } /// Percentage of quota remaining (0 to 100). + [JsonInclude] [JsonPropertyName("remainingPercentage")] - public required double RemainingPercentage { get; set; } + internal required double RemainingPercentage { get; set; } /// Date when the quota resets. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonInclude] [JsonPropertyName("resetDate")] - public DateTimeOffset? ResetDate { get; set; } + internal DateTimeOffset? ResetDate { get; set; } /// Whether usage is still permitted after quota exhaustion. + [JsonInclude] [JsonPropertyName("usageAllowedWithExhaustedQuota")] - public required bool UsageAllowedWithExhaustedQuota { get; set; } + internal required bool UsageAllowedWithExhaustedQuota { get; set; } /// Number of requests already consumed. + [JsonInclude] [JsonPropertyName("usedRequests")] - public required long UsedRequests { get; set; } + internal required long UsedRequests { get; set; } } /// Error details when the tool execution failed. @@ -3978,7 +3997,7 @@ public sealed partial class SystemMessageMetadata /// Template variables used when constructing the prompt. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("variables")] - public IDictionary? Variables { get; set; } + public IDictionary? Variables { get; set; } } /// Schema for the `SystemNotificationAgentCompleted` type. @@ -4282,7 +4301,7 @@ public sealed partial class PermissionRequestMcp : PermissionRequest /// Arguments to pass to the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Whether this MCP tool is read-only (no side effects). [JsonPropertyName("readOnly")] @@ -4382,7 +4401,7 @@ public sealed partial class PermissionRequestCustomTool : PermissionRequest /// Arguments to pass to the custom tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4414,7 +4433,7 @@ public sealed partial class PermissionRequestHook : PermissionRequest /// Arguments of the tool call being gated. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4597,7 +4616,7 @@ public sealed partial class PermissionPromptRequestMcp : PermissionPromptRequest /// Arguments to pass to the MCP tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Name of the MCP server providing the tool. [JsonPropertyName("serverName")] @@ -4693,7 +4712,7 @@ public sealed partial class PermissionPromptRequestCustomTool : PermissionPrompt /// Arguments to pass to the custom tool. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("args")] - public object? Args { get; set; } + public JsonElement? Args { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -4747,7 +4766,7 @@ public sealed partial class PermissionPromptRequestHook : PermissionPromptReques /// Arguments of the tool call being gated. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// Tool call ID that triggered this permission request. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -5117,7 +5136,7 @@ public sealed partial class ElicitationRequestedSchema { /// Form field definitions, keyed by field name. [JsonPropertyName("properties")] - public required IDictionary Properties { get; set; } + public required IDictionary Properties { get; set; } /// List of required field names. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 9af36f535..e75e0cbbf 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -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; @@ -687,7 +687,7 @@ await HandleElicitationRequestAsync( /// /// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC. /// - 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 { @@ -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; @@ -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}", @@ -1000,12 +997,18 @@ public async Task 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 ConfirmAsync(string message, CancellationToken cancellationToken) @@ -1017,9 +1020,9 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat var schema = new UIElicitationSchema { Type = "object", - Properties = new Dictionary + Properties = new Dictionary { - ["confirmed"] = new Dictionary { ["type"] = "boolean", ["default"] = true } + ["confirmed"] = JsonDocument.Parse("""{"type":"boolean","default":true}""").RootElement.Clone() }, Required = ["confirmed"] }; @@ -1029,11 +1032,10 @@ public async Task 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 }; } @@ -1048,12 +1050,13 @@ public async Task 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 + Properties = new Dictionary { - ["selection"] = new Dictionary { ["type"] = "string", ["enum"] = options } + ["selection"] = JsonDocument.Parse($$"""{"type":"string","enum":{{enumJson}}}""").RootElement.Clone() }, Required = ["selection"] }; @@ -1063,12 +1066,7 @@ public async Task 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; @@ -1080,18 +1078,21 @@ public async Task ConfirmAsync(string message, CancellationToken cancellat session.ThrowIfDisposed(); session.AssertElicitation(); - var field = new Dictionary { ["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 { ["value"] = field }, + Properties = new Dictionary + { + ["value"] = JsonDocument.Parse(fieldNode.ToJsonString()).RootElement.Clone() + }, Required = ["value"] }; @@ -1100,12 +1101,7 @@ public async Task 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; diff --git a/dotnet/src/SessionFsProvider.cs b/dotnet/src/SessionFsProvider.cs index 12dfc8770..fbb8df507 100644 --- a/dotnet/src/SessionFsProvider.cs +++ b/dotnet/src/SessionFsProvider.cs @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ using GitHub.Copilot.Rpc; +using System.Text.Json; namespace GitHub.Copilot; @@ -44,7 +45,7 @@ public interface ISessionFsSqliteProvider Task QueryAsync( SessionFsSqliteQueryType queryType, string query, - IDictionary? bindParams, + IDictionary? bindParams, CancellationToken cancellationToken); /// @@ -287,11 +288,16 @@ async Task 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)row.ToDictionary( + kvp => kvp.Key, + kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [], Columns = result?.Columns ?? [], RowsAffected = result?.RowsAffected ?? 0, LastInsertRowid = result?.LastInsertRowid, @@ -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(), + }; } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 5fa1cf734..29fceb40c 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -672,7 +672,7 @@ public sealed class ToolInvocation /// /// Arguments passed to the tool by the language model. /// - public object? Arguments { get; set; } + public JsonElement? Arguments { get; set; } } /// @@ -1123,7 +1123,7 @@ public sealed class PreToolUseHookInput /// Arguments that will be passed to the tool. /// [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } } /// @@ -1278,13 +1278,13 @@ public sealed class PostToolUseHookInput /// Arguments that were passed to the tool. /// [JsonPropertyName("toolArgs")] - public object? ToolArgs { get; set; } + public JsonElement? ToolArgs { get; set; } /// /// Result returned by the tool execution. /// [JsonPropertyName("toolResult")] - public object? ToolResult { get; set; } + public JsonElement? ToolResult { get; set; } } /// diff --git a/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs b/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs index 0ae9c9a7d..4e573ff5c 100644 --- a/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs +++ b/dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs @@ -43,7 +43,7 @@ private SqliteConnection GetOrCreateDb() public Task QueryAsync( SessionFsSqliteQueryType queryType, string query, - IDictionary? bindParams, + IDictionary? bindParams, CancellationToken cancellationToken) { sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query)); @@ -125,7 +125,7 @@ public Task ExistsAsync(CancellationToken cancellationToken) return Task.FromResult(_db is not null); } - private static void AddParams(SqliteCommand cmd, IDictionary? bindParams) + private static void AddParams(SqliteCommand cmd, IDictionary? bindParams) { if (bindParams is null) return; foreach (var (key, value) in bindParams) diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index eee59690c..9cc0785bf 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -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; @@ -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()); Assert.True(toolResult.Success); var answer = await TestHelper.GetFinalAssistantMessageAsync(session2, PendingWorkTimeout); @@ -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); // continuePendingWork=false may interrupt agent continuation before this response, @@ -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()); Assert.True(resultA.Success); await session2.DisposeAsync(); diff --git a/dotnet/test/E2E/RpcMcpConfigE2ETests.cs b/dotnet/test/E2E/RpcMcpConfigE2ETests.cs index d26e5535d..1b4de0ca1 100644 --- a/dotnet/test/E2E/RpcMcpConfigE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpConfigE2ETests.cs @@ -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()); @@ -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(config, TestSharedJsonContext.Default.McpServerConfig)); var afterAdd = await Client.Rpc.Mcp.Config.ListAsync(); var added = GetServerConfig(afterAdd, serverName); Assert.Equal("http", added.GetProperty("type").GetString()); @@ -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(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()); diff --git a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs index 2ed338129..61c8d5878 100644 --- a/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs +++ b/dotnet/test/E2E/RpcTasksAndHandlersE2ETests.cs @@ -4,6 +4,7 @@ using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; +using System.Text.Json; using Xunit; using Xunit.Abstractions; @@ -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()); Assert.False(tool.Success); var command = await session.Rpc.Commands.HandlePendingCommandAsync( diff --git a/dotnet/test/E2E/SessionFsE2ETests.cs b/dotnet/test/E2E/SessionFsE2ETests.cs index b2e41f024..1d0157658 100644 --- a/dotnet/test/E2E/SessionFsE2ETests.cs +++ b/dotnet/test/E2E/SessionFsE2ETests.cs @@ -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 ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken) => + Task ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken) => Task.FromException(exception); Task ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationToken) => diff --git a/dotnet/test/E2E/ToolResultsE2ETests.cs b/dotnet/test/E2E/ToolResultsE2ETests.cs index 75fd9488e..103c7ffe2 100644 --- a/dotnet/test/E2E/ToolResultsE2ETests.cs +++ b/dotnet/test/E2E/ToolResultsE2ETests.cs @@ -113,8 +113,8 @@ static ToolResultAIContent AnalyzeCode([Description("File to analyze")] string f ResultType = "success", ToolTelemetry = new Dictionary { - ["metrics"] = new Dictionary { ["analysisTimeMs"] = 150 }, - ["properties"] = new Dictionary { ["analyzer"] = "eslint" }, + ["metrics"] = JsonDocument.Parse("""{"analysisTimeMs":150}""").RootElement.Clone(), + ["properties"] = JsonDocument.Parse("""{"analyzer":"eslint"}""").RootElement.Clone(), }, }); } diff --git a/dotnet/test/TestJsonContext.cs b/dotnet/test/TestJsonContext.cs new file mode 100644 index 000000000..12a576510 --- /dev/null +++ b/dotnet/test/TestJsonContext.cs @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json.Serialization; + +namespace GitHub.Copilot.Test; + +[JsonSourceGenerationOptions(System.Text.Json.JsonSerializerDefaults.Web)] +[JsonSerializable(typeof(string[]))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(McpServerConfig))] +[JsonSerializable(typeof(McpHttpServerConfig))] +[JsonSerializable(typeof(McpStdioServerConfig))] +internal partial class TestSharedJsonContext : JsonSerializerContext; diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 3e6d4661f..47b4ac3f7 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -66,7 +66,7 @@ public class SessionEventSerializationTests Content = "ok", DetailedContent = "ok", }, - ToolTelemetry = new Dictionary + ToolTelemetry = new Dictionary { ["properties"] = ParseJsonElement("""{"command":"view"}"""), ["metrics"] = ParseJsonElement("""{"resultLength":2}"""), @@ -84,7 +84,6 @@ public class SessionEventSerializationTests Data = new SessionShutdownData { ShutdownType = ShutdownType.Routine, - TotalPremiumRequests = 1, TotalApiDuration = TimeSpan.FromMilliseconds(100), SessionStartTime = 1773609948932, CodeChanges = new ShutdownCodeChanges diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index d2a332e49..36a5e1831 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -81,6 +81,7 @@ type AgentInfo struct { ID string `json:"id"` // MCP server configurations attached to this agent, keyed by server name. Server config // shape mirrors the MCP `mcpServers` schema. + // Experimental: McpServers is part of an experimental API and may change or be removed. McpServers map[string]any `json:"mcpServers,omitempty"` // Preferred model id for this agent. When omitted, inherits the outer agent's model. Model *string `json:"model,omitempty"` @@ -3403,6 +3404,8 @@ type SendRequest struct { RequiredTool *string `json:"requiredTool,omitempty"` // Optional provenance tag copied to the resulting user.message event. Supported values are // `system`, `command-*`, and `schedule-*`. + // Internal: Source is part of the SDK's internal API surface and is not intended for + // external use. Source any `json:"source,omitempty"` // W3C Trace Context traceparent header for distributed tracing of this agent turn Traceparent *string `json:"traceparent,omitempty"` @@ -4322,6 +4325,8 @@ type SessionTelemetrySetFeatureOverridesResult struct { type SessionUpdateOptionsParams struct { // Additional content-exclusion policies to merge into the session's policy set. Opaque // shape; see `ContentExclusionApiResponse` in the runtime. + // Experimental: AdditionalContentExclusionPolicies is part of an experimental API and may + // change or be removed. AdditionalContentExclusionPolicies []any `json:"additionalContentExclusionPolicies,omitempty"` // Runtime context discriminator (e.g., `cli`, `actions`). AgentContext *string `json:"agentContext,omitempty"` @@ -4382,12 +4387,14 @@ type SessionUpdateOptionsParams struct { Model *string `json:"model,omitempty"` // Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the // runtime. + // Experimental: Provider is part of an experimental API and may change or be removed. Provider any `json:"provider,omitempty"` // Reasoning effort for the selected model (model-defined enum). ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Whether the session is running in an interactive UI. RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + // Experimental: SandboxConfig is part of an experimental API and may change or be removed. SandboxConfig any `json:"sandboxConfig,omitempty"` // Shell init profile (`None` or `NonInteractive`). ShellInitProfile *string `json:"shellInitProfile,omitempty"` diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index fc3a8de60..e9fac278b 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -170,8 +170,10 @@ func (*AssistantReasoningData) Type() SessionEventType { return SessionEventType // Assistant response containing text content, optional tool requests, and interaction metadata type AssistantMessageData struct { // Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping + // Experimental: AnthropicAdvisorBlocks is part of an experimental API and may change or be removed. AnthropicAdvisorBlocks []any `json:"anthropicAdvisorBlocks,omitempty"` // Anthropic advisor model ID used for this response, for timeline display on replay + // Experimental: AnthropicAdvisorModel is part of an experimental API and may change or be removed. AnthropicAdvisorModel *string `json:"anthropicAdvisorModel,omitempty"` // The assistant's text response content Content string `json:"content"` @@ -532,8 +534,10 @@ type AssistantUsageData struct { // Number of tokens written to prompt cache CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` // Per-request cost and usage data from the CAPI copilot_usage response field + // Internal: CopilotUsage is part of the SDK's internal API surface and is not intended for external use. CopilotUsage *AssistantUsageCopilotUsage `json:"copilotUsage,omitempty"` // Model multiplier cost for billing purposes + // Experimental: Cost is part of an experimental API and may change or be removed. Cost *float64 `json:"cost,omitempty"` // Duration of the API call in milliseconds Duration *int64 `json:"duration,omitempty"` @@ -553,6 +557,7 @@ type AssistantUsageData struct { // GitHub request tracing ID (x-github-request-id header) for server-side log correlation ProviderCallID *string `json:"providerCallId,omitempty"` // Per-quota resource usage snapshots, keyed by quota identifier + // Internal: QuotaSnapshots is part of the SDK's internal API surface and is not intended for external use. QuotaSnapshots map[string]AssistantUsageQuotaSnapshot `json:"quotaSnapshots,omitempty"` // Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") ReasoningEffort *string `json:"reasoningEffort,omitempty"` @@ -1052,8 +1057,10 @@ type SessionShutdownData struct { // Cumulative time spent in API calls during the session, in milliseconds TotalAPIDurationMs int64 `json:"totalApiDurationMs"` // Session-wide accumulated nano-AI units cost + // Experimental: TotalNanoAiu is part of an experimental API and may change or be removed. TotalNanoAiu *float64 `json:"totalNanoAiu,omitempty"` // Total number of premium API requests used during the session + // Internal: TotalPremiumRequests is part of the SDK's internal API surface and is not intended for external use. TotalPremiumRequests *float64 `json:"totalPremiumRequests,omitempty"` } @@ -1487,20 +1494,28 @@ type AssistantUsageCopilotUsageTokenDetail struct { // Schema for the `AssistantUsageQuotaSnapshot` type. type AssistantUsageQuotaSnapshot struct { // Total requests allowed by the entitlement + // Internal: EntitlementRequests is part of the SDK's internal API surface and is not intended for external use. EntitlementRequests int64 `json:"entitlementRequests"` // Whether the user has an unlimited usage entitlement + // Internal: IsUnlimitedEntitlement is part of the SDK's internal API surface and is not intended for external use. IsUnlimitedEntitlement bool `json:"isUnlimitedEntitlement"` // Number of additional usage requests made this period + // Internal: Overage is part of the SDK's internal API surface and is not intended for external use. Overage float64 `json:"overage"` // Whether additional usage is allowed when quota is exhausted + // Internal: OverageAllowedWithExhaustedQuota is part of the SDK's internal API surface and is not intended for external use. OverageAllowedWithExhaustedQuota bool `json:"overageAllowedWithExhaustedQuota"` // Percentage of quota remaining (0 to 100) + // Internal: RemainingPercentage is part of the SDK's internal API surface and is not intended for external use. RemainingPercentage float64 `json:"remainingPercentage"` // Date when the quota resets + // Internal: ResetDate is part of the SDK's internal API surface and is not intended for external use. ResetDate *time.Time `json:"resetDate,omitempty"` // Whether usage is still permitted after quota exhaustion + // Internal: UsageAllowedWithExhaustedQuota is part of the SDK's internal API surface and is not intended for external use. UsageAllowedWithExhaustedQuota bool `json:"usageAllowedWithExhaustedQuota"` // Number of requests already consumed + // Internal: UsedRequests is part of the SDK's internal API surface and is not intended for external use. UsedRequests int64 `json:"usedRequests"` } @@ -1525,6 +1540,7 @@ type CompactionCompleteCompactionTokensUsed struct { // Tokens written to prompt cache in the compaction LLM call CacheWriteTokens *int64 `json:"cacheWriteTokens,omitempty"` // Per-request cost and usage data from the CAPI copilot_usage response field + // Internal: CopilotUsage is part of the SDK's internal API surface and is not intended for external use. CopilotUsage *CompactionCompleteCompactionTokensUsedCopilotUsage `json:"copilotUsage,omitempty"` // Duration of the compaction LLM call in milliseconds Duration *int64 `json:"duration,omitempty"` @@ -2229,6 +2245,7 @@ type ShutdownModelMetric struct { // Token count details per type TokenDetails map[string]ShutdownModelMetricTokenDetail `json:"tokenDetails,omitempty"` // Accumulated nano-AI units cost for this model + // Experimental: TotalNanoAiu is part of an experimental API and may change or be removed. TotalNanoAiu *float64 `json:"totalNanoAiu,omitempty"` // Token usage breakdown Usage ShutdownModelMetricUsage `json:"usage"` @@ -2237,8 +2254,10 @@ type ShutdownModelMetric struct { // Request count and cost metrics type ShutdownModelMetricRequests struct { // Cumulative cost multiplier for requests to this model + // Experimental: Cost is part of an experimental API and may change or be removed. Cost *float64 `json:"cost,omitempty"` // Total number of API requests made to this model + // Experimental: Count is part of an experimental API and may change or be removed. Count *int64 `json:"count,omitempty"` } diff --git a/go/zsession_events.go b/go/zsession_events.go index b9cd90a83..d3b246a86 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -21,10 +21,8 @@ type ( AssistantTurnEndData = rpc.AssistantTurnEndData AssistantTurnStartData = rpc.AssistantTurnStartData AssistantUsageAPIEndpoint = rpc.AssistantUsageAPIEndpoint - AssistantUsageCopilotUsage = rpc.AssistantUsageCopilotUsage AssistantUsageCopilotUsageTokenDetail = rpc.AssistantUsageCopilotUsageTokenDetail AssistantUsageData = rpc.AssistantUsageData - AssistantUsageQuotaSnapshot = rpc.AssistantUsageQuotaSnapshot Attachment = rpc.Attachment AttachmentType = rpc.AttachmentType AutoModeSwitchCompletedData = rpc.AutoModeSwitchCompletedData @@ -38,7 +36,6 @@ type ( CommandsChangedCommand = rpc.CommandsChangedCommand CommandsChangedData = rpc.CommandsChangedData CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed - CompactionCompleteCompactionTokensUsedCopilotUsage = rpc.CompactionCompleteCompactionTokensUsedCopilotUsage CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent CustomNotificationPayload = rpc.CustomNotificationPayload diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 86c19d6f8..7263e602c 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -1188,11 +1188,7 @@ export interface AgentInfo { * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. */ mcpServers?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Skill names preloaded into this agent's context. Omitted means none. @@ -2146,11 +2142,7 @@ export interface ExternalToolTextResultForLlm { * Optional tool-specific telemetry */ toolTelemetry?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Base64-encoded binary results returned to the model @@ -6105,11 +6097,7 @@ export interface SessionFsSqliteQueryResult { * For SELECT: array of row objects. For others: empty array. */ rows: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }[]; /** * Column names from the result set @@ -6979,9 +6967,7 @@ export interface SessionUpdateOptionsParams { /** * Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. */ - additionalContentExclusionPolicies?: { - [k: string]: unknown | undefined; - }[]; + additionalContentExclusionPolicies?: unknown[]; /** * Whether to expose the `manage_schedule` tool to the agent. The runtime always owns the per-session schedule registry; this flag only controls tool exposure (typically gated to staff users). */ @@ -7814,11 +7800,7 @@ export interface Tool { * JSON Schema for the tool's input parameters */ parameters?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Optional instructions for how to use this tool effectively diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index f00a04bec..ae0f1eb72 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -2404,9 +2404,7 @@ export interface AssistantMessageData { /** * Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping */ - anthropicAdvisorBlocks?: { - [k: string]: unknown | undefined; - }[]; + anthropicAdvisorBlocks?: unknown[]; /** * Anthropic advisor model ID used for this response, for timeline display on replay */ @@ -3176,11 +3174,7 @@ export interface ToolExecutionCompleteData { * Tool-specific telemetry data (e.g., CodeQL check counts, grep match counts) */ toolTelemetry?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event @@ -3886,11 +3880,7 @@ export interface SystemMessageMetadata { * Template variables used when constructing the prompt */ variables?: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; } /** @@ -5136,11 +5126,7 @@ export interface ElicitationRequestedSchema { * Form field definitions, keyed by field name */ properties: { - [k: string]: - | { - [k: string]: unknown | undefined; - } - | undefined; + [k: string]: unknown | undefined; }; /** * List of required field names diff --git a/python/copilot/client.py b/python/copilot/client.py index b65f62481..a52b8711f 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -38,9 +38,9 @@ from ._telemetry import get_trace_context from .generated.rpc import ( ClientSessionApiHandlers, - ConnectRequest, RemoteSessionMode, ServerRpc, + _ConnectRequest, _InternalServerRpc, from_datetime, register_client_session_api_handlers, @@ -2628,8 +2628,8 @@ async def _verify_protocol_version(self) -> None: server_version: int | None try: - connect_result = await _InternalServerRpc(self._client).connect( - ConnectRequest(token=self._effective_connection_token) + connect_result = await _InternalServerRpc(self._client)._connect( + _ConnectRequest(token=self._effective_connection_token) ) server_version = connect_result.protocol_version except JsonRpcError as err: diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 929aa79e6..ab3d38eb4 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -452,17 +452,17 @@ def to_dict(self) -> dict: # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class ConnectRequest: +class _ConnectRequest: """Optional connection token presented by the SDK client during the handshake.""" token: str | None = None """Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN""" @staticmethod - def from_dict(obj: Any) -> 'ConnectRequest': + def from_dict(obj: Any) -> '_ConnectRequest': assert isinstance(obj, dict) token = from_union([from_str, from_none], obj.get("token")) - return ConnectRequest(token) + return _ConnectRequest(token) def to_dict(self) -> dict: result: dict = {} @@ -472,7 +472,7 @@ def to_dict(self) -> dict: # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass -class ConnectResult: +class _ConnectResult: """Handshake result reporting the server's protocol version and package version on success.""" ok: bool @@ -485,12 +485,12 @@ class ConnectResult: """Server package version""" @staticmethod - def from_dict(obj: Any) -> 'ConnectResult': + def from_dict(obj: Any) -> '_ConnectResult': assert isinstance(obj, dict) ok = from_bool(obj.get("ok")) protocol_version = from_int(obj.get("protocolVersion")) version = from_str(obj.get("version")) - return ConnectResult(ok, protocol_version, version) + return _ConnectResult(ok, protocol_version, version) def to_dict(self) -> dict: result: dict = {} @@ -9420,6 +9420,7 @@ class SendRequest: """If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange """ + # Internal: this field is an internal SDK API and is not part of the public surface. source: Any = None """Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. @@ -13706,8 +13707,8 @@ class RPC: connected_remote_session_metadata_kind: ConnectedRemoteSessionMetadataKind connected_remote_session_metadata_repository: ConnectedRemoteSessionMetadataRepository connect_remote_session_params: ConnectRemoteSessionParams - connect_request: ConnectRequest - connect_result: ConnectResult + connect_request: _ConnectRequest + connect_result: _ConnectResult content_filter_mode: ContentFilterMode copilot_api_token_auth_info: CopilotAPITokenAuthInfo copilot_user_response: CopilotUserResponse @@ -14217,8 +14218,8 @@ def from_dict(obj: Any) -> 'RPC': connected_remote_session_metadata_kind = ConnectedRemoteSessionMetadataKind(obj.get("ConnectedRemoteSessionMetadataKind")) connected_remote_session_metadata_repository = ConnectedRemoteSessionMetadataRepository.from_dict(obj.get("ConnectedRemoteSessionMetadataRepository")) connect_remote_session_params = ConnectRemoteSessionParams.from_dict(obj.get("ConnectRemoteSessionParams")) - connect_request = ConnectRequest.from_dict(obj.get("ConnectRequest")) - connect_result = ConnectResult.from_dict(obj.get("ConnectResult")) + connect_request = _ConnectRequest.from_dict(obj.get("ConnectRequest")) + connect_result = _ConnectResult.from_dict(obj.get("ConnectResult")) content_filter_mode = ContentFilterMode(obj.get("ContentFilterMode")) copilot_api_token_auth_info = CopilotAPITokenAuthInfo.from_dict(obj.get("CopilotApiTokenAuthInfo")) copilot_user_response = CopilotUserResponse.from_dict(obj.get("CopilotUserResponse")) @@ -14728,8 +14729,8 @@ def to_dict(self) -> dict: result["ConnectedRemoteSessionMetadataKind"] = to_enum(ConnectedRemoteSessionMetadataKind, self.connected_remote_session_metadata_kind) result["ConnectedRemoteSessionMetadataRepository"] = to_class(ConnectedRemoteSessionMetadataRepository, self.connected_remote_session_metadata_repository) result["ConnectRemoteSessionParams"] = to_class(ConnectRemoteSessionParams, self.connect_remote_session_params) - result["ConnectRequest"] = to_class(ConnectRequest, self.connect_request) - result["ConnectResult"] = to_class(ConnectResult, self.connect_result) + result["ConnectRequest"] = to_class(_ConnectRequest, self.connect_request) + result["ConnectResult"] = to_class(_ConnectResult, self.connect_result) result["ContentFilterMode"] = to_enum(ContentFilterMode, self.content_filter_mode) result["CopilotApiTokenAuthInfo"] = to_class(CopilotAPITokenAuthInfo, self.copilot_api_token_auth_info) result["CopilotUserResponse"] = to_class(CopilotUserResponse, self.copilot_user_response) @@ -15656,10 +15657,10 @@ class _InternalServerRpc: def __init__(self, client: "JsonRpcClient"): self._client = client - async def connect(self, params: ConnectRequest, *, timeout: float | None = None) -> ConnectResult: + async def _connect(self, params: _ConnectRequest, *, timeout: float | None = None) -> _ConnectResult: "Performs the SDK server connection handshake and validates the optional connection token.\n\nArgs:\n params: Optional connection token presented by the SDK client during the handshake.\n\nReturns:\n Handshake result reporting the server's protocol version and package version on success.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) + return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 4b72621df..b504fff90 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -320,7 +320,9 @@ class AssistantMessageData: "Assistant response containing text content, optional tool requests, and interaction metadata" content: str message_id: str + # Experimental: this field is part of an experimental API and may change or be removed. anthropic_advisor_blocks: list[Any] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. anthropic_advisor_model: str | None = None encrypted_content: str | None = None interaction_id: str | None = None @@ -619,17 +621,17 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageCopilotUsage: +class _AssistantUsageCopilotUsage: "Per-request cost and usage data from the CAPI copilot_usage response field" token_details: list[AssistantUsageCopilotUsageTokenDetail] total_nano_aiu: float @staticmethod - def from_dict(obj: Any) -> "AssistantUsageCopilotUsage": + def from_dict(obj: Any) -> "_AssistantUsageCopilotUsage": assert isinstance(obj, dict) token_details = from_list(AssistantUsageCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return AssistantUsageCopilotUsage( + return _AssistantUsageCopilotUsage( token_details=token_details, total_nano_aiu=total_nano_aiu, ) @@ -680,7 +682,9 @@ class AssistantUsageData: api_endpoint: AssistantUsageApiEndpoint | None = None cache_read_tokens: int | None = None cache_write_tokens: int | None = None - copilot_usage: AssistantUsageCopilotUsage | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _copilot_usage: _AssistantUsageCopilotUsage | None = None + # Experimental: this field is part of an experimental API and may change or be removed. cost: float | None = None duration: timedelta | None = None initiator: str | None = None @@ -690,7 +694,8 @@ class AssistantUsageData: # Deprecated: this field is deprecated. parent_tool_call_id: str | None = None provider_call_id: str | None = None - quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _quota_snapshots: dict[str, _AssistantUsageQuotaSnapshot] | None = None reasoning_effort: str | None = None reasoning_tokens: int | None = None time_to_first_token: timedelta | None = None @@ -703,7 +708,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_endpoint = from_union([from_none, lambda x: parse_enum(AssistantUsageApiEndpoint, x)], obj.get("apiEndpoint")) cache_read_tokens = from_union([from_none, from_int], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, from_int], obj.get("cacheWriteTokens")) - copilot_usage = from_union([from_none, AssistantUsageCopilotUsage.from_dict], obj.get("copilotUsage")) + _copilot_usage = from_union([from_none, _AssistantUsageCopilotUsage.from_dict], obj.get("copilotUsage")) cost = from_union([from_none, from_float], obj.get("cost")) duration = from_union([from_none, from_timedelta], obj.get("duration")) initiator = from_union([from_none, from_str], obj.get("initiator")) @@ -712,7 +717,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens = from_union([from_none, from_int], obj.get("outputTokens")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) - quota_snapshots = from_union([from_none, lambda x: from_dict(AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) + _quota_snapshots = from_union([from_none, lambda x: from_dict(_AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_tokens = from_union([from_none, from_int], obj.get("reasoningTokens")) time_to_first_token = from_union([from_none, from_timedelta], obj.get("timeToFirstTokenMs")) @@ -722,7 +727,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_endpoint=api_endpoint, cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, - copilot_usage=copilot_usage, + _copilot_usage=_copilot_usage, cost=cost, duration=duration, initiator=initiator, @@ -731,7 +736,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": output_tokens=output_tokens, parent_tool_call_id=parent_tool_call_id, provider_call_id=provider_call_id, - quota_snapshots=quota_snapshots, + _quota_snapshots=_quota_snapshots, reasoning_effort=reasoning_effort, reasoning_tokens=reasoning_tokens, time_to_first_token=time_to_first_token, @@ -748,8 +753,8 @@ def to_dict(self) -> dict: result["cacheReadTokens"] = from_union([from_none, to_int], self.cache_read_tokens) if self.cache_write_tokens is not None: result["cacheWriteTokens"] = from_union([from_none, to_int], self.cache_write_tokens) - if self.copilot_usage is not None: - result["copilotUsage"] = from_union([from_none, lambda x: to_class(AssistantUsageCopilotUsage, x)], self.copilot_usage) + if self._copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(_AssistantUsageCopilotUsage, x)], self._copilot_usage) if self.cost is not None: result["cost"] = from_union([from_none, to_float], self.cost) if self.duration is not None: @@ -766,8 +771,8 @@ def to_dict(self) -> dict: result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) - if self.quota_snapshots is not None: - result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(AssistantUsageQuotaSnapshot, x), x)], self.quota_snapshots) + if self._quota_snapshots is not None: + result["quotaSnapshots"] = from_union([from_none, lambda x: from_dict(lambda x: to_class(_AssistantUsageQuotaSnapshot, x), x)], self._quota_snapshots) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) if self.reasoning_tokens is not None: @@ -778,50 +783,58 @@ def to_dict(self) -> dict: @dataclass -class AssistantUsageQuotaSnapshot: - "Schema for the `AssistantUsageQuotaSnapshot` type." - entitlement_requests: int - is_unlimited_entitlement: bool - overage: float - overage_allowed_with_exhausted_quota: bool - remaining_percentage: float - usage_allowed_with_exhausted_quota: bool - used_requests: int - reset_date: datetime | None = None +class _AssistantUsageQuotaSnapshot: + "Schema for the `_AssistantUsageQuotaSnapshot` type." + # Internal: this field is an internal SDK API and is not part of the public surface. + _entitlement_requests: int + # Internal: this field is an internal SDK API and is not part of the public surface. + _is_unlimited_entitlement: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _overage: float + # Internal: this field is an internal SDK API and is not part of the public surface. + _overage_allowed_with_exhausted_quota: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _remaining_percentage: float + # Internal: this field is an internal SDK API and is not part of the public surface. + _usage_allowed_with_exhausted_quota: bool + # Internal: this field is an internal SDK API and is not part of the public surface. + _used_requests: int + # Internal: this field is an internal SDK API and is not part of the public surface. + _reset_date: datetime | None = None @staticmethod - def from_dict(obj: Any) -> "AssistantUsageQuotaSnapshot": + def from_dict(obj: Any) -> "_AssistantUsageQuotaSnapshot": assert isinstance(obj, dict) - entitlement_requests = from_int(obj.get("entitlementRequests")) - is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) - overage = from_float(obj.get("overage")) - overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) - remaining_percentage = from_float(obj.get("remainingPercentage")) - usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) - used_requests = from_int(obj.get("usedRequests")) - reset_date = from_union([from_none, from_datetime], obj.get("resetDate")) - return AssistantUsageQuotaSnapshot( - entitlement_requests=entitlement_requests, - is_unlimited_entitlement=is_unlimited_entitlement, - overage=overage, - overage_allowed_with_exhausted_quota=overage_allowed_with_exhausted_quota, - remaining_percentage=remaining_percentage, - usage_allowed_with_exhausted_quota=usage_allowed_with_exhausted_quota, - used_requests=used_requests, - reset_date=reset_date, + _entitlement_requests = from_int(obj.get("entitlementRequests")) + _is_unlimited_entitlement = from_bool(obj.get("isUnlimitedEntitlement")) + _overage = from_float(obj.get("overage")) + _overage_allowed_with_exhausted_quota = from_bool(obj.get("overageAllowedWithExhaustedQuota")) + _remaining_percentage = from_float(obj.get("remainingPercentage")) + _usage_allowed_with_exhausted_quota = from_bool(obj.get("usageAllowedWithExhaustedQuota")) + _used_requests = from_int(obj.get("usedRequests")) + _reset_date = from_union([from_none, from_datetime], obj.get("resetDate")) + return _AssistantUsageQuotaSnapshot( + _entitlement_requests=_entitlement_requests, + _is_unlimited_entitlement=_is_unlimited_entitlement, + _overage=_overage, + _overage_allowed_with_exhausted_quota=_overage_allowed_with_exhausted_quota, + _remaining_percentage=_remaining_percentage, + _usage_allowed_with_exhausted_quota=_usage_allowed_with_exhausted_quota, + _used_requests=_used_requests, + _reset_date=_reset_date, ) def to_dict(self) -> dict: result: dict = {} - result["entitlementRequests"] = to_int(self.entitlement_requests) - result["isUnlimitedEntitlement"] = from_bool(self.is_unlimited_entitlement) - result["overage"] = to_float(self.overage) - result["overageAllowedWithExhaustedQuota"] = from_bool(self.overage_allowed_with_exhausted_quota) - result["remainingPercentage"] = to_float(self.remaining_percentage) - result["usageAllowedWithExhaustedQuota"] = from_bool(self.usage_allowed_with_exhausted_quota) - result["usedRequests"] = to_int(self.used_requests) - if self.reset_date is not None: - result["resetDate"] = from_union([from_none, to_datetime], self.reset_date) + result["entitlementRequests"] = to_int(self._entitlement_requests) + result["isUnlimitedEntitlement"] = from_bool(self._is_unlimited_entitlement) + result["overage"] = to_float(self._overage) + result["overageAllowedWithExhaustedQuota"] = from_bool(self._overage_allowed_with_exhausted_quota) + result["remainingPercentage"] = to_float(self._remaining_percentage) + result["usageAllowedWithExhaustedQuota"] = from_bool(self._usage_allowed_with_exhausted_quota) + result["usedRequests"] = to_int(self._used_requests) + if self._reset_date is not None: + result["resetDate"] = from_union([from_none, to_datetime], self._reset_date) return result @@ -1038,7 +1051,8 @@ class CompactionCompleteCompactionTokensUsed: "Token usage breakdown for the compaction LLM call (aligned with assistant.usage format)" cache_read_tokens: int | None = None cache_write_tokens: int | None = None - copilot_usage: CompactionCompleteCompactionTokensUsedCopilotUsage | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _copilot_usage: _CompactionCompleteCompactionTokensUsedCopilotUsage | None = None duration: timedelta | None = None input_tokens: int | None = None model: str | None = None @@ -1049,7 +1063,7 @@ def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": assert isinstance(obj, dict) cache_read_tokens = from_union([from_none, from_int], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, from_int], obj.get("cacheWriteTokens")) - copilot_usage = from_union([from_none, CompactionCompleteCompactionTokensUsedCopilotUsage.from_dict], obj.get("copilotUsage")) + _copilot_usage = from_union([from_none, _CompactionCompleteCompactionTokensUsedCopilotUsage.from_dict], obj.get("copilotUsage")) duration = from_union([from_none, from_timedelta], obj.get("duration")) input_tokens = from_union([from_none, from_int], obj.get("inputTokens")) model = from_union([from_none, from_str], obj.get("model")) @@ -1057,7 +1071,7 @@ def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": return CompactionCompleteCompactionTokensUsed( cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, - copilot_usage=copilot_usage, + _copilot_usage=_copilot_usage, duration=duration, input_tokens=input_tokens, model=model, @@ -1070,8 +1084,8 @@ def to_dict(self) -> dict: result["cacheReadTokens"] = from_union([from_none, to_int], self.cache_read_tokens) if self.cache_write_tokens is not None: result["cacheWriteTokens"] = from_union([from_none, to_int], self.cache_write_tokens) - if self.copilot_usage is not None: - result["copilotUsage"] = from_union([from_none, lambda x: to_class(CompactionCompleteCompactionTokensUsedCopilotUsage, x)], self.copilot_usage) + if self._copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(_CompactionCompleteCompactionTokensUsedCopilotUsage, x)], self._copilot_usage) if self.duration is not None: result["duration"] = from_union([from_none, to_timedelta_int], self.duration) if self.input_tokens is not None: @@ -1084,17 +1098,17 @@ def to_dict(self) -> dict: @dataclass -class CompactionCompleteCompactionTokensUsedCopilotUsage: +class _CompactionCompleteCompactionTokensUsedCopilotUsage: "Per-request cost and usage data from the CAPI copilot_usage response field" token_details: list[CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail] total_nano_aiu: float @staticmethod - def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsedCopilotUsage": + def from_dict(obj: Any) -> "_CompactionCompleteCompactionTokensUsedCopilotUsage": assert isinstance(obj, dict) token_details = from_list(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) total_nano_aiu = from_float(obj.get("totalNanoAiu")) - return CompactionCompleteCompactionTokensUsedCopilotUsage( + return _CompactionCompleteCompactionTokensUsedCopilotUsage( token_details=token_details, total_nano_aiu=total_nano_aiu, ) @@ -3681,8 +3695,10 @@ class SessionShutdownData: system_tokens: int | None = None token_details: dict[str, ShutdownTokenDetail] | None = None tool_definitions_tokens: int | None = None + # Experimental: this field is part of an experimental API and may change or be removed. total_nano_aiu: float | None = None - total_premium_requests: float | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. + _total_premium_requests: float | None = None @staticmethod def from_dict(obj: Any) -> "SessionShutdownData": @@ -3700,7 +3716,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": token_details = from_union([from_none, lambda x: from_dict(ShutdownTokenDetail.from_dict, x)], obj.get("tokenDetails")) tool_definitions_tokens = from_union([from_none, from_int], obj.get("toolDefinitionsTokens")) total_nano_aiu = from_union([from_none, from_float], obj.get("totalNanoAiu")) - total_premium_requests = from_union([from_none, from_float], obj.get("totalPremiumRequests")) + _total_premium_requests = from_union([from_none, from_float], obj.get("totalPremiumRequests")) return SessionShutdownData( code_changes=code_changes, model_metrics=model_metrics, @@ -3715,7 +3731,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": token_details=token_details, tool_definitions_tokens=tool_definitions_tokens, total_nano_aiu=total_nano_aiu, - total_premium_requests=total_premium_requests, + _total_premium_requests=_total_premium_requests, ) def to_dict(self) -> dict: @@ -3741,8 +3757,8 @@ def to_dict(self) -> dict: result["toolDefinitionsTokens"] = from_union([from_none, to_int], self.tool_definitions_tokens) if self.total_nano_aiu is not None: result["totalNanoAiu"] = from_union([from_none, to_float], self.total_nano_aiu) - if self.total_premium_requests is not None: - result["totalPremiumRequests"] = from_union([from_none, to_float], self.total_premium_requests) + if self._total_premium_requests is not None: + result["totalPremiumRequests"] = from_union([from_none, to_float], self._total_premium_requests) return result @@ -4099,6 +4115,7 @@ class ShutdownModelMetric: requests: ShutdownModelMetricRequests usage: ShutdownModelMetricUsage token_details: dict[str, ShutdownModelMetricTokenDetail] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. total_nano_aiu: float | None = None @staticmethod @@ -4129,7 +4146,9 @@ def to_dict(self) -> dict: @dataclass class ShutdownModelMetricRequests: "Request count and cost metrics" + # Experimental: this field is part of an experimental API and may change or be removed. cost: float | None = None + # Experimental: this field is part of an experimental API and may change or be removed. count: int | None = None @staticmethod diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 0ef378f48..843eb5afd 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,7 +10,8 @@ use super::session_events::{ AbortReason, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval, }; -use crate::types::{RequestId, SessionEvent, SessionId}; +use crate::types::SessionEvent; +use crate::types::{RequestId, SessionId}; /// JSON-RPC method name constants. pub mod rpc_methods { @@ -472,6 +473,13 @@ pub struct AgentInfo { /// Stable identifier for selection. For most agents this is the same as `name`; for plugin/builtin agents it may differ. Always populated; defaults to `name` when no distinct id was assigned. pub id: String, /// MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub mcp_servers: HashMap, /// Preferred model id for this agent. When omitted, inherits the outer agent's model. @@ -1102,7 +1110,7 @@ pub struct ConnectRemoteSessionParams { /// Optional connection token presented by the SDK client during the handshake. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ConnectRequest { +pub(crate) struct ConnectRequest { /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, @@ -1111,7 +1119,7 @@ pub struct ConnectRequest { /// Handshake result reporting the server's protocol version and package version on success. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ConnectResult { +pub(crate) struct ConnectResult { /// Always true on success pub ok: bool, /// Server protocol version number @@ -5160,8 +5168,9 @@ pub struct SendRequest { #[serde(skip_serializing_if = "Option::is_none")] pub required_tool: Option, /// Optional provenance tag copied to the resulting user.message event. Supported values are `system`, `command-*`, and `schedule-*`. + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, + pub(crate) source: Option, /// W3C Trace Context traceparent header for distributed tracing of this agent turn #[serde(skip_serializing_if = "Option::is_none")] pub traceparent: Option, @@ -6397,6 +6406,13 @@ pub struct SessionsSetAdditionalPluginsResult {} #[serde(rename_all = "camelCase")] pub struct SessionUpdateOptionsParams { /// Additional content-exclusion policies to merge into the session's policy set. Opaque shape; see `ContentExclusionApiResponse` in the runtime. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub additional_content_exclusion_policies: Vec, /// Runtime context discriminator (e.g., `cli`, `actions`). @@ -6475,6 +6491,13 @@ pub struct SessionUpdateOptionsParams { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Custom model-provider configuration (BYOK). Opaque shape; see `ProviderConfig` in the runtime. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, /// Reasoning effort for the selected model (model-defined enum). @@ -6484,6 +6507,13 @@ pub struct SessionUpdateOptionsParams { #[serde(skip_serializing_if = "Option::is_none")] pub running_in_interactive_mode: Option, /// Sandbox configuration shape; opaque to SDK consumers. See `SandboxConfig` in the runtime. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub sandbox_config: Option, /// Shell init profile (`None` or `NonInteractive`). diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index b5599e09a..f8d2ecb30 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -107,7 +107,7 @@ impl<'a> ClientRpc<'a> { /// # Returns /// /// Handshake result reporting the server's protocol version and package version on success. - pub async fn connect(&self, params: ConnectRequest) -> Result { + pub(crate) async fn connect(&self, params: ConnectRequest) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 1f6334466..20a123cd1 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -723,9 +723,23 @@ pub struct ShutdownCodeChanges { #[serde(rename_all = "camelCase")] pub struct ShutdownModelMetricRequests { /// Cumulative cost multiplier for requests to this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub cost: Option, /// Total number of API requests made to this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub count: Option, } @@ -765,6 +779,13 @@ pub struct ShutdownModelMetric { #[serde(default)] pub token_details: HashMap, /// Accumulated nano-AI units cost for this model + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub total_nano_aiu: Option, /// Token usage breakdown @@ -815,11 +836,19 @@ pub struct SessionShutdownData { /// Cumulative time spent in API calls during the session, in milliseconds pub total_api_duration_ms: i64, /// Session-wide accumulated nano-AI units cost + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub total_nano_aiu: Option, /// Total number of premium API requests used during the session + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub total_premium_requests: Option, + pub(crate) total_premium_requests: Option, } /// Session event "session.context_changed". Updated working directory and git context after the change @@ -907,7 +936,7 @@ pub struct CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail { /// Per-request cost and usage data from the CAPI copilot_usage response field #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CompactionCompleteCompactionTokensUsedCopilotUsage { +pub(crate) struct CompactionCompleteCompactionTokensUsedCopilotUsage { /// Itemized token usage breakdown pub token_details: Vec, /// Total cost in nano-AI units for this request @@ -925,8 +954,9 @@ pub struct CompactionCompleteCompactionTokensUsed { #[serde(skip_serializing_if = "Option::is_none")] pub cache_write_tokens: Option, /// Per-request cost and usage data from the CAPI copilot_usage response field + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_usage: Option, + pub(crate) copilot_usage: Option, /// Duration of the compaction LLM call in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, @@ -1126,9 +1156,23 @@ pub struct AssistantMessageToolRequest { #[serde(rename_all = "camelCase")] pub struct AssistantMessageData { /// Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(default)] pub anthropic_advisor_blocks: Vec, /// Anthropic advisor model ID used for this response, for timeline display on replay + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub anthropic_advisor_model: Option, /// The assistant's text response content @@ -1223,7 +1267,7 @@ pub struct AssistantUsageCopilotUsageTokenDetail { /// Per-request cost and usage data from the CAPI copilot_usage response field #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AssistantUsageCopilotUsage { +pub(crate) struct AssistantUsageCopilotUsage { /// Itemized token usage breakdown pub token_details: Vec, /// Total cost in nano-AI units for this request @@ -1233,24 +1277,32 @@ pub struct AssistantUsageCopilotUsage { /// Schema for the `AssistantUsageQuotaSnapshot` type. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AssistantUsageQuotaSnapshot { +pub(crate) struct AssistantUsageQuotaSnapshot { /// Total requests allowed by the entitlement - pub entitlement_requests: i64, + #[doc(hidden)] + pub(crate) entitlement_requests: i64, /// Whether the user has an unlimited usage entitlement - pub is_unlimited_entitlement: bool, + #[doc(hidden)] + pub(crate) is_unlimited_entitlement: bool, /// Number of additional usage requests made this period - pub overage: f64, + #[doc(hidden)] + pub(crate) overage: f64, /// Whether additional usage is allowed when quota is exhausted - pub overage_allowed_with_exhausted_quota: bool, + #[doc(hidden)] + pub(crate) overage_allowed_with_exhausted_quota: bool, /// Percentage of quota remaining (0 to 100) - pub remaining_percentage: f64, + #[doc(hidden)] + pub(crate) remaining_percentage: f64, /// Date when the quota resets + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub reset_date: Option, + pub(crate) reset_date: Option, /// Whether usage is still permitted after quota exhaustion - pub usage_allowed_with_exhausted_quota: bool, + #[doc(hidden)] + pub(crate) usage_allowed_with_exhausted_quota: bool, /// Number of requests already consumed - pub used_requests: i64, + #[doc(hidden)] + pub(crate) used_requests: i64, } /// Session event "assistant.usage". LLM API call usage metrics including tokens, costs, quotas, and billing information @@ -1270,9 +1322,17 @@ pub struct AssistantUsageData { #[serde(skip_serializing_if = "Option::is_none")] pub cache_write_tokens: Option, /// Per-request cost and usage data from the CAPI copilot_usage response field + #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] - pub copilot_usage: Option, + pub(crate) copilot_usage: Option, /// Model multiplier cost for billing purposes + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(skip_serializing_if = "Option::is_none")] pub cost: Option, /// Duration of the API call in milliseconds @@ -1301,8 +1361,9 @@ pub struct AssistantUsageData { #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, /// Per-quota resource usage snapshots, keyed by quota identifier + #[doc(hidden)] #[serde(default)] - pub quota_snapshots: HashMap, + pub(crate) quota_snapshots: HashMap, /// Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index f35c0a52b..f5dd2a8f7 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -26,6 +26,7 @@ import { collectRpcMethodReferencedDefinitionNames, findSharedSchemaDefinitions, postProcessSchema, + propagateInternalVisibility, resolveRef, resolveObjectSchema, resolveSchema, @@ -36,6 +37,8 @@ import { isNodeFullyDeprecated, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, + isOpaqueJson, isObjectSchema, isVoidSchema, getNullableInner, @@ -275,9 +278,30 @@ function isNonNullableCSharpValueType(typeName: string): boolean { "long", "DateTimeOffset", "TimeSpan", + "JsonElement", ].includes(typeName) || generatedEnums.has(typeName) || emittedRpcEnumResultTypes.has(typeName) || externalRpcValueTypes.has(typeName); } +/** + * Schemas marked `.asOpaqueJson()` on the runtime side carry + * `x-opaque-json: true`. These are the only shapes that legitimately surface + * as opaque JSON in the SDK (mapped to `JsonElement` in C#). Anything else + * that lacks an idiomatic mapping (untyped fields, non-discriminated unions, + * etc.) is rejected by the runtime's schema-shape lint, so the codegen + * treats reaching an unmappable schema here as a bug. + * + * The predicate itself lives in {@link "./utils".isOpaqueJson} for reuse. + */ +function failUnmappable(context: string, schema: JSONSchema7): never { + const summary = JSON.stringify(schema, (key, value) => (key === "description" ? undefined : value)).slice(0, 200); + throw new Error( + `C# codegen: cannot map schema to an idiomatic C# type (${context}). ` + + `On the runtime side, either tighten the Zod schema to a typed shape, or — if it is genuinely free-form JSON — ` + + `mark it \`.asOpaqueJson()\` so the schema emits \`x-opaque-json: true\` and the codegen maps it to JsonElement. ` + + `Offending schema (truncated): ${summary}`, + ); +} + function requiresArgumentNullCheck(typeName: string, isRequired: boolean): boolean { return isRequired && !typeName.endsWith("?") && !isNonNullableCSharpValueType(typeName); } @@ -309,6 +333,9 @@ function localRequestVariableName(paramEntries: [string, JSONSchema7Definition][ } function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: Map, propName?: string): string { + if (isOpaqueJson(schema)) { + return required ? "JsonElement" : "JsonElement?"; + } const nullableInner = getNullableInner(schema); if (nullableInner) { // Pass required=true to get the base type, then add "?" for nullable @@ -361,7 +388,8 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: if (type === "boolean") return required ? "bool" : "bool?"; if (type === "array") { const items = schema.items as JSONSchema7 | undefined; - const itemType = items ? schemaTypeToCSharp(items, true, knownTypes) : "object"; + if (!items) failUnmappable(`array without items (propName=${propName ?? "?"})`, schema); + const itemType = schemaTypeToCSharp(items, true, knownTypes); return required ? `${itemType}[]` : `${itemType}[]?`; } if (type === "object") { @@ -369,9 +397,9 @@ function schemaTypeToCSharp(schema: JSONSchema7, required: boolean, knownTypes: const valueType = schemaTypeToCSharp(schema.additionalProperties as JSONSchema7, true, knownTypes); return required ? `IDictionary` : `IDictionary?`; } - return required ? "object" : "object?"; + failUnmappable(`object without properties or typed additionalProperties (propName=${propName ?? "?"})`, schema); } - return required ? "object" : "object?"; + failUnmappable(`unknown/missing type (propName=${propName ?? "?"})`, schema); } /** Tracks whether any TimeSpan property was emitted so the converter can be generated. */ @@ -489,6 +517,20 @@ function pushObsoleteAttributes(lines: string[], indent = ""): void { lines.push(...obsoleteAttributes(indent)); } +/** + * Emit the `[JsonInclude]` attribute for an internally-marked property and + * return the C# access modifier to use for the property declaration. + * + * `[JsonInclude]` is required because System.Text.Json only auto-(de)serialises + * public members by default; without it, the `internal` setter would silently + * be skipped. + */ +function pushCSharpInternalAttribute(lines: string[], schema: JSONSchema7, indent = " "): "public" | "internal" { + const propInternal = isSchemaInternal(schema); + if (propInternal) lines.push(`${indent}[JsonInclude]`); + return propInternal ? "internal" : "public"; +} + // ══════════════════════════════════════════════════════════════════════════════ // SESSION EVENTS // ══════════════════════════════════════════════════════════════════════════════ @@ -747,11 +789,13 @@ function generateFlattenedBooleanDiscriminatedClass( lines.push(...xmlDocPropertyComment(info.schema.description, propName, " ")); lines.push(...emitDataAnnotations(info.schema, " ", csharpType)); if (isSchemaDeprecated(info.schema)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(info.schema)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, info.schema)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, info.schema); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`); } lines.push(`}`); @@ -850,11 +894,13 @@ function generateDerivedClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); } } @@ -917,11 +963,11 @@ function getJsonUnionMatchExpression(variant: JsonUnionVariant, variants: JsonUn ].join(" && "); } -function generateJsonUnionClass(className: string, variants: JsonUnionVariant[], description: string | undefined, jsonContextType: string): string { +function generateJsonUnionClass(className: string, variants: JsonUnionVariant[], description: string | undefined, jsonContextType: string, isInternal: boolean): string { const lines: string[] = []; lines.push(...xmlDocCommentWithFallback(description, `JSON union data type for ${escapeXml(className)}.`, "")); lines.push(`[JsonConverter(typeof(Converter))]`); - lines.push(`public sealed partial class ${className}`); + lines.push(`${isInternal ? "internal" : "public"} sealed partial class ${className}`); lines.push(`{`); for (const variant of variants) { @@ -1047,7 +1093,7 @@ function tryGenerateSessionJsonUnionType( }); } - nestedClasses.set(className, generateJsonUnionClass(className, variants, schema.description, "SessionEventsJsonContext")); + nestedClasses.set(className, generateJsonUnionClass(className, variants, schema.description, "SessionEventsJsonContext", isSchemaInternal(schema))); return className; } @@ -1063,7 +1109,7 @@ function generateNestedClass( lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, "")); if (isSchemaExperimental(schema)) pushExperimentalAttribute(lines); if (isSchemaDeprecated(schema)) pushObsoleteAttributes(lines); - lines.push(`public sealed partial class ${className}`, `{`); + lines.push(`${isSchemaInternal(schema) ? "internal" : "public"} sealed partial class ${className}`, `{`); for (const [propName, propSchema] of Object.entries(schema.properties || {}).sort(([a], [b]) => a.localeCompare(b))) { if (typeof propSchema !== "object") continue; @@ -1075,11 +1121,13 @@ function generateNestedClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; - lines.push(` public ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); + lines.push(` ${propVisibility} ${reqMod}${csharpType} ${csharpName} { get; set; }`, ""); } if (lines[lines.length - 1] === "") lines.pop(); lines.push(`}`); @@ -1095,6 +1143,9 @@ function resolveSessionPropertyType( nestedClasses: Map, enumOutput: string[] ): string { + if (isOpaqueJson(propSchema)) { + return isRequired ? "JsonElement" : "JsonElement?"; + } // Handle $ref by resolving against schema definitions if (propSchema.$ref) { const className = typeToClassName(refTypeName(propSchema.$ref, sessionDefinitions)); @@ -1145,12 +1196,12 @@ function resolveSessionPropertyType( } const unionType = tryGenerateSessionJsonUnionType(propSchema, parentClassName, propName, knownTypes, nestedClasses, enumOutput); if (unionType) return isRequired ? unionType : `${unionType}?`; - return !isRequired ? "object?" : "object"; + failUnmappable(`anyOf without discriminator (${parentClassName}.${propName})`, propSchema); } if (propSchema.oneOf) { const unionType = tryGenerateSessionJsonUnionType(propSchema, parentClassName, propName, knownTypes, nestedClasses, enumOutput); if (unionType) return isRequired ? unionType : `${unionType}?`; - return !isRequired ? "object?" : "object"; + failUnmappable(`oneOf without discriminator (${parentClassName}.${propName})`, propSchema); } if (propSchema.enum && Array.isArray(propSchema.enum)) { const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); @@ -1191,7 +1242,8 @@ function resolveSessionPropertyType( } function generateDataClass(variant: EventVariant, knownTypes: Map, nestedClasses: Map, enumOutput: string[]): string { - if (!variant.dataSchema?.properties) return `public sealed partial class ${variant.dataClassName} { }`; + const dataVisibility = isSchemaInternal(variant.dataSchema) ? "internal" : "public"; + if (!variant.dataSchema?.properties) return `${dataVisibility} sealed partial class ${variant.dataClassName} { }`; const required = new Set(variant.dataSchema.required || []); const lines: string[] = []; @@ -1206,7 +1258,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map a.localeCompare(b))) { if (typeof propSchema !== "object") continue; @@ -1218,11 +1270,13 @@ function generateDataClass(variant: EventVariant, knownTypes: Map`); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); lines.push(` /// The ${escapeXml(variant.typeName)} event payload.`); - lines.push(` [JsonPropertyName("data")]`, ` public required ${variant.dataClassName} Data { get; set; }`, `}`, ""); + lines.push(` [JsonPropertyName("data")]`, ` ${variantVisibility} required ${variant.dataClassName} Data { get; set; }`, `}`, ""); } // Data classes @@ -1352,7 +1409,7 @@ export async function generateSessionEvents(schemaPath?: string): Promise console.log("C#: generating session-events..."); const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7); - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const code = generateSessionEventsCode(processed); const outPath = await writeGeneratedFile("dotnet/src/Generated/SessionEvents.cs", code); console.log(` ✓ ${outPath}`); @@ -1603,14 +1660,15 @@ function emitRpcClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); if (isMillisecondsDurationProperty(propName, prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); + const propVisibility = pushCSharpInternalAttribute(lines, prop); lines.push(` [JsonPropertyName("${propName}")]`); let defaultVal = ""; let propAccessors = "{ get; set; }"; if (isReq && !csharpType.endsWith("?")) { if (csharpType === "string") defaultVal = " = string.Empty;"; - else if (csharpType === "object") defaultVal = " = null!;"; else if (csharpType.startsWith("IList<")) { propAccessors = "{ get => field ??= []; set; }"; } else if (csharpType.startsWith("IDictionary<")) { @@ -1622,7 +1680,7 @@ function emitRpcClass( defaultVal = " = null!;"; } } - lines.push(` public ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); + lines.push(` ${propVisibility} ${csharpType} ${csharpName} ${propAccessors}${defaultVal}`); if (i < props.length - 1) lines.push(""); } lines.push(`}`); @@ -1799,12 +1857,24 @@ function emitServerInstanceMethod( const csharpName = requestClassName ? toCSharpPropertyName(pName, jsonSchema) : toPascalCase(pName); - const csType = requestClassName + const naturalType = requestClassName ? resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes) : schemaTypeToCSharp(jsonSchema, isReq, rpcKnownTypes, csharpName); + // Boundary special-case: if the natural type is JsonElement/JsonElement? (i.e. the + // schema is opaque-JSON), accept object at the public surface for ergonomics and + // convert at the call site. DTO fields keep JsonElement/JsonElement?. + const opaqueRequired = naturalType === "JsonElement"; + const opaqueOptional = naturalType === "JsonElement?"; + const opaque = opaqueRequired || opaqueOptional; + const csType = opaqueRequired ? "object" : opaqueOptional ? "object?" : naturalType; sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${csharpName} = ${pName}`); - if (requiresArgumentNullCheck(csType, isReq)) { + const assignedValue = opaqueRequired + ? `CopilotClient.ToJsonElementForWire(${pName})!.Value` + : opaqueOptional + ? `CopilotClient.ToJsonElementForWire(${pName})` + : pName; + bodyAssignments.push(`${csharpName} = ${assignedValue}`); + if (opaqueRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } parameterDescriptions.push({ name: pName, description: jsonSchema.description }); @@ -1956,14 +2026,24 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas for (const [pName, pSchema] of paramEntries) { if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); - const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7); - const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, csharpName, classes); + const jsonSchema = pSchema as JSONSchema7; + const csharpName = toCSharpPropertyName(pName, jsonSchema); + const naturalType = resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes); + const opaqueRequired = naturalType === "JsonElement"; + const opaqueOptional = naturalType === "JsonElement?"; + const opaque = opaqueRequired || opaqueOptional; + const csType = opaqueRequired ? "object" : opaqueOptional ? "object?" : naturalType; sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${csharpName} = ${pName}`); - if (requiresArgumentNullCheck(csType, isReq)) { + const assignedValue = opaqueRequired + ? `CopilotClient.ToJsonElementForWire(${pName})!.Value` + : opaqueOptional + ? `CopilotClient.ToJsonElementForWire(${pName})` + : pName; + bodyAssignments.push(`${csharpName} = ${assignedValue}`); + if (opaqueRequired || (!opaque && requiresArgumentNullCheck(csType, isReq))) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } - parameterDescriptions.push({ name: pName, description: (pSchema as JSONSchema7).description }); + parameterDescriptions.push({ name: pName, description: jsonSchema.description }); } } sigParams.push("CancellationToken cancellationToken = default"); @@ -2334,7 +2414,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro await generateSessionEvents(sessionSchemaPath); try { const resolvedSessionPath = sessionSchemaPath ?? (await getSessionEventsSchemaPath()); - const sessionSchema = postProcessSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7)); + const sessionSchema = propagateInternalVisibility(postProcessSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7))); await generateRpc(apiSchemaPath, sessionSchema); } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT" && !apiSchemaPath) { diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 6723906d4..cab21ebba 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -37,9 +37,11 @@ import { isRpcMethod, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, isVoidSchema, parseExternalSchemaRef, postProcessSchema, + propagateInternalVisibility, refTypeName, resolveObjectSchema, resolveRef, @@ -201,6 +203,31 @@ function pushGoExperimentalMethodComment(lines: string[], methodName: string, in pushGoComment(lines, `Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`, indent); } +function pushGoInternalPropertyComment(lines: string[], goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + pushGoCommentForContext(lines, `Internal: ${goName} is part of the SDK's internal API surface and is not intended for external use.`, ctx, indent); +} + +function pushGoExperimentalPropertyComment(lines: string[], goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + pushGoCommentForContext(lines, `Experimental: ${goName} is part of an experimental API and may change or be removed.`, ctx, indent); +} + +/** + * Emit `Deprecated:` / `Experimental:` / `Internal:` doc comments above a Go + * struct field. Centralises the per-field marker logic shared between the + * regular struct emitter and the discriminated-union variant emitters. + */ +function pushGoFieldMarkers(lines: string[], prop: JSONSchema7, goName: string, ctx: GoCodegenCtx, indent = "\t"): void { + if (isSchemaDeprecated(prop)) { + pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, indent); + } + if (isSchemaExperimental(prop)) { + pushGoExperimentalPropertyComment(lines, goName, ctx, indent); + } + if (isSchemaInternal(prop)) { + pushGoInternalPropertyComment(lines, goName, ctx, indent); + } +} + function lowerFirst(value: string): string { if (value.length === 0) return value; return value.charAt(0).toLowerCase() + value.slice(1); @@ -1111,9 +1138,7 @@ function emitGoStruct( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -1802,9 +1827,7 @@ function emitGoFlatDiscriminatedUnion( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -1931,9 +1954,7 @@ function emitGoRequiredFieldDiscriminatedUnion( if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -3043,9 +3064,7 @@ export function generateGoSessionEventsCode(schema: JSONSchema7, packageName: st if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } - if (isSchemaDeprecated(prop)) { - pushGoCommentForContext(lines, `Deprecated: ${goName} is deprecated.`, ctx, "\t"); - } + pushGoFieldMarkers(lines, prop, goName, ctx); const jsonTag = `json:"${propName}${omit}"`; lines.push(`\t${goName} ${goType} \`${jsonTag}\``); fields.push({ propName, goName, goType, jsonTag }); @@ -3337,11 +3356,15 @@ function collectGoTopLevelNames(code: string, keyword: "type" | "const"): string function generateGoSessionEventAliasFile( generatedSessionTypeCode: string, additionalTypeNames: Iterable = [], - additionalConstNames: Iterable = [] + additionalConstNames: Iterable = [], + excludeTypeNames: Iterable = [] ): string { + const excluded = new Set(excludeTypeNames); const typeNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "type"), ...additionalTypeNames])] + .filter((name) => !excluded.has(name)) .sort(compareGoTypeNames); const constNames = [...new Set([...collectGoTopLevelNames(generatedSessionTypeCode, "const"), ...additionalConstNames])] + .filter((name) => !excluded.has(name)) .sort(compareGoTypeNames); const lines: string[] = []; @@ -3428,7 +3451,7 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7); - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const sharedDefinitions = apiSchema ? findSharedSchemaDefinitions( processed as unknown as Record, @@ -3460,9 +3483,24 @@ async function generateSessionEvents(schemaPath?: string, apiSchema?: ApiSchema) const sharedAliasNames = apiSchema ? collectGoSharedSessionEventAliasNames(sharedSessionEventDefinitions, apiSchema) : { typeNames: [], constNames: [] }; + // Exclude internal types from the public `copilot` package re-exports. They + // remain accessible in the lower-level `rpc` package (where they're tagged + // with `// Internal:` doc comments), but consumers using only the canonical + // `copilot.*` namespace never see them. This is the strongest practical + // signal Go offers without requiring runtime refactoring to enable full + // lowercase/unexported types. + const internalTypesInSession = new Set(); + { + const { definitions, $defs } = collectDefinitionCollections(sessionSchema as Record); + for (const [name, def] of Object.entries({ ...definitions, ...$defs })) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalTypesInSession.add(name); + } + } + } const aliasOutPath = await writeGeneratedFile( "go/zsession_events.go", - generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames) + generateGoSessionEventAliasFile(generatedTypeCode, sharedAliasNames.typeNames, sharedAliasNames.constNames, internalTypesInSession) ); console.log(` ✓ ${aliasOutPath}`); @@ -3476,7 +3514,7 @@ async function generateRpc(schemaPath?: string): Promise { console.log("Go: generating RPC types..."); const resolvedPath = schemaPath ?? (await getApiSchemaPath()); - const schema = fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as ApiSchema)); + const schema = propagateInternalVisibility(fixNullableRequiredRefsInApiSchema(cloneSchemaForCodegen(JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as ApiSchema)) as JSONSchema7) as unknown as ApiSchema; const allMethods = [ ...collectRpcMethods(schema.server || {}), diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index ff7c16f93..b173ddec8 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -795,7 +795,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index eae6f09b4..7e7dc63b8 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -8,7 +8,7 @@ import fs from "fs/promises"; import path from "path"; -import type { JSONSchema7 } from "json-schema"; +import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { fileURLToPath } from "url"; import { cloneSchemaForCodegen, @@ -25,7 +25,13 @@ import { isNodeFullyDeprecated, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, postProcessSchema, + propagateInternalVisibility, + collectInternalSymbols, + collectInternalFieldsOnPublicTypes, + annotateInternalPythonFields, + renameInternalPythonSymbols, stripBooleanLiterals, writeGeneratedFile, collectDefinitionCollections, @@ -677,6 +683,24 @@ function pushPyExperimentalApiGroupComment(lines: string[]): void { lines.push("# Experimental: this API group is experimental and may change or be removed."); } +/** + * Emit `# Deprecated:` / `# Experimental:` / `# Internal:` comments above a + * dataclass field. Order matches our other codegens (deprecated, experimental, + * internal) and keeps the comments out of the field declaration itself. + */ +function pushPyFieldMarkers(lines: string[], propSchema: JSONSchema7 | null | undefined): void { + if (!propSchema) return; + if (isSchemaDeprecated(propSchema)) { + lines.push(` # Deprecated: this field is deprecated.`); + } + if (isSchemaExperimental(propSchema)) { + lines.push(` # Experimental: this field is part of an experimental API and may change or be removed.`); + } + if (isSchemaInternal(propSchema)) { + lines.push(` # Internal: this field is an internal SDK API and is not part of the public surface.`); + } +} + /** * Modernize quicktype's Python 3.7 output to Python 3.11+ syntax: * - Optional[T] → T | None @@ -2057,9 +2081,11 @@ function emitPyClass( const fieldInfos = orderedFieldEntries.map(([propName, propSchema]) => { const isRequired = required.has(propName); const resolved = resolvePyPropertyType(propSchema, typeName, propName, isRequired, ctx); + const baseFieldName = toPyFieldName(propName, propSchema, ctx); + const fieldName = isSchemaInternal(propSchema) ? `_${baseFieldName}` : baseFieldName; return { jsonName: propName, - fieldName: toPyFieldName(propName, propSchema, ctx), + fieldName, isRequired, resolved, }; @@ -2092,9 +2118,8 @@ function emitPyClass( for (const field of fieldInfos) { const suffix = field.isRequired ? "" : " = None"; - if (isSchemaDeprecated(orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7)) { - lines.push(` # Deprecated: this field is deprecated.`); - } + const propSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7 | undefined; + pushPyFieldMarkers(lines, propSchema); lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); } @@ -2217,7 +2242,7 @@ function emitPyFlatDiscriminatedUnion( return { jsonName: propName, - fieldName: toPyFieldName(propName, propSchema, ctx), + fieldName: isSchemaInternal(propSchema) ? `_${toPyFieldName(propName, propSchema, ctx)}` : toPyFieldName(propName, propSchema, ctx), isRequired: requiredInAll, resolved, }; @@ -2234,10 +2259,8 @@ function emitPyFlatDiscriminatedUnion( } for (const field of fieldInfos) { const suffix = field.isRequired ? "" : " = None"; - const fieldSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1]; - if (fieldSchema && isSchemaDeprecated(fieldSchema)) { - lines.push(` # Deprecated: this field is deprecated.`); - } + const fieldSchema = orderedFieldEntries.find(([n]) => n === field.jsonName)?.[1] as JSONSchema7 | undefined; + pushPyFieldMarkers(lines, fieldSchema); lines.push(` ${field.fieldName}: ${field.resolved.annotation}${suffix}`); } lines.push(``); @@ -2624,8 +2647,10 @@ async function generateSessionEvents(schemaPath?: string): Promise { const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; - const processed = postProcessSchema(schema); - const code = generatePythonSessionEventsCode(processed); + const processed = propagateInternalVisibility(postProcessSchema(schema)); + let code = generatePythonSessionEventsCode(processed); + const { typeNames } = collectInternalSymbols(processed); + code = renameInternalPythonSymbols(code, typeNames); const outPath = await writeGeneratedFile("python/copilot/generated/session_events.py", code); console.log(` ✓ ${outPath}`); @@ -3017,6 +3042,44 @@ def _patch_model_capabilities(data: dict) -> dict: finalCode = postProcessDiscriminatorDefaultsForPython(finalCode, refBasedUnions); finalCode = unwrapRedundantPythonLambdas(finalCode); + // Apply `_`-prefix to type names of internal RPC types so the leading-underscore + // Python convention signals "internal, no stability guarantees" to consumers. + { + const internalDefs = new Set(); + for (const [name, def] of Object.entries(rpcDefinitions.definitions)) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalDefs.add(name); + } + } + for (const [name, def] of Object.entries(rpcDefinitions.$defs)) { + if (def && typeof def === "object" && (def as Record).visibility === "internal") { + internalDefs.add(name); + } + } + if (internalDefs.size > 0) { + finalCode = renameInternalPythonSymbols(finalCode, internalDefs); + } + } + + // Annotate internal fields on otherwise-public RPC types with a `# Internal:` + // comment immediately above the field declaration. Quicktype's generated + // from_dict/to_dict reference field names in patterns that are brittle to + // regex-based identifier rewriting, so we annotate rather than rename. The + // marker is visible in IDE hovers and signals "internal, no stability + // guarantee" without breaking the wire-protocol round-trip. + { + const combinedSchema: JSONSchema7 = { + definitions: { + ...(rpcDefinitions.definitions as Record), + ...(rpcDefinitions.$defs as Record), + }, + }; + const fieldsByType = collectInternalFieldsOnPublicTypes(combinedSchema); + if (fieldsByType.size > 0) { + finalCode = annotateInternalPythonFields(finalCode, fieldsByType, toSnakeCase); + } + } + const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", finalCode); console.log(` ✓ ${outPath}`); } @@ -3141,7 +3204,8 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio } function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false, groupDeprecated = false): void { - const methodName = toSnakeCase(name); + const isInternal = method.visibility === "internal"; + const methodName = (isInternal ? "_" : "") + toSnakeCase(name); const resultSchema = getMethodResultSchema(method); const nullableInner = resultSchema ? getNullableInner(resultSchema) : undefined; const effectiveResultSchema = nullableInner ?? resultSchema; diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index f2056c35c..db3c7792c 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -38,9 +38,11 @@ import { isRpcMethod, isSchemaDeprecated, isSchemaExperimental, + isSchemaInternal, isVoidSchema, parseExternalSchemaRef, postProcessSchema, + propagateInternalVisibility, refTypeName, resolveObjectSchema, resolveRef, @@ -776,6 +778,7 @@ function emitRustStruct( if (isSchemaDeprecated(schema)) { lines.push(...rustDeprecatedAttributes()); } + const structVis = isSchemaInternal(schema) ? "pub(crate)" : "pub"; // Resolve field types up-front so we can decide whether `Default` can be // derived. A required field whose bare type is non-default-able (e.g. an @@ -810,7 +813,7 @@ function emitRustStruct( lines.push("#[derive(Debug, Clone, Default, Serialize, Deserialize)]"); } lines.push(`#[serde(rename_all = "camelCase")]`); - lines.push(`pub struct ${typeName} {`); + lines.push(`${structVis} struct ${typeName} {`); for (const { propName, prop, isReq, rustField, rustType } of fields) { if (prop.description) { @@ -818,6 +821,11 @@ function emitRustStruct( lines.push(` /// ${line}`); } } + pushRustExperimentalDocs(lines, isSchemaExperimental(prop), " "); + const propIsInternal = isSchemaInternal(prop); + if (propIsInternal) { + lines.push(` #[doc(hidden)]`); + } if (isSchemaDeprecated(prop)) { lines.push(...rustDeprecatedAttributes(" ")); } @@ -846,7 +854,7 @@ function emitRustStruct( lines.push(` #[serde(rename = "${propName}")]`); } - lines.push(` pub ${rustField}: ${rustType},`); + lines.push(` ${propIsInternal ? "pub(crate)" : "pub"} ${rustField}: ${rustType},`); } lines.push("}"); @@ -1781,17 +1789,18 @@ function emitNamespaceMethod( }; const paramArg = hasParams ? `, params: ${paramsTypeName}` : ""; + const fnVis = method.visibility === "internal" ? "pub(crate)" : "pub"; if (hasParams && paramsInfo.optional) { out.push(...buildDocs(false)); out.push( - ` pub async fn ${fnName}(&self) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}(&self) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, false, resultIsVoid); out.push(""); out.push(...buildDocs(true)); out.push( - ` pub async fn ${fnName}_with_params(&self, params: ${paramsTypeName}) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}_with_params(&self, params: ${paramsTypeName}) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, true, resultIsVoid); out.push(""); @@ -1800,7 +1809,7 @@ function emitNamespaceMethod( out.push(...buildDocs(hasParams)); out.push( - ` pub async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, + ` ${fnVis} async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, ); pushNamespaceMethodBody(out, constName, isSession, hasParams, resultIsVoid); out.push(""); @@ -1988,11 +1997,15 @@ async function generate(): Promise { await fs.readFile(apiSchemaPath, "utf-8"), ) as ApiSchema; - const sessionEventsSchema = postProcessSchema( - stripBooleanLiterals(sessionEventsRaw) as JSONSchema7, + const sessionEventsSchema = propagateInternalVisibility( + postProcessSchema( + stripBooleanLiterals(sessionEventsRaw) as JSONSchema7, + ), ); - const apiSchema = postProcessSchema( - stripBooleanLiterals(apiRaw) as JSONSchema7, + const apiSchema = propagateInternalVisibility( + postProcessSchema( + stripBooleanLiterals(apiRaw) as JSONSchema7, + ), ) as unknown as ApiSchema; // Ensure output directory exists diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 3afaec395..bd92719ee 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -18,6 +18,7 @@ import { getRpcSchemaTypeName, getSessionEventsSchemaPath, postProcessSchema, + propagateInternalVisibility, writeGeneratedFile, collectExternalSchemaRefNames, collectDefinitionCollections, @@ -37,6 +38,7 @@ import { isVoidSchema, isSchemaExperimental, getEnumValueDescriptions, + stripOpaqueJsonMarker, type ApiSchema, type DefinitionCollections, type RpcMethod, @@ -280,6 +282,13 @@ export function normalizeSchemaForTypeScript(schema: JSONSchema7): JSONSchema7 { Object.entries(value as Record).map(([key, child]) => [key, rewrite(child)]) ) as Record; + // The TypeScript codegen doesn't distinguish opaque JSON from any + // other unconstrained value, so drop the marker before feeding the + // schema to json-schema-to-typescript. C# codegen reads the marker + // from its own (un-normalized) view of the schema and emits + // `JsonElement` instead. + stripOpaqueJsonMarker(rewritten); + const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7); if (enumValueDescriptions && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) { rewritten.tsType = (rewritten.enum as string[]) @@ -330,7 +339,7 @@ async function generateSessionEvents(schemaPath?: string): Promise { const resolvedPath = schemaPath ?? (await getSessionEventsSchemaPath()); const schema = JSON.parse(await fs.readFile(resolvedPath, "utf-8")) as JSONSchema7; - const processed = postProcessSchema(schema); + const processed = propagateInternalVisibility(postProcessSchema(schema)); const definitionCollections = collectDefinitionCollections(processed as Record); const sessionEvent = resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ?? @@ -895,7 +904,7 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro await generateSessionEvents(sessionSchemaPath); try { const resolvedSessionPath = sessionSchemaPath ?? (await getSessionEventsSchemaPath()); - const sessionSchema = postProcessSchema(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7); + const sessionSchema = propagateInternalVisibility(postProcessSchema(JSON.parse(await fs.readFile(resolvedSessionPath, "utf-8")) as JSONSchema7)); await generateRpc(apiSchemaPath, sessionSchema); } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT" && !apiSchemaPath) { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index a06be7607..0bebae13c 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -501,6 +501,335 @@ export function isSchemaExperimental(schema: JSONSchema7 | null | undefined): bo return typeof schema === "object" && schema !== null && (schema as Record).stability === "experimental"; } +/** Returns true when a JSON Schema node is marked as visibility:"internal" (set via `.asInternal()` on the Zod source). */ +export function isSchemaInternal(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && schema !== null && (schema as Record).visibility === "internal"; +} + +/** + * Collects the set of definition names marked `visibility: "internal"` and a + * per-definition set of internal property names. Used by code generators that + * need to apply `_`-prefix or similar renames consistently across both type + * declarations and references. + * + * Call after `propagateInternalVisibility` so transitively-internal fields are + * also picked up. + */ +export function collectInternalSymbols(schema: JSONSchema7): { + typeNames: Set; + fieldsByType: Map>; +} { + const typeNames = new Set(); + const fieldsByType = new Map>(); + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + for (const [name, def] of Object.entries(allDefs)) { + if (!def || typeof def !== "object") continue; + const d = def as Record; + if (d.visibility === "internal") typeNames.add(name); + const props = d.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const [propName, propSchema] of Object.entries(props as Record)) { + if (propSchema && typeof propSchema === "object" && (propSchema as Record).visibility === "internal") { + if (!fieldsByType.has(name)) fieldsByType.set(name, new Set()); + fieldsByType.get(name)!.add(propName); + } + } + } + } + return { typeNames, fieldsByType }; +} + +/** + * Post-process a Python module so that types marked `visibility: "internal"` + * carry an underscore prefix on their class identifier. + * + * Why: Python has no compiler-enforced visibility, but the leading-underscore + * convention is universally recognized as "no stability guarantee". Combined + * with `__all__` exclusion at the module level (handled separately), this is + * the strongest "internal" signal Python idioms provide and matches the + * cross-language bar of "we can do breaking changes on these without + * having to apologize". + * + * Field-level visibility is expected to be handled at emission time by each + * Python emitter (because field names depend on the emitter's PEP 8 normalization + * and the emitter's class-name conventions may diverge from the schema's + * definition names, breaking any single-class regex). Type-level renaming is + * safe to do globally because schema definition names match the emitted class + * identifiers for the types that carry `visibility: "internal"`. + */ +export function renameInternalPythonSymbols( + code: string, + typeNames: Iterable +): string { + const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + let result = code; + const sortedTypes = [...typeNames].sort((a, b) => b.length - a.length); + // Phase 1: rename each identifier globally at word boundaries. + for (const t of sortedTypes) { + result = result.replace( + new RegExp(`(?> { + const out = new Map>(); + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + for (const [name, def] of Object.entries(allDefs)) { + if (!def || typeof def !== "object") continue; + const d = def as Record; + if (d.visibility === "internal") continue; + const props = d.properties; + if (!props || typeof props !== "object" || Array.isArray(props)) continue; + for (const [propName, propSchema] of Object.entries(props as Record)) { + if (propSchema && typeof propSchema === "object" && (propSchema as Record).visibility === "internal") { + if (!out.has(name)) out.set(name, new Set()); + out.get(name)!.add(propName); + } + } + } + return out; +} + +/** + * Annotate quicktype-generated Python field declarations whose schema is marked + * `visibility: "internal"` with a `# Internal:` comment immediately above the + * declaration. The comment is visible in IDE hovers/code completion, so + * consumers see the marker even though the identifier itself is unchanged. + * + * This is the field-level fallback for code paths that can't rename the field + * identifier (quicktype's generated `from_dict`/`to_dict` reference field names + * in patterns brittle to regex rewriting). For session-events and other + * hand-rolled emitters, prefer renaming. + * + * The `toFieldName` callback maps a JSON property name to its Python attribute + * name (typically snake_case). + */ +export function annotateInternalPythonFields( + code: string, + fieldsByType: Map>, + toFieldName: (jsonName: string) => string +): string { + const escapeRegex = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + let result = code; + for (const [typeName, fields] of fieldsByType) { + // Match the class body up to the next top-level statement. quicktype's + // generated classes are separated by blank-line boundaries. + const classRe = new RegExp( + `(@dataclass\\nclass ${escapeRegex(typeName)}[:(][^]*?)(?=\\n(?:@dataclass\\n)?class \\w|\\n\\nclass |\\n[A-Za-z_]\\w* =|$)`, + "g" + ); + result = result.replace(classRe, (block) => { + for (const jsonField of fields) { + const pyField = toFieldName(jsonField); + const escaped = escapeRegex(pyField); + // Match ` fieldName: type` style declarations (PEP 526). Avoid + // double-annotating if the comment is already present immediately above. + block = block.replace( + new RegExp(`(^(?! # Internal:.*$)(?:.*\\n)?)( )${escaped}(?=\\s*:)`, "gm"), + (_match, prefix, indent) => { + // Avoid duplicate annotation if the previous line is already an Internal: marker. + if (/ # Internal:/.test(prefix)) return `${prefix}${indent}${pyField}`; + return `${prefix}${indent}# Internal: this field is an internal SDK API and is not part of the public surface.\n${indent}${pyField}`; + } + ); + } + return block; + }); + } + return result; +} + +/** + * Walks a top-level JSON Schema and marks any property whose referenced type + * resolves to an internal definition as `visibility: "internal"` itself. + * + * Schemas can be authored with an internal-typed reference on a property that + * isn't itself explicitly marked internal (e.g. `copilotUsage` referencing + * `AssistantUsageCopilotUsage`). Code generators that map `visibility: + * "internal"` to hard language-level visibility (C# `internal`, Rust + * `pub(crate)`) would otherwise produce inconsistent-accessibility errors + * (CS0053 in C#, E0446 in Rust). This pass closes that gap by promoting + * referencing properties to internal — matching the language compilers' + * own transitivity rule. + * + * Only references that resolve directly, through arrays, or through dictionary + * `additionalProperties` are considered. References that flow only through a + * `oneOf`/`anyOf` of public+internal variants are left alone (the union itself + * is the carrier of visibility there). + * + * Mutates `schema` in place and returns it. Idempotent. + */ +export function propagateInternalVisibility(schema: JSONSchema7): JSONSchema7 { + if (typeof schema !== "object" || schema === null) return schema; + + const { definitions, $defs } = collectDefinitionCollections(schema as Record); + const allDefs: Record = { ...definitions, ...$defs }; + const internalTypeNames = new Set(); + for (const [name, def] of Object.entries(allDefs)) { + if (def && typeof def === "object" && isSchemaInternal(def as JSONSchema7)) { + internalTypeNames.add(name); + } + } + if (internalTypeNames.size === 0) return schema; + + const refToName = (ref: unknown): string | undefined => { + if (typeof ref !== "string") return undefined; + const m = ref.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/); + return m ? m[1] : undefined; + }; + + /** Returns true when a property's *direct* type carrier is an internal definition. */ + const propertyReferencesInternal = (propSchema: JSONSchema7): boolean => { + const direct = refToName((propSchema as Record).$ref); + if (direct && internalTypeNames.has(direct)) return true; + const items = (propSchema as Record).items; + if (items && typeof items === "object" && !Array.isArray(items)) { + const itemsRef = refToName((items as Record).$ref); + if (itemsRef && internalTypeNames.has(itemsRef)) return true; + } + const addl = (propSchema as Record).additionalProperties; + if (addl && typeof addl === "object") { + const addlRef = refToName((addl as Record).$ref); + if (addlRef && internalTypeNames.has(addlRef)) return true; + } + return false; + }; + + const visit = (node: unknown): void => { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + const record = node as Record; + const props = record.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const propSchema of Object.values(props as Record)) { + if (!propSchema || typeof propSchema !== "object") continue; + if (!isSchemaInternal(propSchema as JSONSchema7) && propertyReferencesInternal(propSchema as JSONSchema7)) { + (propSchema as Record).visibility = "internal"; + } + visit(propSchema); + } + } + for (const key of ["items", "additionalProperties", "anyOf", "allOf", "oneOf"]) { + if (record[key]) visit(record[key]); + } + for (const collectionKey of ["definitions", "$defs"]) { + const collection = record[collectionKey]; + if (collection && typeof collection === "object" && !Array.isArray(collection)) { + for (const def of Object.values(collection as Record)) { + if (def && typeof def === "object") visit(def); + } + } + } + }; + + visit(schema); + return schema; +} + +/** + * Returns true when a JSON Schema node is marked `x-opaque-json: true` (set via + * `.asOpaqueJson()` on the Zod source). These are the only shapes that legitimately + * surface as opaque JSON in the SDK; everything else with an underspecified type + * is rejected by the runtime's schema lint pass. + */ +export function isOpaqueJson(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && schema !== null && (schema as Record)["x-opaque-json"] === true; +} + +/** + * Removes the `x-opaque-json` marker from a schema node in place. Useful for + * codegens (e.g. TypeScript) that don't distinguish opaque JSON from any other + * unconstrained value and would otherwise have the marker confuse downstream + * tooling. Codegens that *do* care (e.g. C#, which maps opaque JSON to + * `JsonElement`) should call `isOpaqueJson` *before* this point. + */ +export function stripOpaqueJsonMarker(schema: Record): void { + delete schema["x-opaque-json"]; +} + +/** + * Append `@internal` and/or `@experimental` JSDoc-style tags to the `description` + * of every property that carries `visibility: "internal"` or `stability: "experimental"` + * inline. Used by codegens whose output mechanism (e.g. `json-schema-to-typescript`) + * renders `description` verbatim as JSDoc; downstream tooling then picks the tags + * up automatically. + * + * Mutates `schema` in place and returns it. Callers that don't want their input + * mutated should clone first. + */ +export function appendPropertyMarkerTagsToDescriptions(schema: JSONSchema7): JSONSchema7 { + const seen = new WeakSet(); + const visit = (node: unknown): void => { + if (!node || typeof node !== "object") return; + if (seen.has(node)) return; + seen.add(node); + + if (Array.isArray(node)) { + for (const item of node) visit(item); + return; + } + + const record = node as Record; + const props = record.properties; + if (props && typeof props === "object" && !Array.isArray(props)) { + for (const propSchema of Object.values(props as Record)) { + if (!propSchema || typeof propSchema !== "object") continue; + const tags: string[] = []; + if (isSchemaInternal(propSchema as JSONSchema7)) tags.push("@internal"); + if (isSchemaExperimental(propSchema as JSONSchema7)) tags.push("@experimental"); + if (tags.length === 0) continue; + const propRecord = propSchema as Record; + const existing = typeof propRecord.description === "string" ? propRecord.description : ""; + const suffix = tags.join("\n"); + propRecord.description = existing.length > 0 ? `${existing}\n\n${suffix}` : suffix; + } + } + + for (const value of Object.values(record)) { + if (value && typeof value === "object") visit(value); + } + }; + visit(schema); + return schema; +} + // ── $ref resolution ───────────────────────────────────────────────────────── /** Extract the generated type name from a `$ref` path (e.g. "#/definitions/Model" → "Model"). */