diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index f542307be..393db2d18 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -70,6 +70,8 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false - name: cargo fmt --check (nightly) if: runner.os == 'Linux' @@ -135,6 +137,8 @@ jobs: with: workspaces: "rust" key: bundled-cli + prefix-key: v1-rust-no-bin + cache-bin: false - name: Read pinned @github/copilot CLI version id: cli-version diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index d51dff03e..efed73c1d 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -65,12 +65,36 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// Token-level pricing information for this model. +public sealed class ModelBillingTokenPrices +{ + /// Number of tokens per standard billing batch. + [JsonPropertyName("batchSize")] + public long? BatchSize { get; set; } + + /// Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + [JsonPropertyName("cachePrice")] + public long? CachePrice { get; set; } + + /// Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + [JsonPropertyName("inputPrice")] + public long? InputPrice { get; set; } + + /// Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD). + [JsonPropertyName("outputPrice")] + public long? OutputPrice { get; set; } +} + /// Billing information. public sealed class ModelBilling { /// Billing cost multiplier relative to the base rate. [JsonPropertyName("multiplier")] public double? Multiplier { get; set; } + + /// Token-level pricing information for this model. + [JsonPropertyName("tokenPrices")] + public ModelBillingTokenPrices? TokenPrices { get; set; } } /// Vision-specific limits. @@ -492,6 +516,10 @@ internal sealed class SessionFsSetProviderRequest [Experimental(Diagnostics.Experimental)] public sealed class SessionsForkResult { + /// Friendly name assigned to the forked session, if any. + [JsonPropertyName("name")] + public string? Name { get; set; } + /// The new forked session's ID. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -501,6 +529,10 @@ public sealed class SessionsForkResult [Experimental(Diagnostics.Experimental)] internal sealed class SessionsForkRequest { + /// Optional friendly name to assign to the forked session. + [JsonPropertyName("name")] + public string? Name { get; set; } + /// Source session ID to fork from. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -1494,6 +1526,19 @@ internal sealed class SkillsDisableRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for SkillsLoadDiagnostics operations. +[Experimental(Diagnostics.Experimental)] +public sealed class SkillsLoadDiagnostics +{ + /// Errors emitted while loading skills (e.g. skills that failed to load entirely). + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + /// RPC data type for SessionSkillsReload operations. [Experimental(Diagnostics.Experimental)] internal sealed class SessionSkillsReloadRequest @@ -1765,6 +1810,204 @@ internal sealed class HandlePendingToolCallRequest public string SessionId { get; set; } = string.Empty; } +/// Optional unstructured input hint. +public sealed class SlashCommandInput +{ + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). + [JsonPropertyName("completion")] + public SlashCommandInputCompletion? Completion { get; set; } + + /// Hint to display when command input has not been provided. + [JsonPropertyName("hint")] + public string Hint { get; set; } = string.Empty; + + /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace. + [JsonPropertyName("preserveMultilineInput")] + public bool? PreserveMultilineInput { get; set; } + + /// When true, the command requires non-empty input; clients should render the input hint as required. + [JsonPropertyName("required")] + public bool? Required { get; set; } +} + +/// RPC data type for SlashCommandInfo operations. +public sealed class SlashCommandInfo +{ + /// Canonical aliases without leading slashes. + [JsonPropertyName("aliases")] + public IList? Aliases { get; set; } + + /// Whether the command may run while an agent turn is active. + [JsonPropertyName("allowDuringAgentExecution")] + public bool AllowDuringAgentExecution { get; set; } + + /// Human-readable command description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Whether the command is experimental. + [JsonPropertyName("experimental")] + public bool? Experimental { get; set; } + + /// Optional unstructured input hint. + [JsonPropertyName("input")] + public SlashCommandInput? Input { get; set; } + + /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. + [JsonPropertyName("kind")] + public SlashCommandKind Kind { get; set; } + + /// Canonical command name without a leading slash. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +/// RPC data type for CommandList operations. +public sealed class CommandList +{ + /// Commands available in this session. + [JsonPropertyName("commands")] + public IList Commands { get => field ??= []; set; } +} + +/// RPC data type for CommandsList operations. +public sealed class CommandsListRequest +{ + /// Include runtime built-in commands. + [JsonPropertyName("includeBuiltins")] + public bool? IncludeBuiltins { get; set; } + + /// Include commands registered by protocol clients, including SDK clients and extensions. + [JsonPropertyName("includeClientCommands")] + public bool? IncludeClientCommands { get; set; } + + /// Include enabled user-invocable skills and commands. + [JsonPropertyName("includeSkills")] + public bool? IncludeSkills { get; set; } +} + +/// RPC data type for CommandsListRequestWithSession operations. +internal sealed class CommandsListRequestWithSession +{ + /// Include runtime built-in commands. + [JsonPropertyName("includeBuiltins")] + public bool? IncludeBuiltins { get; set; } + + /// Include commands registered by protocol clients, including SDK clients and extensions. + [JsonPropertyName("includeClientCommands")] + public bool? IncludeClientCommands { get; set; } + + /// Include enabled user-invocable skills and commands. + [JsonPropertyName("includeSkills")] + public bool? IncludeSkills { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Polymorphic base type discriminated by kind. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(SlashCommandInvocationResultText), "text")] +[JsonDerivedType(typeof(SlashCommandInvocationResultAgentPrompt), "agent-prompt")] +[JsonDerivedType(typeof(SlashCommandInvocationResultCompleted), "completed")] +public partial class SlashCommandInvocationResult +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// The text variant of . +public partial class SlashCommandInvocationResultText : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "text"; + + /// Whether text contains Markdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("markdown")] + public bool? Markdown { get; set; } + + /// Whether ANSI sequences should be preserved. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("preserveAnsi")] + public bool? PreserveAnsi { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } + + /// Text output for the client to render. + [JsonPropertyName("text")] + public required string Text { get; set; } +} + +/// The agent-prompt variant of . +public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "agent-prompt"; + + /// Prompt text to display to the user. + [JsonPropertyName("displayPrompt")] + public required string DisplayPrompt { get; set; } + + /// Optional target session mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SlashCommandAgentPromptMode? Mode { get; set; } + + /// Prompt to submit to the agent. + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } +} + +/// The completed variant of . +public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// Optional user-facing message describing the completed command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } +} + +/// RPC data type for CommandsInvoke operations. +internal sealed class CommandsInvokeRequest +{ + /// Raw input after the command name. + [JsonPropertyName("input")] + public string? Input { get; set; } + + /// Command name. Leading slashes are stripped and the name is matched case-insensitively. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for CommandsHandlePendingCommand operations. public sealed class CommandsHandlePendingCommandResult { @@ -4223,6 +4466,195 @@ public override void Write(Utf8JsonWriter writer, ExtensionStatus value, JsonSer } +/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SlashCommandInputCompletion : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SlashCommandInputCompletion(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the directory value. + public static SlashCommandInputCompletion Directory { get; } = new("directory"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); + + /// + public bool Equals(SlashCommandInputCompletion other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SlashCommandInputCompletion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GitHub.Copilot.SDK.GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SlashCommandInputCompletion value, JsonSerializerOptions options) + { + GitHub.Copilot.SDK.GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); + } + } +} + + +/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SlashCommandKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SlashCommandKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the builtin value. + public static SlashCommandKind Builtin { get; } = new("builtin"); + + /// Gets the skill value. + public static SlashCommandKind Skill { get; } = new("skill"); + + /// Gets the client value. + public static SlashCommandKind Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); + + /// + public bool Equals(SlashCommandKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SlashCommandKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GitHub.Copilot.SDK.GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SlashCommandKind value, JsonSerializerOptions options) + { + GitHub.Copilot.SDK.GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); + } + } +} + + +/// Optional target session mode. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SlashCommandAgentPromptMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SlashCommandAgentPromptMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the interactive value. + public static SlashCommandAgentPromptMode Interactive { get; } = new("interactive"); + + /// Gets the plan value. + public static SlashCommandAgentPromptMode Plan { get; } = new("plan"); + + /// Gets the autopilot value. + public static SlashCommandAgentPromptMode Autopilot { get; } = new("autopilot"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SlashCommandAgentPromptMode left, SlashCommandAgentPromptMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SlashCommandAgentPromptMode left, SlashCommandAgentPromptMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is SlashCommandAgentPromptMode other && Equals(other); + + /// + public bool Equals(SlashCommandAgentPromptMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SlashCommandAgentPromptMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GitHub.Copilot.SDK.GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SlashCommandAgentPromptMode value, JsonSerializerOptions options) + { + GitHub.Copilot.SDK.GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandAgentPromptMode)); + } + } +} + + /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -4728,9 +5160,9 @@ internal ServerSessionsApi(JsonRpc rpc) } /// Calls "sessions.fork". - public async Task ForkAsync(string sessionId, string? toEventId = null, CancellationToken cancellationToken = default) + public async Task ForkAsync(string sessionId, string? toEventId = null, string? name = null, CancellationToken cancellationToken = default) { - var request = new SessionsForkRequest { SessionId = sessionId, ToEventId = toEventId }; + var request = new SessionsForkRequest { SessionId = sessionId, ToEventId = toEventId, Name = name }; return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.fork", [request], cancellationToken); } } @@ -5207,10 +5639,10 @@ public async Task DisableAsync(string name, CancellationToken cancellationToken } /// Calls "session.skills.reload". - public async Task ReloadAsync(CancellationToken cancellationToken = default) + public async Task ReloadAsync(CancellationToken cancellationToken = default) { var request = new SessionSkillsReloadRequest { SessionId = _sessionId }; - await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.reload", [request], cancellationToken); + return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.reload", [request], cancellationToken); } } @@ -5376,6 +5808,20 @@ internal CommandsApi(JsonRpc rpc, string sessionId) _sessionId = sessionId; } + /// Calls "session.commands.list". + public async Task ListAsync(CommandsListRequest? request = null, CancellationToken cancellationToken = default) + { + var rpcRequest = new CommandsListRequestWithSession { SessionId = _sessionId, IncludeBuiltins = request?.IncludeBuiltins, IncludeSkills = request?.IncludeSkills, IncludeClientCommands = request?.IncludeClientCommands }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.commands.list", [rpcRequest], cancellationToken); + } + + /// Calls "session.commands.invoke". + public async Task InvokeAsync(string name, string? input = null, CancellationToken cancellationToken = default) + { + var request = new CommandsInvokeRequest { SessionId = _sessionId, Name = name, Input = input }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.commands.invoke", [request], cancellationToken); + } + /// Calls "session.commands.handlePendingCommand". public async Task HandlePendingCommandAsync(string requestId, string? error = null, CancellationToken cancellationToken = default) { @@ -5679,8 +6125,12 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncTrue when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("isAutopilotContinuation")] + public bool? IsAutopilotContinuation { get; set; } + /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("nativeDocumentPathFallbackPaths")] @@ -2049,6 +2054,11 @@ public partial class AssistantUsageData [JsonPropertyName("apiCallId")] public string? ApiCallId { get; set; } + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("apiEndpoint")] + public AssistantUsageApiEndpoint? ApiEndpoint { get; set; } + /// Number of tokens read from prompt cache. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cacheReadTokens")] @@ -5521,6 +5531,73 @@ public override void Write(Utf8JsonWriter writer, AssistantMessageToolRequestTyp } } +/// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AssistantUsageApiEndpoint : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AssistantUsageApiEndpoint(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the /chat/completions value. + public static AssistantUsageApiEndpoint ChatCompletions { get; } = new("/chat/completions"); + + /// Gets the /v1/messages value. + public static AssistantUsageApiEndpoint V1Messages { get; } = new("/v1/messages"); + + /// Gets the /responses value. + public static AssistantUsageApiEndpoint Responses { get; } = new("/responses"); + + /// Gets the ws:/responses value. + public static AssistantUsageApiEndpoint WsResponses { get; } = new("ws:/responses"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AssistantUsageApiEndpoint left, AssistantUsageApiEndpoint right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AssistantUsageApiEndpoint left, AssistantUsageApiEndpoint right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AssistantUsageApiEndpoint other && Equals(other); + + /// + public bool Equals(AssistantUsageApiEndpoint other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AssistantUsageApiEndpoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GitHub.Copilot.SDK.GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AssistantUsageApiEndpoint value, JsonSerializerOptions options) + { + GitHub.Copilot.SDK.GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AssistantUsageApiEndpoint)); + } + } +} + /// Where the failed model call originated. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index fa579ac6b..fd1d0228c 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -264,6 +264,21 @@ public void QueuedCommandResult_SerializesHandledAsBoolean_WithSdkOptions() Assert.Null(deserialized.StopProcessingQueue); } + [Fact] + public void PermissionDecision_SerializesBaseDiscriminator_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new PermissionDecision + { + Kind = PermissionRequestResultKind.Approved.Value + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + + Assert.Equal("approve-once", document.RootElement.GetProperty("kind").GetString()); + } + private static JsonSerializerOptions GetSerializerOptions() { var prop = typeof(CopilotClient) diff --git a/go/rpc/generated_rpc_union_test.go b/go/rpc/generated_rpc_union_test.go index e2ca093df..a8a34ec60 100644 --- a/go/rpc/generated_rpc_union_test.go +++ b/go/rpc/generated_rpc_union_test.go @@ -2,7 +2,10 @@ package rpc import ( "encoding/json" + "io" "testing" + + "github.com/github/copilot-sdk/go/internal/jsonrpc2" ) func TestExternalToolResultJSONUnion(t *testing.T) { @@ -130,6 +133,58 @@ func TestMcpServerConfigJSONUnion(t *testing.T) { } } +func TestCommandsInvokeUnmarshalsSlashCommandInvocationResult(t *testing.T) { + clientToServerReader, clientToServerWriter := io.Pipe() + serverToClientReader, serverToClientWriter := io.Pipe() + + client := jsonrpc2.NewClient(clientToServerWriter, serverToClientReader) + server := jsonrpc2.NewClient(serverToClientWriter, clientToServerReader) + server.SetRequestHandler("session.commands.invoke", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request struct { + Input string `json:"input"` + Name string `json:"name"` + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: err.Error()} + } + if request.SessionID != "session-1" || request.Name != "help" || request.Input != "details" { + return nil, &jsonrpc2.Error{Code: -32602, Message: "unexpected invoke request"} + } + return json.RawMessage(`{"kind":"text","text":"hello","markdown":true}`), nil + }) + + client.Start() + server.Start() + t.Cleanup(func() { + client.Stop() + server.Stop() + _ = clientToServerWriter.Close() + _ = clientToServerReader.Close() + _ = serverToClientWriter.Close() + _ = serverToClientReader.Close() + }) + + input := "details" + result, err := NewSessionRpc(client, "session-1").Commands.Invoke(t.Context(), &CommandsInvokeRequest{ + Input: &input, + Name: "help", + }) + if err != nil { + t.Fatalf("invoke command: %v", err) + } + textResult, ok := result.(*SlashCommandTextResult) + if !ok { + t.Fatalf("invoke result = %T, want *SlashCommandTextResult", result) + } + if textResult.Text != "hello" { + t.Fatalf("invoke result text = %q, want hello", textResult.Text) + } + if textResult.Markdown == nil || !*textResult.Markdown { + t.Fatalf("invoke result markdown = %v, want true", textResult.Markdown) + } +} + func TestUIElicitationFieldValueJSONUnion(t *testing.T) { raw, err := json.Marshal(UIElicitationBooleanValue(true)) if err != nil { diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 81e84d3c0..f83d26baa 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -93,6 +93,11 @@ type AgentSelectResult struct { Agent AgentInfo `json:"agent"` } +type CommandList struct { + // Commands available in this session + Commands []SlashCommandInfo `json:"commands"` +} + type CommandsHandlePendingCommandRequest struct { // Error message if the command handler failed Error *string `json:"error,omitempty"` @@ -105,6 +110,22 @@ type CommandsHandlePendingCommandResult struct { Success bool `json:"success"` } +type CommandsInvokeRequest struct { + // Raw input after the command name + Input *string `json:"input,omitempty"` + // Command name. Leading slashes are stripped and the name is matched case-insensitively. + Name string `json:"name"` +} + +type CommandsListRequest struct { + // Include runtime built-in commands + IncludeBuiltins *bool `json:"includeBuiltins,omitempty"` + // Include commands registered by protocol clients, including SDK clients and extensions + IncludeClientCommands *bool `json:"includeClientCommands,omitempty"` + // Include enabled user-invocable skills and commands + IncludeSkills *bool `json:"includeSkills,omitempty"` +} + type CommandsRespondToQueuedCommandRequest struct { // Request ID from the queued command event RequestID string `json:"requestId"` @@ -705,6 +726,23 @@ type Model struct { type ModelBilling struct { // Billing cost multiplier relative to the base rate Multiplier *float64 `json:"multiplier,omitempty"` + // Token-level pricing information for this model + TokenPrices *ModelBillingTokenPrices `json:"tokenPrices,omitempty"` +} + +// Token-level pricing information for this model +type ModelBillingTokenPrices struct { + // Number of tokens per standard billing batch + BatchSize *int64 `json:"batchSize,omitempty"` + // Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 + // AIU = $0.01 USD) + CachePrice *int64 `json:"cachePrice,omitempty"` + // Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU + // = $0.01 USD) + InputPrice *int64 `json:"inputPrice,omitempty"` + // Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 + // AIU = $0.01 USD) + OutputPrice *int64 `json:"outputPrice,omitempty"` } // Model capabilities and limits @@ -1427,6 +1465,8 @@ type SessionFsWriteFileRequest struct { // Experimental: SessionsForkRequest is part of an experimental API and may change or be // removed. type SessionsForkRequest struct { + // Optional friendly name to assign to the forked session. + Name *string `json:"name,omitempty"` // Source session ID to fork from SessionID string `json:"sessionId"` // Optional event ID boundary. When provided, the fork includes only events before this ID @@ -1437,6 +1477,8 @@ type SessionsForkRequest struct { // Experimental: SessionsForkResult is part of an experimental API and may change or be // removed. type SessionsForkResult struct { + // Friendly name assigned to the forked session, if any. + Name *string `json:"name,omitempty"` // The new forked session's ID SessionID string `json:"sessionId"` } @@ -1527,9 +1569,107 @@ type SkillsEnableRequest struct { type SkillsEnableResult struct { } -// Experimental: SkillsReloadResult is part of an experimental API and may change or be +// Experimental: SkillsLoadDiagnostics is part of an experimental API and may change or be // removed. -type SkillsReloadResult struct { +type SkillsLoadDiagnostics struct { + // Errors emitted while loading skills (e.g. skills that failed to load entirely) + Errors []string `json:"errors"` + // Warnings emitted while loading skills (e.g. skills that loaded but had issues) + Warnings []string `json:"warnings"` +} + +type SlashCommandInfo struct { + // Canonical aliases without leading slashes + Aliases []string `json:"aliases,omitempty"` + // Whether the command may run while an agent turn is active + AllowDuringAgentExecution bool `json:"allowDuringAgentExecution"` + // Human-readable command description + Description string `json:"description"` + // Whether the command is experimental + Experimental *bool `json:"experimental,omitempty"` + // Optional unstructured input hint + Input *SlashCommandInput `json:"input,omitempty"` + // Coarse command category for grouping and behavior: runtime built-in, skill-backed + // command, or SDK/client-owned command + Kind SlashCommandKind `json:"kind"` + // Canonical command name without a leading slash + Name string `json:"name"` +} + +// Optional unstructured input hint +type SlashCommandInput struct { + // Optional completion hint for the input (e.g. 'directory' for filesystem path completion) + Completion *SlashCommandInputCompletion `json:"completion,omitempty"` + // Hint to display when command input has not been provided + Hint string `json:"hint"` + // When true, clients should pass the full text after the command name as a single argument + // rather than splitting on whitespace + PreserveMultilineInput *bool `json:"preserveMultilineInput,omitempty"` + // When true, the command requires non-empty input; clients should render the input hint as + // required + Required *bool `json:"required,omitempty"` +} + +type SlashCommandInvocationResult interface { + slashCommandInvocationResult() + Kind() SlashCommandInvocationResultKind +} + +type RawSlashCommandInvocationResultData struct { + Discriminator SlashCommandInvocationResultKind + Raw json.RawMessage +} + +func (RawSlashCommandInvocationResultData) slashCommandInvocationResult() {} +func (r RawSlashCommandInvocationResultData) Kind() SlashCommandInvocationResultKind { + return r.Discriminator +} + +type SlashCommandAgentPromptResult struct { + // Prompt text to display to the user + DisplayPrompt string `json:"displayPrompt"` + // Optional target session mode + Mode *SlashCommandAgentPromptMode `json:"mode,omitempty"` + // Prompt to submit to the agent + Prompt string `json:"prompt"` + // True when the invocation mutated user runtime settings; consumers caching settings should + // refresh + RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` +} + +func (SlashCommandAgentPromptResult) slashCommandInvocationResult() {} +func (SlashCommandAgentPromptResult) Kind() SlashCommandInvocationResultKind { + return SlashCommandInvocationResultKindAgentPrompt +} + +type SlashCommandCompletedResult struct { + // Optional user-facing message describing the completed command + Message *string `json:"message,omitempty"` + // True when the invocation mutated user runtime settings; consumers caching settings should + // refresh + RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` +} + +func (SlashCommandCompletedResult) slashCommandInvocationResult() {} +func (SlashCommandCompletedResult) Kind() SlashCommandInvocationResultKind { + return SlashCommandInvocationResultKindCompleted +} + +type SlashCommandTextResult struct { + // Whether text contains Markdown + Markdown *bool `json:"markdown,omitempty"` + // Whether ANSI sequences should be preserved + PreserveAnsi *bool `json:"preserveAnsi,omitempty"` + // True when the invocation mutated user runtime settings; consumers caching settings should + // refresh + RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` + // Text output for the client to render + Text string `json:"text"` +} + +func (SlashCommandTextResult) slashCommandInvocationResult() {} +func (SlashCommandTextResult) Kind() SlashCommandInvocationResultKind { + return SlashCommandInvocationResultKindText } type SuspendResult struct { @@ -2326,6 +2466,41 @@ const ( ShellKillSignalSIGTERM ShellKillSignal = "SIGTERM" ) +// Optional target session mode +type SlashCommandAgentPromptMode string + +const ( + SlashCommandAgentPromptModeAutopilot SlashCommandAgentPromptMode = "autopilot" + SlashCommandAgentPromptModeInteractive SlashCommandAgentPromptMode = "interactive" + SlashCommandAgentPromptModePlan SlashCommandAgentPromptMode = "plan" +) + +// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) +type SlashCommandInputCompletion string + +const ( + SlashCommandInputCompletionDirectory SlashCommandInputCompletion = "directory" +) + +// Kind discriminator for SlashCommandInvocationResult. +type SlashCommandInvocationResultKind string + +const ( + SlashCommandInvocationResultKindAgentPrompt SlashCommandInvocationResultKind = "agent-prompt" + SlashCommandInvocationResultKindCompleted SlashCommandInvocationResultKind = "completed" + SlashCommandInvocationResultKindText SlashCommandInvocationResultKind = "text" +) + +// Coarse command category for grouping and behavior: runtime built-in, skill-backed +// command, or SDK/client-owned command +type SlashCommandKind string + +const ( + SlashCommandKindBuiltin SlashCommandKind = "builtin" + SlashCommandKindClient SlashCommandKind = "client" + SlashCommandKindSkill SlashCommandKind = "skill" +) + // How the agent is currently being managed by the runtime type TaskAgentInfoExecutionMode string @@ -2443,8 +2618,12 @@ type serverApi struct { type ServerAccountApi serverApi -func (a *ServerAccountApi) GetQuota(ctx context.Context, params *AccountGetQuotaRequest) (*AccountGetQuotaResult, error) { - raw, err := a.client.Request("account.getQuota", params) +func (a *ServerAccountApi) GetQuota(ctx context.Context, params ...*AccountGetQuotaRequest) (*AccountGetQuotaResult, error) { + var requestParams *AccountGetQuotaRequest + if len(params) > 0 { + requestParams = params[0] + } + raw, err := a.client.Request("account.getQuota", requestParams) if err != nil { return nil, err } @@ -2549,8 +2728,12 @@ func (s *ServerMcpApi) Config() *ServerMcpConfigApi { type ServerModelsApi serverApi -func (a *ServerModelsApi) List(ctx context.Context, params *ModelsListRequest) (*ModelList, error) { - raw, err := a.client.Request("models.list", params) +func (a *ServerModelsApi) List(ctx context.Context, params ...*ModelsListRequest) (*ModelList, error) { + var requestParams *ModelsListRequest + if len(params) > 0 { + requestParams = params[0] + } + raw, err := a.client.Request("models.list", requestParams) if err != nil { return nil, err } @@ -2818,6 +3001,53 @@ func (a *CommandsApi) HandlePendingCommand(ctx context.Context, params *Commands return &result, nil } +func (a *CommandsApi) Invoke(ctx context.Context, params *CommandsInvokeRequest) (SlashCommandInvocationResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + if params.Input != nil { + req["input"] = *params.Input + } + req["name"] = params.Name + } + raw, err := a.client.Request("session.commands.invoke", req) + if err != nil { + return nil, err + } + result, err := unmarshalSlashCommandInvocationResult(raw) + if err != nil { + return nil, err + } + return result, nil +} + +func (a *CommandsApi) List(ctx context.Context, params ...*CommandsListRequest) (*CommandList, error) { + var requestParams *CommandsListRequest + if len(params) > 0 { + requestParams = params[0] + } + req := map[string]any{"sessionId": a.sessionID} + if requestParams != nil { + if requestParams.IncludeBuiltins != nil { + req["includeBuiltins"] = *requestParams.IncludeBuiltins + } + if requestParams.IncludeClientCommands != nil { + req["includeClientCommands"] = *requestParams.IncludeClientCommands + } + if requestParams.IncludeSkills != nil { + req["includeSkills"] = *requestParams.IncludeSkills + } + } + raw, err := a.client.Request("session.commands.list", req) + if err != nil { + return nil, err + } + var result CommandList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + func (a *CommandsApi) RespondToQueuedCommand(ctx context.Context, params *CommandsRespondToQueuedCommandRequest) (*CommandsRespondToQueuedCommandResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { @@ -3385,13 +3615,13 @@ func (a *SkillsApi) List(ctx context.Context) (*SkillList, error) { return &result, nil } -func (a *SkillsApi) Reload(ctx context.Context) (*SkillsReloadResult, error) { +func (a *SkillsApi) Reload(ctx context.Context) (*SkillsLoadDiagnostics, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request("session.skills.reload", req) if err != nil { return nil, err } - var result SkillsReloadResult + var result SkillsLoadDiagnostics if err := json.Unmarshal(raw, &result); err != nil { return nil, err } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index b4ab8518c..bf77c3c2e 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1054,6 +1054,86 @@ func (r *PermissionDecisionRequest) UnmarshalJSON(data []byte) error { return nil } +func unmarshalSlashCommandInvocationResult(data []byte) (SlashCommandInvocationResult, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind SlashCommandInvocationResultKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case SlashCommandInvocationResultKindAgentPrompt: + var d SlashCommandAgentPromptResult + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SlashCommandInvocationResultKindCompleted: + var d SlashCommandCompletedResult + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case SlashCommandInvocationResultKindText: + var d SlashCommandTextResult + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawSlashCommandInvocationResultData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawSlashCommandInvocationResultData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind SlashCommandInvocationResultKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r SlashCommandAgentPromptResult) MarshalJSON() ([]byte, error) { + type alias SlashCommandAgentPromptResult + return json.Marshal(struct { + Kind SlashCommandInvocationResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SlashCommandCompletedResult) MarshalJSON() ([]byte, error) { + type alias SlashCommandCompletedResult + return json.Marshal(struct { + Kind SlashCommandInvocationResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r SlashCommandTextResult) MarshalJSON() ([]byte, error) { + type alias SlashCommandTextResult + return json.Marshal(struct { + Kind SlashCommandInvocationResultKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func unmarshalTaskInfo(data []byte) (TaskInfo, error) { if string(data) == "null" { return nil, nil diff --git a/go/zsession_encoding.go b/go/zsession_encoding.go index 72fa12aed..fc603c5ca 100644 --- a/go/zsession_encoding.go +++ b/go/zsession_encoding.go @@ -670,6 +670,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { Attachments []json.RawMessage `json:"attachments,omitempty"` Content string `json:"content"` InteractionID *string `json:"interactionId,omitempty"` + IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitempty"` ParentAgentTaskID *string `json:"parentAgentTaskId,omitempty"` Source *string `json:"source,omitempty"` @@ -693,6 +694,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { } r.Content = raw.Content r.InteractionID = raw.InteractionID + r.IsAutopilotContinuation = raw.IsAutopilotContinuation r.NativeDocumentPathFallbackPaths = raw.NativeDocumentPathFallbackPaths r.ParentAgentTaskID = raw.ParentAgentTaskID r.Source = raw.Source diff --git a/go/zsession_events.go b/go/zsession_events.go index 0c62df5c4..c92c53d7e 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -522,6 +522,8 @@ func (*SessionInfoData) Type() SessionEventType { return SessionEventTypeSession type AssistantUsageData struct { // Completion ID from the model provider (e.g., chatcmpl-abc123) APICallID *string `json:"apiCallId,omitempty"` + // API endpoint used for this model call, matching CAPI supported_endpoints vocabulary + APIEndpoint *AssistantUsageAPIEndpoint `json:"apiEndpoint,omitempty"` // Number of tokens read from prompt cache CacheReadTokens *float64 `json:"cacheReadTokens,omitempty"` // Number of tokens written to prompt cache @@ -1337,6 +1339,8 @@ type UserMessageData struct { Content string `json:"content"` // CAPI interaction ID for correlating this user message with its turn InteractionID *string `json:"interactionId,omitempty"` + // True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. + IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` // Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitempty"` // Parent agent task ID for background telemetry correlated to this user turn @@ -2753,6 +2757,16 @@ const ( AssistantMessageToolRequestTypeFunction AssistantMessageToolRequestType = "function" ) +// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary +type AssistantUsageAPIEndpoint string + +const ( + AssistantUsageAPIEndpointChatCompletions AssistantUsageAPIEndpoint = "/chat/completions" + AssistantUsageAPIEndpointResponses AssistantUsageAPIEndpoint = "/responses" + AssistantUsageAPIEndpointV1Messages AssistantUsageAPIEndpoint = "/v1/messages" + AssistantUsageAPIEndpointWsResponses AssistantUsageAPIEndpoint = "ws:/responses" +) + // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) type ElicitationCompletedAction string diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 97dbbd3d7..b341f485c 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.46", + "@github/copilot": "^1.0.47", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -663,26 +663,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.46.tgz", - "integrity": "sha512-e3gxCj8DLGesTAZQ5+jCCbCxe3lMyjKfs5eLgER/SID8Rcb7YpgBXoUvOn3eXxLSsJEmJ3GagHaaHDkf3Zm+Ng==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.47.tgz", + "integrity": "sha512-U4WrajOOjjMVleqIRvRt+kDsjYQPLHxtJMMtdzW2N18dbRddlxqN+qo6ZOxOTy3tks2+YI+G89zyO1qpxpuWSg==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.46", - "@github/copilot-darwin-x64": "1.0.46", - "@github/copilot-linux-arm64": "1.0.46", - "@github/copilot-linux-x64": "1.0.46", - "@github/copilot-win32-arm64": "1.0.46", - "@github/copilot-win32-x64": "1.0.46" + "@github/copilot-darwin-arm64": "1.0.47", + "@github/copilot-darwin-x64": "1.0.47", + "@github/copilot-linux-arm64": "1.0.47", + "@github/copilot-linux-x64": "1.0.47", + "@github/copilot-win32-arm64": "1.0.47", + "@github/copilot-win32-x64": "1.0.47" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.46.tgz", - "integrity": "sha512-zbhXuRguCdDgeIZKH+rjgBM/6CDMUmhLMck8w9XFDxUY2wrP7MSWXuX8yA4/1H3ySOTZMIH1G5DQpWh+npmR2Q==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.47.tgz", + "integrity": "sha512-sGuN+7VfBjOTbPkyKFm0dPfp1hwyNsJVkNsV+3xmOwVsGy3nhROc76sQ5SWWSmyDGl7H58KnpPazlSDwbpf4PQ==", "cpu": [ "arm64" ], @@ -696,9 +696,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.46.tgz", - "integrity": "sha512-kSUcV6cARhM+b/BuNSQtazbORTetRjIWpO3SqWSmH+2UoeZP5A5x+ipr7mhshq+E+pcWPeQKMGbKGY3lrCSMFw==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.47.tgz", + "integrity": "sha512-nVHYbzvOau5zy4nONWZPXROIrqzd7DhY12bMkE7spLe7lj0Sh6MFtTdPpMT7kkaObEikGYLTrZtOUpguwqHkmA==", "cpu": [ "x64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.46.tgz", - "integrity": "sha512-Tz3F0LuGFbOvvv0VKQJ4E5XYBsTdqTNMAwOhbkwX6TuKMX88uLJNKP5uPf6yuu1z3J3nt/5rfEd9CxVrZbnqLA==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.47.tgz", + "integrity": "sha512-7aDoE6pnSGcCTuPdJKyHfzif/Rj1z5UE0gLMHHQMo1QIYJkUZFX7mV8Ng4zB+2edq8lNL5DiYRcbFajV54ibSg==", "cpu": [ "arm64" ], @@ -728,9 +728,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.46.tgz", - "integrity": "sha512-s9JWe/YE78I7QEeXrvDGHB5x2XnnkegUJYVE9QR2DI/qLXviHMarM3akOUhed21uVqzoiLPacXKZcTcaDO8tOg==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.47.tgz", + "integrity": "sha512-wB5ekOdoxM/6Ogguk54fqJTHTRJkXwUIyzrbYaMy7zANE82jeRE1PQqs+5SdUZXq2IBMZIN1vq6bM56gpb54qg==", "cpu": [ "x64" ], @@ -744,9 +744,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.46.tgz", - "integrity": "sha512-auX8o8vG8A+rdSthvey1D8q3o6lNlNIfHFjoBU0Z9Fxid6Ghz2paaAn0/Uwz9Ev8W8cn/5C5kEPs3niMXSh4Jw==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.47.tgz", + "integrity": "sha512-AenPXpTeXApOh25biS+Vmc1Uau78OLHxeXjXDF6Po07xWO7fVzorEK0hnSoD6xmpjptvP2MDSMk4as7jyvM0sQ==", "cpu": [ "arm64" ], @@ -760,9 +760,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.46.tgz", - "integrity": "sha512-iXo9TUqtSxqlBfC+SZSQMrctKJpWR19zr+8dk7hczE42gOVB0/A+NySJwCmY3UFAEY98lbLDjIC+NCbYFcpEHA==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.47.tgz", + "integrity": "sha512-35bOBTTIm31rgbvFDogAMojWMSV6sLTd3mGjLl1Lf/d0KZGCGLqWXAYMAcV3grEjiAEXxlLLzNs8OfBR/9OdZg==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 60002a2aa..7067cc76c 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.46", + "@github/copilot": "^1.0.47", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index f2208347e..061d3f3b1 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.46", + "@github/copilot": "^1.0.47", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 57aeb6f69..36dcb5952 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -12,6 +12,20 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; * via the `definition` "AuthInfoType". */ export type AuthInfoType = "hmac" | "env" | "user" | "gh-cli" | "api-key" | "token" | "copilot-api-token"; +/** + * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SlashCommandKind". + */ +export type SlashCommandKind = "builtin" | "skill" | "client"; +/** + * Optional completion hint for the input (e.g. 'directory' for filesystem path completion) + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SlashCommandInputCompletion". + */ +export type SlashCommandInputCompletion = "directory"; /** * Result of the queued command execution * @@ -235,6 +249,18 @@ export type SessionFsSetProviderConventions = "windows" | "posix"; * via the `definition` "ShellKillSignal". */ export type ShellKillSignal = "SIGTERM" | "SIGKILL" | "SIGINT"; +/** + * Optional target session mode + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SlashCommandAgentPromptMode". + */ +export type SlashCommandAgentPromptMode = "interactive" | "plan" | "autopilot"; + +export type SlashCommandInvocationResult = + | SlashCommandTextResult + | SlashCommandAgentPromptResult + | SlashCommandCompletedResult; /** * Current lifecycle status of the task * @@ -402,6 +428,59 @@ export interface AgentSelectResult { agent: AgentInfo; } +export interface CommandList { + /** + * Commands available in this session + */ + commands: SlashCommandInfo[]; +} + +export interface SlashCommandInfo { + /** + * Canonical command name without a leading slash + */ + name: string; + /** + * Canonical aliases without leading slashes + */ + aliases?: string[]; + /** + * Human-readable command description + */ + description: string; + kind: SlashCommandKind; + input?: SlashCommandInput; + /** + * Whether the command may run while an agent turn is active + */ + allowDuringAgentExecution: boolean; + /** + * Whether the command is experimental + */ + experimental?: boolean; +} +/** + * Optional unstructured input hint + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SlashCommandInput". + */ +export interface SlashCommandInput { + /** + * Hint to display when command input has not been provided + */ + hint: string; + /** + * When true, the command requires non-empty input; clients should render the input hint as required + */ + required?: boolean; + completion?: SlashCommandInputCompletion; + /** + * When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace + */ + preserveMultilineInput?: boolean; +} + export interface CommandsHandlePendingCommandRequest { /** * Request ID from the command invocation event @@ -420,6 +499,32 @@ export interface CommandsHandlePendingCommandResult { success: boolean; } +export interface CommandsInvokeRequest { + /** + * Command name. Leading slashes are stripped and the name is matched case-insensitively. + */ + name: string; + /** + * Raw input after the command name + */ + input?: string; +} + +export interface CommandsListRequest { + /** + * Include runtime built-in commands + */ + includeBuiltins?: boolean; + /** + * Include enabled user-invocable skills and commands + */ + includeSkills?: boolean; + /** + * Include commands registered by protocol clients, including SDK clients and extensions + */ + includeClientCommands?: boolean; +} + export interface CommandsRespondToQueuedCommandRequest { /** * Request ID from the queued command event @@ -1199,6 +1304,31 @@ export interface ModelBilling { * Billing cost multiplier relative to the base rate */ multiplier?: number; + tokenPrices?: ModelBillingTokenPrices; +} +/** + * Token-level pricing information for this model + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelBillingTokenPrices". + */ +export interface ModelBillingTokenPrices { + /** + * Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + */ + inputPrice?: number; + /** + * Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + */ + outputPrice?: number; + /** + * Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + */ + cachePrice?: number; + /** + * Number of tokens per standard billing batch + */ + batchSize?: number; } /** * Override individual model capabilities resolved by the runtime @@ -1882,6 +2012,10 @@ export interface SessionsForkRequest { * Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. */ toEventId?: string; + /** + * Optional friendly name to assign to the forked session. + */ + name?: string; } /** @experimental */ @@ -1890,6 +2024,10 @@ export interface SessionsForkResult { * The new forked session's ID */ sessionId: string; + /** + * Friendly name assigned to the forked session, if any. + */ + name?: string; } export interface ShellExecRequest { @@ -1998,6 +2136,76 @@ export interface SkillsEnableRequest { name: string; } +/** @experimental */ +export interface SkillsLoadDiagnostics { + /** + * Warnings emitted while loading skills (e.g. skills that loaded but had issues) + */ + warnings: string[]; + /** + * Errors emitted while loading skills (e.g. skills that failed to load entirely) + */ + errors: string[]; +} + +export interface SlashCommandAgentPromptResult { + /** + * Agent prompt result discriminator + */ + kind: "agent-prompt"; + /** + * Prompt to submit to the agent + */ + prompt: string; + /** + * Prompt text to display to the user + */ + displayPrompt: string; + mode?: SlashCommandAgentPromptMode; + /** + * True when the invocation mutated user runtime settings; consumers caching settings should refresh + */ + runtimeSettingsChanged?: boolean; +} + +export interface SlashCommandCompletedResult { + /** + * Completed result discriminator + */ + kind: "completed"; + /** + * Optional user-facing message describing the completed command + */ + message?: string; + /** + * True when the invocation mutated user runtime settings; consumers caching settings should refresh + */ + runtimeSettingsChanged?: boolean; +} + +export interface SlashCommandTextResult { + /** + * Text result discriminator + */ + kind: "text"; + /** + * Text output for the client to render + */ + text: string; + /** + * Whether text contains Markdown + */ + markdown?: boolean; + /** + * Whether ANSI sequences should be preserved + */ + preserveAnsi?: boolean; + /** + * True when the invocation mutated user runtime settings; consumers caching settings should refresh + */ + runtimeSettingsChanged?: boolean; +} + export interface TaskAgentInfo { /** * Task kind @@ -2765,7 +2973,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.skills.enable", { sessionId, ...params }), disable: async (params: SkillsDisableRequest): Promise => connection.sendRequest("session.skills.disable", { sessionId, ...params }), - reload: async (): Promise => + reload: async (): Promise => connection.sendRequest("session.skills.reload", { sessionId }), }, /** @experimental */ @@ -2805,6 +3013,10 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }), }, commands: { + list: async (params?: CommandsListRequest): Promise => + connection.sendRequest("session.commands.list", { sessionId, ...params }), + invoke: async (params: CommandsInvokeRequest): Promise => + connection.sendRequest("session.commands.invoke", { sessionId, ...params }), handlePendingCommand: async (params: CommandsHandlePendingCommandRequest): Promise => connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }), respondToQueuedCommand: async (params: CommandsRespondToQueuedCommandRequest): Promise => diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 6f2bab31c..606cffae9 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -125,6 +125,10 @@ export type UserMessageAttachmentGithubReferenceType = "issue" | "pr" | "discuss * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ export type AssistantMessageToolRequestType = "function" | "custom"; +/** + * API endpoint used for this model call, matching CAPI supported_endpoints vocabulary + */ +export type AssistantUsageApiEndpoint = "/chat/completions" | "/v1/messages" | "/responses" | "ws:/responses"; /** * Where the failed model call originated */ @@ -1582,6 +1586,10 @@ export interface UserMessageData { * CAPI interaction ID for correlating this user message with its turn */ interactionId?: string; + /** + * True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. + */ + isAutopilotContinuation?: boolean; /** * Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit */ @@ -2207,6 +2215,7 @@ export interface AssistantUsageData { * Completion ID from the model provider (e.g., chatcmpl-abc123) */ apiCallId?: string; + apiEndpoint?: AssistantUsageApiEndpoint; /** * Number of tokens read from prompt cache */ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index c4f6fd56b..9c7329be1 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -207,6 +207,19 @@ class AuthInfoType(Enum): TOKEN = "token" USER = "user" +class SlashCommandInputCompletion(Enum): + """Optional completion hint for the input (e.g. 'directory' for filesystem path completion)""" + + DIRECTORY = "directory" + +class SlashCommandKind(Enum): + """Coarse command category for grouping and behavior: runtime built-in, skill-backed + command, or SDK/client-owned command + """ + BUILTIN = "builtin" + CLIENT = "client" + SKILL = "skill" + @dataclass class CommandsHandlePendingCommandRequest: request_id: str @@ -245,6 +258,57 @@ def to_dict(self) -> dict: result["success"] = from_bool(self.success) return result +@dataclass +class CommandsInvokeRequest: + name: str + """Command name. Leading slashes are stripped and the name is matched case-insensitively.""" + + input: str | None = None + """Raw input after the command name""" + + @staticmethod + def from_dict(obj: Any) -> 'CommandsInvokeRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + input = from_union([from_str, from_none], obj.get("input")) + return CommandsInvokeRequest(name, input) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.input is not None: + result["input"] = from_union([from_str, from_none], self.input) + return result + +@dataclass +class CommandsListRequest: + include_builtins: bool | None = None + """Include runtime built-in commands""" + + include_client_commands: bool | None = None + """Include commands registered by protocol clients, including SDK clients and extensions""" + + include_skills: bool | None = None + """Include enabled user-invocable skills and commands""" + + @staticmethod + def from_dict(obj: Any) -> 'CommandsListRequest': + assert isinstance(obj, dict) + include_builtins = from_union([from_bool, from_none], obj.get("includeBuiltins")) + include_client_commands = from_union([from_bool, from_none], obj.get("includeClientCommands")) + include_skills = from_union([from_bool, from_none], obj.get("includeSkills")) + return CommandsListRequest(include_builtins, include_client_commands, include_skills) + + def to_dict(self) -> dict: + result: dict = {} + if self.include_builtins is not None: + result["includeBuiltins"] = from_union([from_bool, from_none], self.include_builtins) + if self.include_client_commands is not None: + result["includeClientCommands"] = from_union([from_bool, from_none], self.include_client_commands) + if self.include_skills is not None: + result["includeSkills"] = from_union([from_bool, from_none], self.include_skills) + return result + @dataclass class QueuedCommandResult: """Result of the queued command execution""" @@ -538,7 +602,7 @@ class ExternalToolTextResultForLlmContentResourceLinkType(Enum): class ExternalToolTextResultForLlmContentTerminalType(Enum): TERMINAL = "terminal" -class ExternalToolTextResultForLlmContentTextType(Enum): +class KindEnum(Enum): TEXT = "text" class FilterMappingString(Enum): @@ -917,30 +981,54 @@ class MCPServerConfigLocalType(Enum): LOCAL = "local" STDIO = "stdio" -class SessionMode(Enum): - """The agent mode. Valid values: "interactive", "plan", "autopilot".""" +class Mode(Enum): + """The agent mode. Valid values: "interactive", "plan", "autopilot". + Optional target session mode + """ AUTOPILOT = "autopilot" INTERACTIVE = "interactive" PLAN = "plan" @dataclass -class ModelBilling: - """Billing information""" +class ModelBillingTokenPrices: + """Token-level pricing information for this model""" - multiplier: float | None = None - """Billing cost multiplier relative to the base rate""" + batch_size: int | None = None + """Number of tokens per standard billing batch""" + + cache_price: int | None = None + """Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 + AIU = $0.01 USD) + """ + input_price: int | None = None + """Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU + = $0.01 USD) + """ + output_price: int | None = None + """Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 + AIU = $0.01 USD) + """ @staticmethod - def from_dict(obj: Any) -> 'ModelBilling': + def from_dict(obj: Any) -> 'ModelBillingTokenPrices': assert isinstance(obj, dict) - multiplier = from_union([from_float, from_none], obj.get("multiplier")) - return ModelBilling(multiplier) + batch_size = from_union([from_int, from_none], obj.get("batchSize")) + cache_price = from_union([from_int, from_none], obj.get("cachePrice")) + input_price = from_union([from_int, from_none], obj.get("inputPrice")) + output_price = from_union([from_int, from_none], obj.get("outputPrice")) + return ModelBillingTokenPrices(batch_size, cache_price, input_price, output_price) def to_dict(self) -> dict: result: dict = {} - if self.multiplier is not None: - result["multiplier"] = from_union([to_float, from_none], self.multiplier) + if self.batch_size is not None: + result["batchSize"] = from_union([from_int, from_none], self.batch_size) + if self.cache_price is not None: + result["cachePrice"] = from_union([from_int, from_none], self.cache_price) + if self.input_price is not None: + result["inputPrice"] = from_union([from_int, from_none], self.input_price) + if self.output_price is not None: + result["outputPrice"] = from_union([from_int, from_none], self.output_price) return result @dataclass @@ -1829,6 +1917,9 @@ class SessionsForkRequest: session_id: str """Source session ID to fork from""" + name: str | None = None + """Optional friendly name to assign to the forked session.""" + to_event_id: str | None = None """Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. @@ -1838,12 +1929,15 @@ class SessionsForkRequest: def from_dict(obj: Any) -> 'SessionsForkRequest': assert isinstance(obj, dict) session_id = from_str(obj.get("sessionId")) + name = from_union([from_str, from_none], obj.get("name")) to_event_id = from_union([from_str, from_none], obj.get("toEventId")) - return SessionsForkRequest(session_id, to_event_id) + return SessionsForkRequest(session_id, name, to_event_id) def to_dict(self) -> dict: result: dict = {} result["sessionId"] = from_str(self.session_id) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) if self.to_event_id is not None: result["toEventId"] = from_union([from_str, from_none], self.to_event_id) return result @@ -1854,15 +1948,21 @@ class SessionsForkResult: session_id: str """The new forked session's ID""" + name: str | None = None + """Friendly name assigned to the forked session, if any.""" + @staticmethod def from_dict(obj: Any) -> 'SessionsForkResult': assert isinstance(obj, dict) session_id = from_str(obj.get("sessionId")) - return SessionsForkResult(session_id) + name = from_union([from_str, from_none], obj.get("name")) + return SessionsForkResult(session_id, name) def to_dict(self) -> dict: result: dict = {} result["sessionId"] = from_str(self.session_id) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) return result @dataclass @@ -2047,6 +2147,39 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillsLoadDiagnostics: + errors: list[str] + """Errors emitted while loading skills (e.g. skills that failed to load entirely)""" + + warnings: list[str] + """Warnings emitted while loading skills (e.g. skills that loaded but had issues)""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillsLoadDiagnostics': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + warnings = from_list(from_str, obj.get("warnings")) + return SkillsLoadDiagnostics(errors, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["warnings"] = from_list(from_str, self.warnings) + return result + +class SlashCommandAgentPromptResultKind(Enum): + AGENT_PROMPT = "agent-prompt" + +class SlashCommandCompletedResultKind(Enum): + COMPLETED = "completed" + +class SlashCommandInvocationResultKind(Enum): + AGENT_PROMPT = "agent-prompt" + COMPLETED = "completed" + TEXT = "text" + class TaskInfoExecutionMode(Enum): """How the agent is currently being managed by the runtime @@ -2767,6 +2900,45 @@ def to_dict(self) -> dict: result["statusMessage"] = from_union([from_str, from_none], self.status_message) return result +@dataclass +class SlashCommandInput: + """Optional unstructured input hint""" + + hint: str + """Hint to display when command input has not been provided""" + + completion: SlashCommandInputCompletion | None = None + """Optional completion hint for the input (e.g. 'directory' for filesystem path completion)""" + + preserve_multiline_input: bool | None = None + """When true, clients should pass the full text after the command name as a single argument + rather than splitting on whitespace + """ + required: bool | None = None + """When true, the command requires non-empty input; clients should render the input hint as + required + """ + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandInput': + assert isinstance(obj, dict) + hint = from_str(obj.get("hint")) + completion = from_union([SlashCommandInputCompletion, from_none], obj.get("completion")) + preserve_multiline_input = from_union([from_bool, from_none], obj.get("preserveMultilineInput")) + required = from_union([from_bool, from_none], obj.get("required")) + return SlashCommandInput(hint, completion, preserve_multiline_input, required) + + def to_dict(self) -> dict: + result: dict = {} + result["hint"] = from_str(self.hint) + if self.completion is not None: + result["completion"] = from_union([lambda x: to_enum(SlashCommandInputCompletion, x), from_none], self.completion) + if self.preserve_multiline_input is not None: + result["preserveMultilineInput"] = from_union([from_bool, from_none], self.preserve_multiline_input) + if self.required is not None: + result["required"] = from_union([from_bool, from_none], self.required) + return result + @dataclass class CommandsRespondToQueuedCommandRequest: request_id: str @@ -3014,20 +3186,61 @@ class ExternalToolTextResultForLlmContentText: text: str """The text content""" - type: ExternalToolTextResultForLlmContentTextType + type: KindEnum """Content block type discriminator""" @staticmethod def from_dict(obj: Any) -> 'ExternalToolTextResultForLlmContentText': assert isinstance(obj, dict) text = from_str(obj.get("text")) - type = ExternalToolTextResultForLlmContentTextType(obj.get("type")) + type = KindEnum(obj.get("type")) return ExternalToolTextResultForLlmContentText(text, type) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) - result["type"] = to_enum(ExternalToolTextResultForLlmContentTextType, self.type) + result["type"] = to_enum(KindEnum, self.type) + return result + +@dataclass +class SlashCommandTextResult: + kind: KindEnum + """Text result discriminator""" + + text: str + """Text output for the client to render""" + + markdown: bool | None = None + """Whether text contains Markdown""" + + preserve_ansi: bool | None = None + """Whether ANSI sequences should be preserved""" + + runtime_settings_changed: bool | None = None + """True when the invocation mutated user runtime settings; consumers caching settings should + refresh + """ + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandTextResult': + assert isinstance(obj, dict) + kind = KindEnum(obj.get("kind")) + text = from_str(obj.get("text")) + markdown = from_union([from_bool, from_none], obj.get("markdown")) + preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) + runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) + return SlashCommandTextResult(kind, text, markdown, preserve_ansi, runtime_settings_changed) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(KindEnum, self.kind) + result["text"] = from_str(self.text) + if self.markdown is not None: + result["markdown"] = from_union([from_bool, from_none], self.markdown) + if self.preserve_ansi is not None: + result["preserveAnsi"] = from_union([from_bool, from_none], self.preserve_ansi) + if self.runtime_settings_changed is not None: + result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -3368,18 +3581,43 @@ def to_dict(self) -> dict: @dataclass class ModeSetRequest: - mode: SessionMode + mode: Mode """The agent mode. Valid values: "interactive", "plan", "autopilot".""" @staticmethod def from_dict(obj: Any) -> 'ModeSetRequest': assert isinstance(obj, dict) - mode = SessionMode(obj.get("mode")) + mode = Mode(obj.get("mode")) return ModeSetRequest(mode) def to_dict(self) -> dict: result: dict = {} - result["mode"] = to_enum(SessionMode, self.mode) + result["mode"] = to_enum(Mode, self.mode) + return result + +@dataclass +class ModelBilling: + """Billing information""" + + multiplier: float | None = None + """Billing cost multiplier relative to the base rate""" + + token_prices: ModelBillingTokenPrices | None = None + """Token-level pricing information for this model""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelBilling': + assert isinstance(obj, dict) + multiplier = from_union([from_float, from_none], obj.get("multiplier")) + token_prices = from_union([ModelBillingTokenPrices.from_dict, from_none], obj.get("tokenPrices")) + return ModelBilling(multiplier, token_prices) + + def to_dict(self) -> dict: + result: dict = {} + if self.multiplier is not None: + result["multiplier"] = from_union([to_float, from_none], self.multiplier) + if self.token_prices is not None: + result["tokenPrices"] = from_union([lambda x: to_class(ModelBillingTokenPrices, x), from_none], self.token_prices) return result @dataclass @@ -4096,6 +4334,145 @@ def to_dict(self) -> dict: result["skills"] = from_list(lambda x: to_class(Skill, x), self.skills) return result +@dataclass +class SlashCommandAgentPromptResult: + display_prompt: str + """Prompt text to display to the user""" + + kind: SlashCommandAgentPromptResultKind + """Agent prompt result discriminator""" + + prompt: str + """Prompt to submit to the agent""" + + mode: Mode | None = None + """Optional target session mode""" + + runtime_settings_changed: bool | None = None + """True when the invocation mutated user runtime settings; consumers caching settings should + refresh + """ + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandAgentPromptResult': + assert isinstance(obj, dict) + display_prompt = from_str(obj.get("displayPrompt")) + kind = SlashCommandAgentPromptResultKind(obj.get("kind")) + prompt = from_str(obj.get("prompt")) + mode = from_union([Mode, from_none], obj.get("mode")) + runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) + return SlashCommandAgentPromptResult(display_prompt, kind, prompt, mode, runtime_settings_changed) + + def to_dict(self) -> dict: + result: dict = {} + result["displayPrompt"] = from_str(self.display_prompt) + result["kind"] = to_enum(SlashCommandAgentPromptResultKind, self.kind) + result["prompt"] = from_str(self.prompt) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode) + if self.runtime_settings_changed is not None: + result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) + return result + +@dataclass +class SlashCommandCompletedResult: + kind: SlashCommandCompletedResultKind + """Completed result discriminator""" + + message: str | None = None + """Optional user-facing message describing the completed command""" + + runtime_settings_changed: bool | None = None + """True when the invocation mutated user runtime settings; consumers caching settings should + refresh + """ + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandCompletedResult': + assert isinstance(obj, dict) + kind = SlashCommandCompletedResultKind(obj.get("kind")) + message = from_union([from_str, from_none], obj.get("message")) + runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) + return SlashCommandCompletedResult(kind, message, runtime_settings_changed) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(SlashCommandCompletedResultKind, self.kind) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.runtime_settings_changed is not None: + result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) + return result + +@dataclass +class SlashCommandInvocationResult: + kind: SlashCommandInvocationResultKind + """Text result discriminator + + Agent prompt result discriminator + + Completed result discriminator + """ + markdown: bool | None = None + """Whether text contains Markdown""" + + preserve_ansi: bool | None = None + """Whether ANSI sequences should be preserved""" + + runtime_settings_changed: bool | None = None + """True when the invocation mutated user runtime settings; consumers caching settings should + refresh + """ + text: str | None = None + """Text output for the client to render""" + + display_prompt: str | None = None + """Prompt text to display to the user""" + + mode: Mode | None = None + """Optional target session mode""" + + prompt: str | None = None + """Prompt to submit to the agent""" + + message: str | None = None + """Optional user-facing message describing the completed command""" + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandInvocationResult': + assert isinstance(obj, dict) + kind = SlashCommandInvocationResultKind(obj.get("kind")) + markdown = from_union([from_bool, from_none], obj.get("markdown")) + preserve_ansi = from_union([from_bool, from_none], obj.get("preserveAnsi")) + runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) + text = from_union([from_str, from_none], obj.get("text")) + display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) + mode = from_union([Mode, from_none], obj.get("mode")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + message = from_union([from_str, from_none], obj.get("message")) + return SlashCommandInvocationResult(kind, markdown, preserve_ansi, runtime_settings_changed, text, display_prompt, mode, prompt, message) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(SlashCommandInvocationResultKind, self.kind) + if self.markdown is not None: + result["markdown"] = from_union([from_bool, from_none], self.markdown) + if self.preserve_ansi is not None: + result["preserveAnsi"] = from_union([from_bool, from_none], self.preserve_ansi) + if self.runtime_settings_changed is not None: + result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) + if self.text is not None: + result["text"] = from_union([from_str, from_none], self.text) + if self.display_prompt is not None: + result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + return result + @dataclass class TaskShellInfo: attachment_mode: TaskShellInfoAttachmentMode @@ -4543,6 +4920,56 @@ def to_dict(self) -> dict: result["user_named"] = from_union([from_bool, from_none], self.user_named) return result +@dataclass +class SlashCommandInfo: + allow_during_agent_execution: bool + """Whether the command may run while an agent turn is active""" + + description: str + """Human-readable command description""" + + kind: SlashCommandKind + """Coarse command category for grouping and behavior: runtime built-in, skill-backed + command, or SDK/client-owned command + """ + name: str + """Canonical command name without a leading slash""" + + aliases: list[str] | None = None + """Canonical aliases without leading slashes""" + + experimental: bool | None = None + """Whether the command is experimental""" + + input: SlashCommandInput | None = None + """Optional unstructured input hint""" + + @staticmethod + def from_dict(obj: Any) -> 'SlashCommandInfo': + assert isinstance(obj, dict) + allow_during_agent_execution = from_bool(obj.get("allowDuringAgentExecution")) + description = from_str(obj.get("description")) + kind = SlashCommandKind(obj.get("kind")) + name = from_str(obj.get("name")) + aliases = from_union([lambda x: from_list(from_str, x), from_none], obj.get("aliases")) + experimental = from_union([from_bool, from_none], obj.get("experimental")) + input = from_union([SlashCommandInput.from_dict, from_none], obj.get("input")) + return SlashCommandInfo(allow_during_agent_execution, description, kind, name, aliases, experimental, input) + + def to_dict(self) -> dict: + result: dict = {} + result["allowDuringAgentExecution"] = from_bool(self.allow_during_agent_execution) + result["description"] = from_str(self.description) + result["kind"] = to_enum(SlashCommandKind, self.kind) + result["name"] = from_str(self.name) + if self.aliases is not None: + result["aliases"] = from_union([lambda x: from_list(from_str, x), from_none], self.aliases) + if self.experimental is not None: + result["experimental"] = from_union([from_bool, from_none], self.experimental) + if self.input is not None: + result["input"] = from_union([lambda x: to_class(SlashCommandInput, x), from_none], self.input) + return result + @dataclass class MCPDiscoverResult: servers: list[DiscoveredMCPServer] @@ -5331,6 +5758,22 @@ def to_dict(self) -> dict: result["workspace"] = from_union([lambda x: to_class(Workspace, x), from_none], self.workspace) return result +@dataclass +class CommandList: + commands: list[SlashCommandInfo] + """Commands available in this session""" + + @staticmethod + def from_dict(obj: Any) -> 'CommandList': + assert isinstance(obj, dict) + commands = from_list(SlashCommandInfo.from_dict, obj.get("commands")) + return CommandList(commands) + + def to_dict(self) -> dict: + result: dict = {} + result["commands"] = from_list(lambda x: to_class(SlashCommandInfo, x), self.commands) + return result + @dataclass class ExternalToolTextResultForLlm: """Expanded external tool result payload""" @@ -5903,8 +6346,11 @@ class RPC: agent_select_request: AgentSelectRequest agent_select_result: AgentSelectResult auth_info_type: AuthInfoType + command_list: CommandList commands_handle_pending_command_request: CommandsHandlePendingCommandRequest commands_handle_pending_command_result: CommandsHandlePendingCommandResult + commands_invoke_request: CommandsInvokeRequest + commands_list_request: CommandsListRequest commands_respond_to_queued_command_request: CommandsRespondToQueuedCommandRequest commands_respond_to_queued_command_result: CommandsRespondToQueuedCommandResult connect_request: ConnectRequest @@ -5974,6 +6420,7 @@ class RPC: mcp_server_status: MCPServerStatus model: Model model_billing: ModelBilling + model_billing_token_prices: ModelBillingTokenPrices model_capabilities: ModelCapabilities model_capabilities_limits: ModelCapabilitiesLimits model_capabilities_limits_vision: ModelCapabilitiesLimitsVision @@ -6061,7 +6508,7 @@ class RPC: session_fs_stat_result: SessionFSStatResult session_fs_write_file_request: SessionFSWriteFileRequest session_log_level: SessionLogLevel - session_mode: SessionMode + session_mode: Mode sessions_fork_request: SessionsForkRequest sessions_fork_result: SessionsForkResult shell_exec_request: ShellExecRequest @@ -6075,6 +6522,16 @@ class RPC: skills_disable_request: SkillsDisableRequest skills_discover_request: SkillsDiscoverRequest skills_enable_request: SkillsEnableRequest + skills_load_diagnostics: SkillsLoadDiagnostics + slash_command_agent_prompt_mode: Mode + slash_command_agent_prompt_result: SlashCommandAgentPromptResult + slash_command_completed_result: SlashCommandCompletedResult + slash_command_info: SlashCommandInfo + slash_command_input: SlashCommandInput + slash_command_input_completion: SlashCommandInputCompletion + slash_command_invocation_result: SlashCommandInvocationResult + slash_command_kind: SlashCommandKind + slash_command_text_result: SlashCommandTextResult task_agent_info: TaskAgentInfo task_agent_info_execution_mode: TaskInfoExecutionMode task_agent_info_status: TaskInfoStatus @@ -6145,8 +6602,11 @@ def from_dict(obj: Any) -> 'RPC': agent_select_request = AgentSelectRequest.from_dict(obj.get("AgentSelectRequest")) agent_select_result = AgentSelectResult.from_dict(obj.get("AgentSelectResult")) auth_info_type = AuthInfoType(obj.get("AuthInfoType")) + command_list = CommandList.from_dict(obj.get("CommandList")) commands_handle_pending_command_request = CommandsHandlePendingCommandRequest.from_dict(obj.get("CommandsHandlePendingCommandRequest")) commands_handle_pending_command_result = CommandsHandlePendingCommandResult.from_dict(obj.get("CommandsHandlePendingCommandResult")) + commands_invoke_request = CommandsInvokeRequest.from_dict(obj.get("CommandsInvokeRequest")) + commands_list_request = CommandsListRequest.from_dict(obj.get("CommandsListRequest")) commands_respond_to_queued_command_request = CommandsRespondToQueuedCommandRequest.from_dict(obj.get("CommandsRespondToQueuedCommandRequest")) commands_respond_to_queued_command_result = CommandsRespondToQueuedCommandResult.from_dict(obj.get("CommandsRespondToQueuedCommandResult")) connect_request = ConnectRequest.from_dict(obj.get("ConnectRequest")) @@ -6216,6 +6676,7 @@ def from_dict(obj: Any) -> 'RPC': mcp_server_status = MCPServerStatus(obj.get("McpServerStatus")) model = Model.from_dict(obj.get("Model")) model_billing = ModelBilling.from_dict(obj.get("ModelBilling")) + model_billing_token_prices = ModelBillingTokenPrices.from_dict(obj.get("ModelBillingTokenPrices")) model_capabilities = ModelCapabilities.from_dict(obj.get("ModelCapabilities")) model_capabilities_limits = ModelCapabilitiesLimits.from_dict(obj.get("ModelCapabilitiesLimits")) model_capabilities_limits_vision = ModelCapabilitiesLimitsVision.from_dict(obj.get("ModelCapabilitiesLimitsVision")) @@ -6303,7 +6764,7 @@ def from_dict(obj: Any) -> 'RPC': session_fs_stat_result = SessionFSStatResult.from_dict(obj.get("SessionFsStatResult")) session_fs_write_file_request = SessionFSWriteFileRequest.from_dict(obj.get("SessionFsWriteFileRequest")) session_log_level = SessionLogLevel(obj.get("SessionLogLevel")) - session_mode = SessionMode(obj.get("SessionMode")) + session_mode = Mode(obj.get("SessionMode")) sessions_fork_request = SessionsForkRequest.from_dict(obj.get("SessionsForkRequest")) sessions_fork_result = SessionsForkResult.from_dict(obj.get("SessionsForkResult")) shell_exec_request = ShellExecRequest.from_dict(obj.get("ShellExecRequest")) @@ -6317,6 +6778,16 @@ def from_dict(obj: Any) -> 'RPC': skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) skills_discover_request = SkillsDiscoverRequest.from_dict(obj.get("SkillsDiscoverRequest")) skills_enable_request = SkillsEnableRequest.from_dict(obj.get("SkillsEnableRequest")) + skills_load_diagnostics = SkillsLoadDiagnostics.from_dict(obj.get("SkillsLoadDiagnostics")) + slash_command_agent_prompt_mode = Mode(obj.get("SlashCommandAgentPromptMode")) + slash_command_agent_prompt_result = SlashCommandAgentPromptResult.from_dict(obj.get("SlashCommandAgentPromptResult")) + slash_command_completed_result = SlashCommandCompletedResult.from_dict(obj.get("SlashCommandCompletedResult")) + slash_command_info = SlashCommandInfo.from_dict(obj.get("SlashCommandInfo")) + slash_command_input = SlashCommandInput.from_dict(obj.get("SlashCommandInput")) + slash_command_input_completion = SlashCommandInputCompletion(obj.get("SlashCommandInputCompletion")) + slash_command_invocation_result = SlashCommandInvocationResult.from_dict(obj.get("SlashCommandInvocationResult")) + slash_command_kind = SlashCommandKind(obj.get("SlashCommandKind")) + slash_command_text_result = SlashCommandTextResult.from_dict(obj.get("SlashCommandTextResult")) task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) task_agent_info_execution_mode = TaskInfoExecutionMode(obj.get("TaskAgentInfoExecutionMode")) task_agent_info_status = TaskInfoStatus(obj.get("TaskAgentInfoStatus")) @@ -6373,7 +6844,7 @@ def from_dict(obj: Any) -> 'RPC': workspaces_list_files_result = WorkspacesListFilesResult.from_dict(obj.get("WorkspacesListFilesResult")) workspaces_read_file_request = WorkspacesReadFileRequest.from_dict(obj.get("WorkspacesReadFileRequest")) workspaces_read_file_result = WorkspacesReadFileResult.from_dict(obj.get("WorkspacesReadFileResult")) - return RPC(account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, auth_info_type, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connect_request, connect_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, embedded_blob_resource_contents, embedded_text_resource_contents, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_oauth_login_request, mcp_oauth_login_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_picker_category, model_picker_price_category, model_policy, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_request_result, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, remote_enable_result, server_skill, server_skill_list, session_auth_status, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, task_agent_info, task_agent_info_execution_mode, task_agent_info_status, task_info, task_list, tasks_cancel_request, tasks_cancel_result, task_shell_info, task_shell_info_attachment_mode, task_shell_info_execution_mode, task_shell_info_status, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, tool, tool_list, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) + return RPC(account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, auth_info_type, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, connect_request, connect_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, embedded_blob_resource_contents, embedded_text_resource_contents, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_oauth_login_request, mcp_oauth_login_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_billing_token_prices, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_picker_category, model_picker_price_category, model_policy, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_request_result, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, queued_command_handled, queued_command_not_handled, queued_command_result, remote_enable_result, server_skill, server_skill_list, session_auth_status, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_load_diagnostics, slash_command_agent_prompt_mode, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_text_result, task_agent_info, task_agent_info_execution_mode, task_agent_info_status, task_info, task_list, tasks_cancel_request, tasks_cancel_result, task_shell_info, task_shell_info_attachment_mode, task_shell_info_execution_mode, task_shell_info_status, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, tool, tool_list, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) def to_dict(self) -> dict: result: dict = {} @@ -6387,8 +6858,11 @@ def to_dict(self) -> dict: result["AgentSelectRequest"] = to_class(AgentSelectRequest, self.agent_select_request) result["AgentSelectResult"] = to_class(AgentSelectResult, self.agent_select_result) result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) + result["CommandList"] = to_class(CommandList, self.command_list) result["CommandsHandlePendingCommandRequest"] = to_class(CommandsHandlePendingCommandRequest, self.commands_handle_pending_command_request) result["CommandsHandlePendingCommandResult"] = to_class(CommandsHandlePendingCommandResult, self.commands_handle_pending_command_result) + result["CommandsInvokeRequest"] = to_class(CommandsInvokeRequest, self.commands_invoke_request) + result["CommandsListRequest"] = to_class(CommandsListRequest, self.commands_list_request) result["CommandsRespondToQueuedCommandRequest"] = to_class(CommandsRespondToQueuedCommandRequest, self.commands_respond_to_queued_command_request) result["CommandsRespondToQueuedCommandResult"] = to_class(CommandsRespondToQueuedCommandResult, self.commands_respond_to_queued_command_result) result["ConnectRequest"] = to_class(ConnectRequest, self.connect_request) @@ -6458,6 +6932,7 @@ def to_dict(self) -> dict: result["McpServerStatus"] = to_enum(MCPServerStatus, self.mcp_server_status) result["Model"] = to_class(Model, self.model) result["ModelBilling"] = to_class(ModelBilling, self.model_billing) + result["ModelBillingTokenPrices"] = to_class(ModelBillingTokenPrices, self.model_billing_token_prices) result["ModelCapabilities"] = to_class(ModelCapabilities, self.model_capabilities) result["ModelCapabilitiesLimits"] = to_class(ModelCapabilitiesLimits, self.model_capabilities_limits) result["ModelCapabilitiesLimitsVision"] = to_class(ModelCapabilitiesLimitsVision, self.model_capabilities_limits_vision) @@ -6545,7 +7020,7 @@ def to_dict(self) -> dict: result["SessionFsStatResult"] = to_class(SessionFSStatResult, self.session_fs_stat_result) result["SessionFsWriteFileRequest"] = to_class(SessionFSWriteFileRequest, self.session_fs_write_file_request) result["SessionLogLevel"] = to_enum(SessionLogLevel, self.session_log_level) - result["SessionMode"] = to_enum(SessionMode, self.session_mode) + result["SessionMode"] = to_enum(Mode, self.session_mode) result["SessionsForkRequest"] = to_class(SessionsForkRequest, self.sessions_fork_request) result["SessionsForkResult"] = to_class(SessionsForkResult, self.sessions_fork_result) result["ShellExecRequest"] = to_class(ShellExecRequest, self.shell_exec_request) @@ -6559,6 +7034,16 @@ def to_dict(self) -> dict: result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) result["SkillsDiscoverRequest"] = to_class(SkillsDiscoverRequest, self.skills_discover_request) result["SkillsEnableRequest"] = to_class(SkillsEnableRequest, self.skills_enable_request) + result["SkillsLoadDiagnostics"] = to_class(SkillsLoadDiagnostics, self.skills_load_diagnostics) + result["SlashCommandAgentPromptMode"] = to_enum(Mode, self.slash_command_agent_prompt_mode) + result["SlashCommandAgentPromptResult"] = to_class(SlashCommandAgentPromptResult, self.slash_command_agent_prompt_result) + result["SlashCommandCompletedResult"] = to_class(SlashCommandCompletedResult, self.slash_command_completed_result) + result["SlashCommandInfo"] = to_class(SlashCommandInfo, self.slash_command_info) + result["SlashCommandInput"] = to_class(SlashCommandInput, self.slash_command_input) + result["SlashCommandInputCompletion"] = to_enum(SlashCommandInputCompletion, self.slash_command_input_completion) + result["SlashCommandInvocationResult"] = to_class(SlashCommandInvocationResult, self.slash_command_invocation_result) + result["SlashCommandKind"] = to_enum(SlashCommandKind, self.slash_command_kind) + result["SlashCommandTextResult"] = to_class(SlashCommandTextResult, self.slash_command_text_result) result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) result["TaskAgentInfoExecutionMode"] = to_enum(TaskInfoExecutionMode, self.task_agent_info_execution_mode) result["TaskAgentInfoStatus"] = to_enum(TaskInfoStatus, self.task_agent_info_status) @@ -6624,6 +7109,17 @@ def rpc_to_dict(x: RPC) -> Any: return to_class(RPC, x) +DiscoveredMcpServerSource = MCPServerSource +ExternalToolResult = ExternalToolTextResultForLlm +FilterMapping = dict +FilterMappingValue = FilterMappingString +SessionMode = Mode +SlashCommandAgentPromptMode = Mode +TaskAgentInfoExecutionMode = TaskInfoExecutionMode +TaskAgentInfoStatus = TaskInfoStatus +TaskShellInfoExecutionMode = TaskInfoExecutionMode +TaskShellInfoStatus = TaskInfoStatus + def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" if timeout is not None: @@ -6773,7 +7269,7 @@ async def ping(self, params: PingRequest, *, timeout: float | None = None) -> Pi class _InternalServerRpc: - """Internal SDK server-scoped RPC methods (handshake helpers etc.). Not part of the public API.""" + """Internal SDK server-scoped RPC methods. Not part of the public API.""" def __init__(self, client: "JsonRpcClient"): self._client = client @@ -6811,8 +7307,8 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def get(self, *, timeout: float | None = None) -> SessionMode: - return SessionMode(await self._client.request("session.mode.get", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def get(self, *, timeout: float | None = None) -> Mode: + return Mode(await self._client.request("session.mode.get", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def set(self, params: ModeSetRequest, *, timeout: float | None = None) -> None: params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -6972,8 +7468,8 @@ async def disable(self, params: SkillsDisableRequest, *, timeout: float | None = params_dict["sessionId"] = self._session_id await self._client.request("session.skills.disable", params_dict, **_timeout_kwargs(timeout)) - async def reload(self, *, timeout: float | None = None) -> None: - await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) + async def reload(self, *, timeout: float | None = None) -> SkillsLoadDiagnostics: + return SkillsLoadDiagnostics.from_dict(await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. @@ -7061,6 +7557,16 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + async def list(self, params: CommandsListRequest | None = None, *, timeout: float | None = None) -> CommandList: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} if params is not None else {} + params_dict["sessionId"] = self._session_id + return CommandList.from_dict(await self._client.request("session.commands.list", params_dict, **_timeout_kwargs(timeout))) + + async def invoke(self, params: CommandsInvokeRequest, *, timeout: float | None = None) -> SlashCommandInvocationResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SlashCommandInvocationResult.from_dict(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) + async def handle_pending_command(self, params: CommandsHandlePendingCommandRequest, *, timeout: float | None = None) -> CommandsHandlePendingCommandResult: params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 4f3e791ce..9d297887b 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -657,6 +657,7 @@ class AssistantUsageData: "LLM API call usage metrics including tokens, costs, quotas, and billing information" model: str api_call_id: str | None = None + api_endpoint: AssistantUsageApiEndpoint | None = None cache_read_tokens: float | None = None cache_write_tokens: float | None = None copilot_usage: AssistantUsageCopilotUsage | None = None @@ -679,6 +680,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": assert isinstance(obj, dict) model = from_str(obj.get("model")) api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) + api_endpoint = from_union([from_none, lambda x: parse_enum(AssistantUsageApiEndpoint, x)], obj.get("apiEndpoint")) cache_read_tokens = from_union([from_none, from_float], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_none, from_float], obj.get("cacheWriteTokens")) copilot_usage = from_union([from_none, AssistantUsageCopilotUsage.from_dict], obj.get("copilotUsage")) @@ -697,6 +699,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": return AssistantUsageData( model=model, api_call_id=api_call_id, + api_endpoint=api_endpoint, cache_read_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens, copilot_usage=copilot_usage, @@ -719,6 +722,8 @@ def to_dict(self) -> dict: result["model"] = from_str(self.model) if self.api_call_id is not None: result["apiCallId"] = from_union([from_none, from_str], self.api_call_id) + if self.api_endpoint is not None: + result["apiEndpoint"] = from_union([from_none, lambda x: to_enum(AssistantUsageApiEndpoint, x)], self.api_endpoint) if self.cache_read_tokens is not None: result["cacheReadTokens"] = from_union([from_none, to_float], self.cache_read_tokens) if self.cache_write_tokens is not None: @@ -4500,6 +4505,7 @@ class UserMessageData: agent_mode: UserMessageAgentMode | None = None attachments: list[UserMessageAttachment] | None = None interaction_id: str | None = None + is_autopilot_continuation: bool | None = None native_document_path_fallback_paths: list[str] | None = None parent_agent_task_id: str | None = None source: str | None = None @@ -4513,6 +4519,7 @@ def from_dict(obj: Any) -> "UserMessageData": agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) attachments = from_union([from_none, lambda x: from_list(UserMessageAttachment.from_dict, x)], obj.get("attachments")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) + is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) parent_agent_task_id = from_union([from_none, from_str], obj.get("parentAgentTaskId")) source = from_union([from_none, from_str], obj.get("source")) @@ -4523,6 +4530,7 @@ def from_dict(obj: Any) -> "UserMessageData": agent_mode=agent_mode, attachments=attachments, interaction_id=interaction_id, + is_autopilot_continuation=is_autopilot_continuation, native_document_path_fallback_paths=native_document_path_fallback_paths, parent_agent_task_id=parent_agent_task_id, source=source, @@ -4539,6 +4547,8 @@ def to_dict(self) -> dict: result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: to_class(UserMessageAttachment, x), x)], self.attachments) if self.interaction_id is not None: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) + if self.is_autopilot_continuation is not None: + result["isAutopilotContinuation"] = from_union([from_none, from_bool], self.is_autopilot_continuation) if self.native_document_path_fallback_paths is not None: result["nativeDocumentPathFallbackPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.native_document_path_fallback_paths) if self.parent_agent_task_id is not None: @@ -4663,6 +4673,14 @@ class AssistantMessageToolRequestType(Enum): CUSTOM = "custom" +class AssistantUsageApiEndpoint(Enum): + "API endpoint used for this model call, matching CAPI supported_endpoints vocabulary" + CHAT_COMPLETIONS = "/chat/completions" + V1_MESSAGES = "/v1/messages" + RESPONSES = "/responses" + WS_RESPONSES = "ws:/responses" + + class ElicitationCompletedAction(Enum): "The user action: \"accept\" (submitted form), \"decline\" (explicitly refused), or \"cancel\" (dismissed)" ACCEPT = "accept" diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py new file mode 100644 index 000000000..5f484add0 --- /dev/null +++ b/python/test_rpc_generated.py @@ -0,0 +1,24 @@ +"""Tests for generated RPC method behavior.""" + +from unittest.mock import AsyncMock + +import pytest + +from copilot.generated.rpc import ( + CommandsApi, + CommandsInvokeRequest, + SlashCommandInvocationResultKind, +) + + +@pytest.mark.asyncio +async def test_commands_invoke_deserializes_slash_command_result(): + client = AsyncMock() + client.request = AsyncMock(return_value={"kind": "text", "text": "hello", "markdown": True}) + api = CommandsApi(client, "sess-1") + + result = await api.invoke(CommandsInvokeRequest(name="help")) + + assert result.kind is SlashCommandInvocationResultKind.TEXT + assert result.text == "hello" + assert result.markdown is True diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 0fcaf1e2f..c6340ff55 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -128,6 +128,10 @@ pub mod rpc_methods { pub const SESSION_EXTENSIONS_RELOAD: &str = "session.extensions.reload"; /// `session.tools.handlePendingToolCall` pub const SESSION_TOOLS_HANDLEPENDINGTOOLCALL: &str = "session.tools.handlePendingToolCall"; + /// `session.commands.list` + pub const SESSION_COMMANDS_LIST: &str = "session.commands.list"; + /// `session.commands.invoke` + pub const SESSION_COMMANDS_INVOKE: &str = "session.commands.invoke"; /// `session.commands.handlePendingCommand` pub const SESSION_COMMANDS_HANDLEPENDINGCOMMAND: &str = "session.commands.handlePendingCommand"; /// `session.commands.respondToQueuedCommand` @@ -269,6 +273,52 @@ pub struct AgentSelectResult { pub agent: AgentInfo, } +/// Optional unstructured input hint +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SlashCommandInput { + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) + #[serde(skip_serializing_if = "Option::is_none")] + pub completion: Option, + /// Hint to display when command input has not been provided + pub hint: String, + /// When true, clients should pass the full text after the command name as a single argument rather than splitting on whitespace + #[serde(skip_serializing_if = "Option::is_none")] + pub preserve_multiline_input: Option, + /// When true, the command requires non-empty input; clients should render the input hint as required + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SlashCommandInfo { + /// Canonical aliases without leading slashes + #[serde(default)] + pub aliases: Vec, + /// Whether the command may run while an agent turn is active + pub allow_during_agent_execution: bool, + /// Human-readable command description + pub description: String, + /// Whether the command is experimental + #[serde(skip_serializing_if = "Option::is_none")] + pub experimental: Option, + /// Optional unstructured input hint + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command + pub kind: SlashCommandKind, + /// Canonical command name without a leading slash + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandList { + /// Commands available in this session + pub commands: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CommandsHandlePendingCommandRequest { @@ -286,6 +336,30 @@ pub struct CommandsHandlePendingCommandResult { pub success: bool, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandsInvokeRequest { + /// Raw input after the command name + #[serde(skip_serializing_if = "Option::is_none")] + pub input: Option, + /// Command name. Leading slashes are stripped and the name is matched case-insensitively. + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CommandsListRequest { + /// Include runtime built-in commands + #[serde(skip_serializing_if = "Option::is_none")] + pub include_builtins: Option, + /// Include commands registered by protocol clients, including SDK clients and extensions + #[serde(skip_serializing_if = "Option::is_none")] + pub include_client_commands: Option, + /// Include enabled user-invocable skills and commands + #[serde(skip_serializing_if = "Option::is_none")] + pub include_skills: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CommandsRespondToQueuedCommandRequest { @@ -839,6 +913,24 @@ pub struct McpServerList { pub servers: Vec, } +/// Token-level pricing information for this model +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelBillingTokenPrices { + /// Number of tokens per standard billing batch + #[serde(skip_serializing_if = "Option::is_none")] + pub batch_size: Option, + /// Price per billing batch of cached tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_price: Option, + /// Price per billing batch of input tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + #[serde(skip_serializing_if = "Option::is_none")] + pub input_price: Option, + /// Price per billing batch of output tokens in nano-AIUs (1 nano-AIU = 0.000000001 AIU, 1 AIU = $0.01 USD) + #[serde(skip_serializing_if = "Option::is_none")] + pub output_price: Option, +} + /// Billing information #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -846,6 +938,9 @@ pub struct ModelBilling { /// Billing cost multiplier relative to the base rate #[serde(skip_serializing_if = "Option::is_none")] pub multiplier: Option, + /// Token-level pricing information for this model + #[serde(skip_serializing_if = "Option::is_none")] + pub token_prices: Option, } /// Vision-specific limits @@ -1608,6 +1703,9 @@ pub struct SessionFsWriteFileRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionsForkRequest { + /// Optional friendly name to assign to the forked session. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, /// Source session ID to fork from pub session_id: SessionId, /// Optional event ID boundary. When provided, the fork includes only events before this ID (exclusive). When omitted, all events are included. @@ -1618,6 +1716,9 @@ pub struct SessionsForkRequest { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionsForkResult { + /// Friendly name assigned to the forked session, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, /// The new forked session's ID pub session_id: SessionId, } @@ -1716,6 +1817,63 @@ pub struct SkillsEnableRequest { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillsLoadDiagnostics { + /// Errors emitted while loading skills (e.g. skills that failed to load entirely) + pub errors: Vec, + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues) + pub warnings: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SlashCommandAgentPromptResult { + /// Prompt text to display to the user + pub display_prompt: String, + /// Agent prompt result discriminator + pub kind: SlashCommandAgentPromptResultKind, + /// Optional target session mode + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + /// Prompt to submit to the agent + pub prompt: String, + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime_settings_changed: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SlashCommandCompletedResult { + /// Completed result discriminator + pub kind: SlashCommandCompletedResultKind, + /// Optional user-facing message describing the completed command + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime_settings_changed: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SlashCommandTextResult { + /// Text result discriminator + pub kind: SlashCommandTextResultKind, + /// Whether text contains Markdown + #[serde(skip_serializing_if = "Option::is_none")] + pub markdown: Option, + /// Whether ANSI sequences should be preserved + #[serde(skip_serializing_if = "Option::is_none")] + pub preserve_ansi: Option, + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime_settings_changed: Option, + /// Text output for the client to render + pub text: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TaskAgentInfo { @@ -2644,6 +2802,15 @@ pub struct SessionSkillsReloadParams { pub session_id: SessionId, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionSkillsReloadResult { + /// Errors emitted while loading skills (e.g. skills that failed to load entirely) + pub errors: Vec, + /// Warnings emitted while loading skills (e.g. skills that loaded but had issues) + pub warnings: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionMcpListParams { @@ -2715,6 +2882,13 @@ pub struct SessionToolsHandlePendingToolCallResult { pub success: bool, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCommandsListResult { + /// Commands available in this session + pub commands: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCommandsHandlePendingCommandResult { @@ -2900,6 +3074,30 @@ pub enum AuthInfoType { Unknown, } +/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandInputCompletion { + #[serde(rename = "directory")] + Directory, + /// Unknown variant for forward compatibility. + #[serde(other)] + Unknown, +} + +/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandKind { + #[serde(rename = "builtin")] + Builtin, + #[serde(rename = "skill")] + Skill, + #[serde(rename = "client")] + Client, + /// Unknown variant for forward compatibility. + #[serde(other)] + Unknown, +} + /// Configuration source #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DiscoveredMcpServerSource { @@ -3440,6 +3638,49 @@ pub enum ShellKillSignal { Unknown, } +/// Optional target session mode +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandAgentPromptMode { + #[serde(rename = "interactive")] + Interactive, + #[serde(rename = "plan")] + Plan, + #[serde(rename = "autopilot")] + Autopilot, + /// Unknown variant for forward compatibility. + #[serde(other)] + Unknown, +} + +/// Agent prompt result discriminator +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandAgentPromptResultKind { + #[serde(rename = "agent-prompt")] + AgentPrompt, +} + +/// Completed result discriminator +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandCompletedResultKind { + #[serde(rename = "completed")] + Completed, +} + +/// Text result discriminator +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SlashCommandTextResultKind { + #[serde(rename = "text")] + Text, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SlashCommandInvocationResult { + Text(SlashCommandTextResult), + AgentPrompt(SlashCommandAgentPromptResult), + Completed(SlashCommandCompletedResult), +} + /// How the agent is currently being managed by the runtime #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TaskAgentInfoExecutionMode { diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 5a0b48434..cb1393483 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -105,6 +105,19 @@ impl<'a> ClientRpcAccount<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Wire method: `account.getQuota`. + pub async fn get_quota_with_params( + &self, + params: AccountGetQuotaRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::ACCOUNT_GETQUOTA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `mcp.*` RPCs. @@ -216,6 +229,16 @@ impl<'a> ClientRpcModels<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Wire method: `models.list`. + pub async fn list_with_params(&self, params: ModelsListRequest) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::MODELS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `sessionFs.*` RPCs. @@ -647,6 +670,47 @@ pub struct SessionRpcCommands<'a> { } impl<'a> SessionRpcCommands<'a> { + /// Wire method: `session.commands.list`. + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_COMMANDS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Wire method: `session.commands.list`. + pub async fn list_with_params( + &self, + params: CommandsListRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_COMMANDS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Wire method: `session.commands.invoke`. + pub async fn invoke( + &self, + params: CommandsInvokeRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_COMMANDS_INVOKE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Wire method: `session.commands.handlePendingCommand`. pub async fn handle_pending_command( &self, @@ -1369,14 +1433,14 @@ impl<'a> SessionRpcSkills<'a> { /// SDK and CLI versions if your code depends on it. /// /// - pub async fn reload(&self) -> Result<(), Error> { + pub async fn reload(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() .call(rpc_methods::SESSION_SKILLS_RELOAD, Some(wire_params)) .await?; - Ok(()) + Ok(serde_json::from_value(_value)?) } } diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 85c523940..37ac4fbe1 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -988,6 +988,9 @@ pub struct UserMessageData { /// CAPI interaction ID for correlating this user message with its turn #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, + /// True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_autopilot_continuation: Option, /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload would exceed the request size limit #[serde(default)] pub native_document_path_fallback_paths: Vec, @@ -1221,6 +1224,9 @@ pub struct AssistantUsageData { /// Completion ID from the model provider (e.g., chatcmpl-abc123) #[serde(skip_serializing_if = "Option::is_none")] pub api_call_id: Option, + /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary + #[serde(skip_serializing_if = "Option::is_none")] + pub api_endpoint: Option, /// Number of tokens read from prompt cache #[serde(skip_serializing_if = "Option::is_none")] pub cache_read_tokens: Option, @@ -2739,6 +2745,22 @@ pub enum AssistantMessageToolRequestType { Unknown, } +/// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum AssistantUsageApiEndpoint { + #[serde(rename = "/chat/completions")] + ChatCompletions, + #[serde(rename = "/v1/messages")] + V1Messages, + #[serde(rename = "/responses")] + Responses, + #[serde(rename = "ws:/responses")] + WsResponses, + /// Unknown variant for forward compatibility. + #[serde(other)] + Unknown, +} + /// Where the failed model call originated #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ModelCallFailureSource { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 4520dd5e3..d3eaa9e92 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -147,9 +147,10 @@ pub enum PermissionResult { /// `{ "kind": "user-not-available" }`. UserNotAvailable, /// The handler has no result to provide and the CLI should fall back - /// to its default policy. Sent as `{ "kind": "no-result" }`. Distinct - /// from [`Deferred`](Self::Deferred), which suppresses the reply - /// entirely so the handler can resolve later out-of-band. + /// to another permission responder or its default policy. On the + /// notification path, the SDK will not send a pending permission response. + /// Distinct from [`Deferred`](Self::Deferred), where the handler takes + /// responsibility for resolving the request later out-of-band. NoResult, } diff --git a/rust/src/session.rs b/rust/src/session.rs index 2cdb257eb..9485fe219 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1129,12 +1129,12 @@ fn permission_request_response(response: &HandlerResponse) -> PermissionDecision /// Map a handler response into the `result` payload for the notification /// path (`session.permissions.handlePendingPermissionRequest`). /// -/// Returns `None` when the SDK must not respond — currently only the -/// [`PermissionResult::Deferred`] case, where the handler takes over -/// responsibility for the round-trip itself. +/// Returns `None` when the SDK must not respond. fn notification_permission_payload(response: &HandlerResponse) -> Option { match response { - HandlerResponse::Permission(PermissionResult::Deferred) => None, + HandlerResponse::Permission(PermissionResult::Deferred | PermissionResult::NoResult) => { + None + } HandlerResponse::Permission(PermissionResult::Custom(value)) => Some(value.clone()), _ => Some(serde_json::json!({ "kind": pending_permission_result_kind(response), @@ -2116,14 +2116,20 @@ mod tests { } #[test] - fn notification_payload_handles_deferred_and_custom() { - // Deferred → no payload, SDK must not respond. + fn notification_payload_handles_non_responses_and_custom() { + // Deferred/NoResult -> no payload, SDK must not respond. assert!( notification_permission_payload(&HandlerResponse::Permission( PermissionResult::Deferred, )) .is_none() ); + assert!( + notification_permission_payload(&HandlerResponse::Permission( + PermissionResult::NoResult, + )) + .is_none() + ); // Custom → handler-supplied value passed through verbatim. let custom = json!({ diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index 8a8ae5c18..8d91a7731 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -641,6 +641,7 @@ async fn should_fork_session_with_persisted_messages() { .rpc() .sessions() .fork(SessionsForkRequest { + name: None, session_id: session.id().clone(), to_event_id: None, }) @@ -706,6 +707,7 @@ async fn should_handle_forking_session_without_persisted_events() { .rpc() .sessions() .fork(SessionsForkRequest { + name: None, session_id: session.id().clone(), to_event_id: None, }) @@ -799,6 +801,7 @@ async fn should_fork_session_to_event_id_excluding_boundary_event() { .rpc() .sessions() .fork(SessionsForkRequest { + name: None, session_id: session.id().clone(), to_event_id: Some(boundary_id.clone()), }) @@ -858,6 +861,7 @@ async fn should_report_error_when_forking_session_to_unknown_event_id() { .rpc() .sessions() .fork(SessionsForkRequest { + name: None, session_id: session.id().clone(), to_event_id: Some(bogus_event_id.to_string()), }) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index a1401e1a5..f66043170 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -1266,19 +1266,14 @@ function emitRpcClass( return lines.join("\n"); } -/** - * Emit the type for a non-object RPC result schema (e.g., a bare enum). - * Returns the C# type name to use in method signatures. For enums, ensures the enum - * is created via getOrCreateEnum. For other primitives, returns the mapped C# type. - */ -function emitNonObjectResultType(typeName: string, schema: JSONSchema7, classes: string[]): string { - if (schema.enum && Array.isArray(schema.enum)) { - const enumName = getOrCreateEnum("", typeName, schema.enum as string[], rpcEnumOutput, schema.description, typeName, isSchemaDeprecated(schema), isSchemaExperimental(schema)); - emittedRpcEnumResultTypes.add(enumName); - return enumName; +function emitRpcResultType(typeName: string, schema: JSONSchema7, visibility: "public" | "internal", classes: string[]): string { + if (isObjectSchema(schema)) { + const resultClass = emitRpcClass(typeName, schema, visibility, classes); + if (resultClass) classes.push(resultClass); + return typeName; } - // For other non-object types, use the basic type mapping - return schemaTypeToCSharp(schema, true, rpcKnownTypes); + + return resolveRpcType(schema, true, typeName, "", classes); } /** @@ -1399,11 +1394,8 @@ function emitServerInstanceMethod( if (!isVoidSchema(resultSchema) && method.stability === "experimental") { experimentalRpcTypes.add(resultClassName); } - if (isObjectSchema(resultSchema)) { - const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes); - if (resultClass) classes.push(resultClass); - } else if (!isVoidSchema(resultSchema)) { - resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes); + if (!isVoidSchema(resultSchema)) { + resultClassName = emitRpcResultType(resultClassName, resultSchema!, methodVisibility, classes); } const effectiveParams = resolveMethodParamsSchema(method); @@ -1507,16 +1499,17 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas if (!isVoidSchema(resultSchema) && method.stability === "experimental") { experimentalRpcTypes.add(resultClassName); } - if (isObjectSchema(resultSchema)) { - const resultClass = emitRpcClass(resultClassName, resultSchema!, methodVisibility, classes); - if (resultClass) classes.push(resultClass); - } else if (!isVoidSchema(resultSchema)) { - resultClassName = emitNonObjectResultType(resultClassName, resultSchema!, classes); + if (!isVoidSchema(resultSchema)) { + resultClassName = emitRpcResultType(resultClassName, resultSchema!, methodVisibility, classes); } const effectiveParams = resolveMethodParamsSchema(method); const paramEntries = (effectiveParams?.properties ? Object.entries(effectiveParams.properties) : []).filter(([k]) => k !== "sessionId"); const requiredSet = new Set(effectiveParams?.required || []); + const useRequestParameter = + paramEntries.length > 0 && + !!getNullableInner(method.params) && + paramEntries.every(([name]) => !requiredSet.has(name)); // Sort so required params come before optional (C# requires defaults at end) paramEntries.sort((a, b) => { @@ -1526,12 +1519,28 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas }); const requestClassName = paramsTypeName(method); + const wireRequestClassName = useRequestParameter ? `${requestClassName}WithSession` : requestClassName; if (method.stability === "experimental") { experimentalRpcTypes.add(requestClassName); + if (useRequestParameter) { + experimentalRpcTypes.add(wireRequestClassName); + } } if (effectiveParams?.properties && Object.keys(effectiveParams.properties).length > 0) { - const reqClass = emitRpcClass(requestClassName, effectiveParams, "internal", classes); - if (reqClass) classes.push(reqClass); + if (useRequestParameter) { + const publicParams: JSONSchema7 = { + ...effectiveParams, + properties: Object.fromEntries(paramEntries), + required: effectiveParams.required?.filter((name) => name !== "sessionId"), + }; + const publicReqClass = emitRpcClass(requestClassName, publicParams, methodVisibility, classes); + if (publicReqClass) classes.push(publicReqClass); + const wireReqClass = emitRpcClass(wireRequestClassName, effectiveParams, "internal", classes); + if (wireReqClass) classes.push(wireReqClass); + } else { + const reqClass = emitRpcClass(requestClassName, effectiveParams, "internal", classes); + if (reqClass) classes.push(reqClass); + } } lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); @@ -1544,22 +1553,30 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas const sigParams: string[] = []; const bodyAssignments = [`SessionId = _sessionId`]; - for (const [pName, pSchema] of paramEntries) { - if (typeof pSchema !== "object") continue; - const isReq = requiredSet.has(pName); - const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); - sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); + if (useRequestParameter) { + sigParams.push(`${requestClassName}? request = null`); + for (const [pName] of paramEntries) { + bodyAssignments.push(`${toPascalCase(pName)} = request?.${toPascalCase(pName)}`); + } + } else { + for (const [pName, pSchema] of paramEntries) { + if (typeof pSchema !== "object") continue; + const isReq = requiredSet.has(pName); + const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); + sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); + bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); + } } sigParams.push("CancellationToken cancellationToken = default"); const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task"; + const localRequestName = useRequestParameter ? "rpcRequest" : "request"; lines.push(`${indent}${methodVisibility} async ${taskType} ${methodName}Async(${sigParams.join(", ")})`); - lines.push(`${indent}{`, `${indent} var request = new ${requestClassName} { ${bodyAssignments.join(", ")} };`); + lines.push(`${indent}{`, `${indent} var ${localRequestName} = new ${wireRequestClassName} { ${bodyAssignments.join(", ")} };`); if (!isVoidSchema(resultSchema)) { - lines.push(`${indent} return await CopilotClient.InvokeRpcAsync<${resultClassName}>(_rpc, "${method.rpcMethod}", [request], cancellationToken);`, `${indent}}`); + lines.push(`${indent} return await CopilotClient.InvokeRpcAsync<${resultClassName}>(_rpc, "${method.rpcMethod}", [${localRequestName}], cancellationToken);`, `${indent}}`); } else { - lines.push(`${indent} await CopilotClient.InvokeRpcAsync(_rpc, "${method.rpcMethod}", [request], cancellationToken);`, `${indent}}`); + lines.push(`${indent} await CopilotClient.InvokeRpcAsync(_rpc, "${method.rpcMethod}", [${localRequestName}], cancellationToken);`, `${indent}}`); } } @@ -1634,12 +1651,7 @@ function emitClientSessionApiRegistration(clientSchema: Record, for (const method of methods) { const resultSchema = getMethodResultSchema(method); if (!isVoidSchema(resultSchema)) { - if (isObjectSchema(resultSchema)) { - const resultClass = emitRpcClass(resultTypeName(method), resultSchema!, "public", classes); - if (resultClass) classes.push(resultClass); - } else { - emitNonObjectResultType(resultTypeName(method), resultSchema!, classes); - } + emitRpcResultType(resultTypeName(method), resultSchema!, "public", classes); } const effectiveParams = resolveMethodParamsSchema(method); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index b1a8fb080..0f251e626 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -2543,6 +2543,7 @@ function emitGoRpcDefinition(definitionName: string, schema: JSONSchema7, ctx: G interface GoGeneratedTypeCode { typeCode: string; encodingCode: string; + discriminatedUnions: Map; } function stripTrailingGoWhitespace(code: string): string { @@ -2637,6 +2638,7 @@ function generateGoRpcTypeCode(definitions: Record, definit return { typeCode: joinGoCode(lines), encodingCode: goEncodingBlocksCode(ctx.encoding), + discriminatedUnions: new Map(ctx.discriminatedUnions), }; } @@ -2965,6 +2967,7 @@ function generateGoSessionEventsCode(schema: JSONSchema7): GoGeneratedTypeCode { return { typeCode: joinGoCode(out), encodingCode: joinGoCode(encodingOut), + discriminatedUnions: new Map(ctx.discriminatedUnions), }; } @@ -3168,21 +3171,21 @@ async function generateRpc(schemaPath?: string): Promise { // Emit ServerRpc if (schema.server) { const publicNode = filterNodeByVisibility(schema.server, "public"); - if (publicNode) emitRpcWrapper(lines, publicNode, false, resolveType, fields, ""); + if (publicNode) emitRpcWrapper(lines, publicNode, false, resolveType, fields, generatedRpcCode.discriminatedUnions, ""); const internalNode = filterNodeByVisibility(schema.server, "internal"); - if (internalNode) emitRpcWrapper(lines, internalNode, false, resolveType, fields, "Internal"); + if (internalNode) emitRpcWrapper(lines, internalNode, false, resolveType, fields, generatedRpcCode.discriminatedUnions, "Internal"); } // Emit SessionRpc if (schema.session) { const publicNode = filterNodeByVisibility(schema.session, "public"); - if (publicNode) emitRpcWrapper(lines, publicNode, true, resolveType, fields, ""); + if (publicNode) emitRpcWrapper(lines, publicNode, true, resolveType, fields, generatedRpcCode.discriminatedUnions, ""); const internalNode = filterNodeByVisibility(schema.session, "internal"); - if (internalNode) emitRpcWrapper(lines, internalNode, true, resolveType, fields, "Internal"); + if (internalNode) emitRpcWrapper(lines, internalNode, true, resolveType, fields, generatedRpcCode.discriminatedUnions, "Internal"); } if (schema.clientSession) { - emitClientSessionApiRegistration(lines, schema.clientSession, resolveType); + emitClientSessionApiRegistration(lines, schema.clientSession, resolveType, generatedRpcCode.discriminatedUnions); } const outPath = await writeGeneratedFile("go/rpc/zrpc.go", wrapGeneratedGoComments(lines.join("\n"))); @@ -3204,6 +3207,7 @@ function emitApiGroup( serviceName: string, resolveType: (name: string) => string, fields: Map>, + unionInfos: Map, groupExperimental: boolean, groupDeprecated: boolean = false ): void { @@ -3221,14 +3225,14 @@ function emitApiGroup( for (const [key, value] of methods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, apiName, key, value, isSession, resolveType, fields, groupExperimental, false, groupDeprecated); + emitMethod(lines, apiName, key, value, isSession, resolveType, fields, unionInfos, groupExperimental, false, groupDeprecated); } for (const [subGroupName, subGroupNode] of subGroups) { const subApiName = apiName.replace(/Api$/, "") + toPascalCase(subGroupName) + "Api"; const subGroupExperimental = isNodeFullyExperimental(subGroupNode as Record); const subGroupDeprecated = isNodeFullyDeprecated(subGroupNode as Record); - emitApiGroup(lines, subApiName, subGroupNode as Record, isSession, serviceName, resolveType, fields, subGroupExperimental, subGroupDeprecated); + emitApiGroup(lines, subApiName, subGroupNode as Record, isSession, serviceName, resolveType, fields, unionInfos, subGroupExperimental, subGroupDeprecated); if (subGroupExperimental) { pushGoExperimentalSubApiComment(lines, toPascalCase(subGroupName)); @@ -3240,7 +3244,7 @@ function emitApiGroup( } } -function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string, fields: Map>, classPrefix: string = ""): void { +function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string, fields: Map>, unionInfos: Map, classPrefix: string = ""): void { const groups = sortByPascalName(Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v))); const topLevelMethods = sortByPascalName(Object.entries(node).filter(([, v]) => isRpcMethod(v))); @@ -3265,7 +3269,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio const apiName = prefix + toPascalCase(groupName) + apiSuffix; const groupExperimental = isNodeFullyExperimental(groupNode as Record); const groupDeprecated = isNodeFullyDeprecated(groupNode as Record); - emitApiGroup(lines, apiName, groupNode as Record, isSession, serviceName, resolveType, fields, groupExperimental, groupDeprecated); + emitApiGroup(lines, apiName, groupNode as Record, isSession, serviceName, resolveType, fields, unionInfos, groupExperimental, groupDeprecated); } // Compute field name lengths for gofmt-compatible column alignment @@ -3295,7 +3299,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods on the wrapper use the common service fields for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, wrapperName, key, value, isSession, resolveType, fields, false, true); + emitMethod(lines, wrapperName, key, value, isSession, resolveType, fields, unionInfos, false, true); } // Constructor @@ -3316,13 +3320,15 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); } -function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, fields: Map>, groupExperimental = false, isWrapper = false, groupDeprecated = false): void { +function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, fields: Map>, unionInfos: Map, groupExperimental = false, isWrapper = false, groupDeprecated = false): void { const methodName = toPascalCase(name); const resultSchema = getMethodResultSchema(method); const nullableInner = resultSchema ? getNullableInner(resultSchema) : undefined; const resultType = nullableInner ? resolveType(goNullableResultTypeName(method, nullableInner)) : resolveType(goResultTypeName(method)); + const resultUnion = unionInfos.get(resultType); + const returnType = resultUnion ? resultType : `*${resultType}`; const effectiveParams = getMethodParamsSchema(method); const paramProps = effectiveParams?.properties || {}; @@ -3332,6 +3338,8 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc .sort((left, right) => compareGoFieldNames(toGoFieldName(left), toGoFieldName(right))); const hasParams = isSession ? nonSessionParams.length > 0 : hasSchemaPayload(effectiveParams); const paramsType = hasParams ? resolveType(goParamsTypeName(method)) : ""; + const hasRequiredNonSessionParams = nonSessionParams.some((name) => requiredParams.has(name)); + const paramsAreOptional = hasParams && !!method.params && !!getNullableInner(method.params) && !hasRequiredNonSessionParams; // For wrapper-level methods, access fields through a.common; for service type aliases, use a directly const clientRef = isWrapper ? "a.common.client" : "a.client"; @@ -3347,15 +3355,22 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc pushGoComment(lines, `Internal: ${methodName} is part of the SDK's internal handshake/plumbing; external callers should not use it.`); } const sig = hasParams - ? `func (a *${receiver}) ${methodName}(ctx context.Context, params *${paramsType}) (*${resultType}, error)` - : `func (a *${receiver}) ${methodName}(ctx context.Context) (*${resultType}, error)`; + ? `func (a *${receiver}) ${methodName}(ctx context.Context, params ${paramsAreOptional ? "..." : ""}*${paramsType}) (${returnType}, error)` + : `func (a *${receiver}) ${methodName}(ctx context.Context) (${returnType}, error)`; lines.push(sig + ` {`); + const paramsRef = paramsAreOptional ? "requestParams" : "params"; + if (paramsAreOptional) { + lines.push(`\tvar requestParams *${paramsType}`); + lines.push(`\tif len(params) > 0 {`); + lines.push(`\t\trequestParams = params[0]`); + lines.push(`\t}`); + } if (isSession) { lines.push(`\treq := map[string]any{"sessionId": ${sessionIDRef}}`); if (hasParams) { - lines.push(`\tif params != nil {`); + lines.push(`\tif ${paramsRef} != nil {`); for (const pName of nonSessionParams) { const field = fields.get(paramsType)?.get(pName); const goField = field?.name ?? toGoFieldName(pName); @@ -3364,30 +3379,38 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc if (isOptional) { // Optional fields are usually pointers; generated union interfaces, slices, // and maps are nilable values and should be passed through directly. - lines.push(`\t\tif params.${goField} != nil {`); - const valueExpr = goOptionalFieldNeedsDereference(goType) ? `*params.${goField}` : `params.${goField}`; + lines.push(`\t\tif ${paramsRef}.${goField} != nil {`); + const valueExpr = goOptionalFieldNeedsDereference(goType) ? `*${paramsRef}.${goField}` : `${paramsRef}.${goField}`; lines.push(`\t\t\treq["${pName}"] = ${valueExpr}`); lines.push(`\t\t}`); } else { - lines.push(`\t\treq["${pName}"] = params.${goField}`); + lines.push(`\t\treq["${pName}"] = ${paramsRef}.${goField}`); } } lines.push(`\t}`); } lines.push(`\traw, err := ${clientRef}.Request("${method.rpcMethod}", req)`); } else { - const arg = hasParams ? "params" : "nil"; + const arg = hasParams ? paramsRef : "nil"; lines.push(`\traw, err := ${clientRef}.Request("${method.rpcMethod}", ${arg})`); } lines.push(`\tif err != nil {`); lines.push(`\t\treturn nil, err`); lines.push(`\t}`); - lines.push(`\tvar result ${resultType}`); - lines.push(`\tif err := json.Unmarshal(raw, &result); err != nil {`); - lines.push(`\t\treturn nil, err`); - lines.push(`\t}`); - lines.push(`\treturn &result, nil`); + if (resultUnion) { + lines.push(`\tresult, err := ${resultUnion.unmarshalFuncName}(raw)`); + lines.push(`\tif err != nil {`); + lines.push(`\t\treturn nil, err`); + lines.push(`\t}`); + lines.push(`\treturn result, nil`); + } else { + lines.push(`\tvar result ${resultType}`); + lines.push(`\tif err := json.Unmarshal(raw, &result); err != nil {`); + lines.push(`\t\treturn nil, err`); + lines.push(`\t}`); + lines.push(`\treturn &result, nil`); + } lines.push(`}`); lines.push(``); } @@ -3420,7 +3443,7 @@ function clientHandlerMethodName(rpcMethod: string): string { return toPascalCase(rpcMethod.split(".").at(-1)!); } -function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string): void { +function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { const groups = collectClientGroups(clientSchema); for (const { groupName, groupNode, methods } of groups) { @@ -3447,7 +3470,8 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< const resultType = nullableInner ? resolveType(goNullableResultTypeName(method, nullableInner)) : resolveType(goResultTypeName(method)); - lines.push(`\t${clientHandlerMethodName(method.rpcMethod)}(request *${paramsType}) (*${resultType}, error)`); + const returnType = unionInfos.has(resultType) ? resultType : `*${resultType}`; + lines.push(`\t${clientHandlerMethodName(method.rpcMethod)}(request *${paramsType}) (${returnType}, error)`); } lines.push(`}`); lines.push(``); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 30aff56bd..6c0a4b572 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -445,6 +445,33 @@ function getMethodResultSchema(method: RpcMethod): JSONSchema7 | undefined { return resolveSchema(method.result, rpcDefinitions) ?? method.result ?? undefined; } +function isPythonObjectResultSchema(schema: JSONSchema7 | undefined): boolean { + if (!schema) return false; + if (isObjectSchema(schema)) return true; + + const variants = schema.anyOf ?? schema.oneOf; + if (!Array.isArray(variants)) return false; + + const nonNullVariants = variants + .filter((variant): variant is JSONSchema7 => typeof variant === "object" && variant !== null) + .map((variant) => resolveObjectSchema(variant, rpcDefinitions) ?? resolveSchema(variant, rpcDefinitions) ?? variant) + .filter( + (variant) => + variant.type !== "null" && + !( + typeof variant.not === "object" && + variant.not !== null && + Object.keys(variant.not).length === 0 + ) + ); + + if (nonNullVariants.length === 1) { + return isPythonObjectResultSchema(nonNullVariants[0]); + } + + return nonNullVariants.length > 1 && findPyDiscriminator(nonNullVariants) !== null; +} + function getMethodParamsSchema(method: RpcMethod): JSONSchema7 | undefined { return ( resolveObjectSchema(method.params, rpcDefinitions) ?? @@ -1849,7 +1876,32 @@ async function generateRpc(schemaPath?: string): Promise { while ((cm = classRe.exec(typesCode)) !== null) { actualTypeNames.set(cm[1].toLowerCase(), cm[1]); } - const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name; + + // quicktype can also choose a shorter generated class name for a titled schema + // definition. Its root RPC dataclass still records the definition field and + // generated class mapping, so use that as an alias table for RPC wrappers. + const definitionAliases = new Map(); + const publicTypeAliases = new Map(); + const rootFields = typesCode.match(/^class RPC:\n([\s\S]*?)\n @staticmethod/m)?.[1] ?? ""; + const rootFieldTypes = new Map(); + for (const line of rootFields.split(/\r?\n/)) { + const match = line.match(/^ ([A-Za-z_]\w*): ([A-Za-z_]\w*)\b/); + if (match) { + rootFieldTypes.set(match[1], match[2]); + } + } + for (const defName of Object.keys(allDefinitions)) { + const actualName = rootFieldTypes.get(toSnakeCase(defName)); + if (actualName) { + definitionAliases.set(defName.toLowerCase(), actualName); + if (actualName !== defName && !actualTypeNames.has(defName.toLowerCase()) && /^[A-Za-z_]\w*$/.test(defName)) { + publicTypeAliases.set(defName, actualName); + } + } + } + + const resolveType = (name: string): string => + actualTypeNames.get(name.toLowerCase()) ?? definitionAliases.get(name.toLowerCase()) ?? name; const lines: string[] = []; lines.push(`""" @@ -1876,6 +1928,14 @@ EnumT = TypeVar("EnumT", bound=Enum) `); lines.push(typesCode); + if (publicTypeAliases.size > 0) { + lines.push(""); + for (const [aliasName, targetName] of [...publicTypeAliases.entries()].sort(([left], [right]) => + left.localeCompare(right), + )) { + lines.push(`${aliasName} = ${targetName}`); + } + } lines.push(` def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" @@ -2024,7 +2084,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio } else { lines.push(`class ${wrapperName}:`); lines.push(classPrefix === "_Internal" - ? ` """Internal SDK server-scoped RPC methods (handshake helpers etc.). Not part of the public API."""` + ? ` """Internal SDK server-scoped RPC methods. Not part of the public API."""` : ` """Typed server-scoped RPC methods."""`); lines.push(` def __init__(self, client: "JsonRpcClient"):`); lines.push(` self._client = client`); @@ -2049,7 +2109,7 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: const effectiveResultSchema = nullableInner ?? resultSchema; const hasResult = !isVoidSchema(resultSchema) && !nullableInner; const hasNullableResult = !!nullableInner; - const resultIsObject = isObjectSchema(effectiveResultSchema); + const resultIsObject = isPythonObjectResultSchema(effectiveResultSchema); let resultType: string; if (hasNullableResult) { diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index b21a889e4..15c8006ee 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -24,6 +24,7 @@ import { collectDefinitionCollections, collectDefinitions, getApiSchemaPath, + getNullableInner, getRpcSchemaTypeName, getSessionEventsSchemaPath, isObjectSchema, @@ -1116,11 +1117,12 @@ function getMethodParamsInfo( method: RpcMethod, defCollections: DefinitionCollections, isSession: boolean, -): { hasParams: boolean; typeName: string | null } { - if (!method.params) return { hasParams: false, typeName: null }; +): { hasParams: boolean; optional: boolean; typeName: string | null } { + if (!method.params) return { hasParams: false, optional: false, typeName: null }; const inline = method.params as JSONSchema7 & { $ref?: string }; - const resolved = resolveSchema(inline, defCollections); - if (!resolved) return { hasParams: false, typeName: null }; + const resolved = resolveObjectSchema(inline, defCollections) ?? + resolveSchema(inline, defCollections); + if (!resolved) return { hasParams: false, optional: false, typeName: null }; let typeName: string | null = null; if (typeof inline.$ref === "string") { @@ -1135,9 +1137,12 @@ function getMethodParamsInfo( const props = isSession ? allProps.filter((p) => p !== "sessionId") : allProps; - if (props.length === 0) return { hasParams: false, typeName: null }; - if (!typeName) return { hasParams: false, typeName: null }; - return { hasParams: true, typeName }; + if (props.length === 0) return { hasParams: false, optional: false, typeName: null }; + if (!typeName) return { hasParams: false, optional: false, typeName: null }; + const required = new Set(resolved.required || []); + const hasRequiredParams = props.some((p) => required.has(p)); + const optional = !!getNullableInner(inline) && !hasRequiredParams; + return { hasParams: true, optional, typeName }; } function rpcMethodConstName(method: RpcMethod): string { @@ -1228,6 +1233,47 @@ function getResultTypeName( return `${toPascalCase(method.rpcMethod)}Result`; } +function pushNamespaceMethodBody( + out: string[], + constName: string, + isSession: boolean, + hasParams: boolean, + resultIsVoid: boolean, +): void { + // Build the params Value sent over the wire. + if (isSession) { + if (hasParams) { + out.push(` let mut wire_params = serde_json::to_value(params)?;`); + out.push( + ` wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string());`, + ); + } else { + out.push( + ` let wire_params = serde_json::json!({ "sessionId": self.session.id() });`, + ); + } + out.push( + ` let _value = self.session.client().call(rpc_methods::${constName}, Some(wire_params)).await?;`, + ); + } else { + if (hasParams) { + out.push(` let wire_params = serde_json::to_value(params)?;`); + } else { + out.push(` let wire_params = serde_json::json!({});`); + } + out.push( + ` let _value = self.client.call(rpc_methods::${constName}, Some(wire_params)).await?;`, + ); + } + + if (resultIsVoid) { + out.push(` Ok(())`); + } else { + out.push(` Ok(serde_json::from_value(_value)?)`); + } + out.push(` }`); +} + function emitNamespaceMethod( out: string[], method: RpcMethod, @@ -1282,42 +1328,25 @@ function emitNamespaceMethod( const paramArg = hasParams ? `, params: ${paramsTypeName}` : ""; out.push(...docs); - out.push( - ` pub async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, - ); - - // Build the params Value sent over the wire. - if (isSession) { - if (hasParams) { - out.push(` let mut wire_params = serde_json::to_value(params)?;`); - out.push( - ` wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string());`, - ); - } else { - out.push( - ` let wire_params = serde_json::json!({ "sessionId": self.session.id() });`, - ); - } + if (hasParams && paramsInfo.optional) { out.push( - ` let _value = self.session.client().call(rpc_methods::${constName}, Some(wire_params)).await?;`, + ` pub async fn ${fnName}(&self) -> Result<${returnType}, Error> {`, ); - } else { - if (hasParams) { - out.push(` let wire_params = serde_json::to_value(params)?;`); - } else { - out.push(` let wire_params = serde_json::json!({});`); - } + pushNamespaceMethodBody(out, constName, isSession, false, resultIsVoid); + out.push(""); + out.push(...docs); out.push( - ` let _value = self.client.call(rpc_methods::${constName}, Some(wire_params)).await?;`, + ` pub async fn ${fnName}_with_params(&self, params: ${paramsTypeName}) -> Result<${returnType}, Error> {`, ); + pushNamespaceMethodBody(out, constName, isSession, true, resultIsVoid); + out.push(""); + return; } - if (resultIsVoid) { - out.push(` Ok(())`); - } else { - out.push(` Ok(serde_json::from_value(_value)?)`); - } - out.push(` }`); + out.push( + ` pub async fn ${fnName}(&self${paramArg}) -> Result<${returnType}, Error> {`, + ); + pushNamespaceMethodBody(out, constName, isSession, hasParams, resultIsVoid); out.push(""); } diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 85d7c1acf..84c2d4a03 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -502,54 +502,76 @@ export function resolveSchema( return current; } +function hasObjectShape(schema: JSONSchema7): boolean { + return !!(schema.properties || schema.additionalProperties || schema.type === "object"); +} + +function isEmptyNotSchema(schema: JSONSchema7): boolean { + return !!schema.not && typeof schema.not === "object" && Object.keys(schema.not).length === 0; +} + +function mergeObjectSchemas(schemas: JSONSchema7[]): JSONSchema7 | undefined { + const mergedProperties: Record = {}; + const mergedRequired = new Set(); + const merged: JSONSchema7 = { + type: "object", + }; + let hasShape = false; + + for (const objectSchema of schemas) { + if (!merged.title && objectSchema.title) { + merged.title = objectSchema.title; + } + if (!merged.description && objectSchema.description) { + merged.description = objectSchema.description; + } + if (objectSchema.properties) { + Object.assign(mergedProperties, objectSchema.properties); + hasShape = true; + } + if (objectSchema.required) { + for (const name of objectSchema.required) { + mergedRequired.add(name); + } + } + if (objectSchema.additionalProperties !== undefined) { + merged.additionalProperties = objectSchema.additionalProperties; + hasShape = true; + } + } + + if (!hasShape) return undefined; + if (Object.keys(mergedProperties).length > 0) { + merged.properties = mergedProperties; + } + if (mergedRequired.size > 0) { + merged.required = [...mergedRequired]; + } + return merged; +} + export function resolveObjectSchema( schema: JSONSchema7 | null | undefined, definitions: DefinitionCollections | undefined ): JSONSchema7 | undefined { const resolved = resolveSchema(schema, definitions) ?? schema ?? undefined; if (!resolved) return undefined; - if (resolved.properties || resolved.additionalProperties || resolved.type === "object") return resolved; + const resolvedHasObjectShape = hasObjectShape(resolved); if (resolved.allOf) { - const mergedProperties: Record = {}; - const mergedRequired = new Set(); - const merged: JSONSchema7 = { - type: "object", - description: resolved.description, - }; - let hasObjectShape = false; + const objectSchemas: JSONSchema7[] = []; + if (resolvedHasObjectShape) { + objectSchemas.push(resolved); + } for (const item of resolved.allOf) { if (typeof item !== "object") continue; const objectSchema = resolveObjectSchema(item as JSONSchema7, definitions); if (!objectSchema) continue; - - if (objectSchema.properties) { - Object.assign(mergedProperties, objectSchema.properties); - hasObjectShape = true; - } - if (objectSchema.required) { - for (const name of objectSchema.required) { - mergedRequired.add(name); - } - } - if (objectSchema.additionalProperties !== undefined) { - merged.additionalProperties = objectSchema.additionalProperties; - hasObjectShape = true; - } - if (!merged.description && objectSchema.description) { - merged.description = objectSchema.description; - } + objectSchemas.push(objectSchema); } - if (!hasObjectShape) return resolved; - if (Object.keys(mergedProperties).length > 0) { - merged.properties = mergedProperties; - } - if (mergedRequired.size > 0) { - merged.required = [...mergedRequired]; - } - return merged; + return mergeObjectSchemas(objectSchemas) ?? resolved; } const singleBranch = (resolved.anyOf ?? resolved.oneOf) @@ -558,13 +580,20 @@ export function resolveObjectSchema( const s = item as JSONSchema7; // Filter out null types and `{ not: {} }` (Zod's representation of "nothing" in optional anyOf) if (s.type === "null") return false; - if (s.not && typeof s.not === "object" && Object.keys(s.not).length === 0) return false; + if (isEmptyNotSchema(s)) return false; return true; }); if (singleBranch && singleBranch.length === 1) { - return resolveObjectSchema(singleBranch[0], definitions); + const objectSchema = resolveObjectSchema(singleBranch[0], definitions); + if (!objectSchema) return resolved; + if (resolvedHasObjectShape) { + return mergeObjectSchemas([resolved, objectSchema]) ?? objectSchema; + } + return objectSchema; } + if (resolvedHasObjectShape) return resolved; + return resolved; } diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 017220925..cbda49ccd 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.46", + "@github/copilot": "^1.0.47", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -464,27 +464,27 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.46.tgz", - "integrity": "sha512-e3gxCj8DLGesTAZQ5+jCCbCxe3lMyjKfs5eLgER/SID8Rcb7YpgBXoUvOn3eXxLSsJEmJ3GagHaaHDkf3Zm+Ng==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.47.tgz", + "integrity": "sha512-U4WrajOOjjMVleqIRvRt+kDsjYQPLHxtJMMtdzW2N18dbRddlxqN+qo6ZOxOTy3tks2+YI+G89zyO1qpxpuWSg==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.46", - "@github/copilot-darwin-x64": "1.0.46", - "@github/copilot-linux-arm64": "1.0.46", - "@github/copilot-linux-x64": "1.0.46", - "@github/copilot-win32-arm64": "1.0.46", - "@github/copilot-win32-x64": "1.0.46" + "@github/copilot-darwin-arm64": "1.0.47", + "@github/copilot-darwin-x64": "1.0.47", + "@github/copilot-linux-arm64": "1.0.47", + "@github/copilot-linux-x64": "1.0.47", + "@github/copilot-win32-arm64": "1.0.47", + "@github/copilot-win32-x64": "1.0.47" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.46.tgz", - "integrity": "sha512-zbhXuRguCdDgeIZKH+rjgBM/6CDMUmhLMck8w9XFDxUY2wrP7MSWXuX8yA4/1H3ySOTZMIH1G5DQpWh+npmR2Q==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.47.tgz", + "integrity": "sha512-sGuN+7VfBjOTbPkyKFm0dPfp1hwyNsJVkNsV+3xmOwVsGy3nhROc76sQ5SWWSmyDGl7H58KnpPazlSDwbpf4PQ==", "cpu": [ "arm64" ], @@ -499,9 +499,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.46.tgz", - "integrity": "sha512-kSUcV6cARhM+b/BuNSQtazbORTetRjIWpO3SqWSmH+2UoeZP5A5x+ipr7mhshq+E+pcWPeQKMGbKGY3lrCSMFw==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.47.tgz", + "integrity": "sha512-nVHYbzvOau5zy4nONWZPXROIrqzd7DhY12bMkE7spLe7lj0Sh6MFtTdPpMT7kkaObEikGYLTrZtOUpguwqHkmA==", "cpu": [ "x64" ], @@ -516,9 +516,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.46.tgz", - "integrity": "sha512-Tz3F0LuGFbOvvv0VKQJ4E5XYBsTdqTNMAwOhbkwX6TuKMX88uLJNKP5uPf6yuu1z3J3nt/5rfEd9CxVrZbnqLA==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.47.tgz", + "integrity": "sha512-7aDoE6pnSGcCTuPdJKyHfzif/Rj1z5UE0gLMHHQMo1QIYJkUZFX7mV8Ng4zB+2edq8lNL5DiYRcbFajV54ibSg==", "cpu": [ "arm64" ], @@ -533,9 +533,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.46.tgz", - "integrity": "sha512-s9JWe/YE78I7QEeXrvDGHB5x2XnnkegUJYVE9QR2DI/qLXviHMarM3akOUhed21uVqzoiLPacXKZcTcaDO8tOg==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.47.tgz", + "integrity": "sha512-wB5ekOdoxM/6Ogguk54fqJTHTRJkXwUIyzrbYaMy7zANE82jeRE1PQqs+5SdUZXq2IBMZIN1vq6bM56gpb54qg==", "cpu": [ "x64" ], @@ -550,9 +550,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.46.tgz", - "integrity": "sha512-auX8o8vG8A+rdSthvey1D8q3o6lNlNIfHFjoBU0Z9Fxid6Ghz2paaAn0/Uwz9Ev8W8cn/5C5kEPs3niMXSh4Jw==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.47.tgz", + "integrity": "sha512-AenPXpTeXApOh25biS+Vmc1Uau78OLHxeXjXDF6Po07xWO7fVzorEK0hnSoD6xmpjptvP2MDSMk4as7jyvM0sQ==", "cpu": [ "arm64" ], @@ -567,9 +567,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.46", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.46.tgz", - "integrity": "sha512-iXo9TUqtSxqlBfC+SZSQMrctKJpWR19zr+8dk7hczE42gOVB0/A+NySJwCmY3UFAEY98lbLDjIC+NCbYFcpEHA==", + "version": "1.0.47", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.47.tgz", + "integrity": "sha512-35bOBTTIm31rgbvFDogAMojWMSV6sLTd3mGjLl1Lf/d0KZGCGLqWXAYMAcV3grEjiAEXxlLLzNs8OfBR/9OdZg==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index 3df463311..e6c67908d 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.46", + "@github/copilot": "^1.0.47", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14",