diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index f6ca0382f..6fc593c12 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -5,12 +5,20 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; namespace GitHub.Copilot.SDK.Rpc; +/// Diagnostic IDs for the Copilot SDK. +internal static class Diagnostics +{ + /// Indicates an experimental API that may change or be removed. + internal const string Experimental = "GHCP001"; +} + /// RPC data type for Ping operations. public class PingResult { @@ -427,6 +435,7 @@ internal class SessionWorkspaceCreateFileRequest } /// RPC data type for SessionFleetStart operations. +[Experimental(Diagnostics.Experimental)] public class SessionFleetStartResult { /// Whether fleet mode was successfully activated. @@ -435,6 +444,7 @@ public class SessionFleetStartResult } /// RPC data type for SessionFleetStart operations. +[Experimental(Diagnostics.Experimental)] internal class SessionFleetStartRequest { /// Target session identifier. @@ -463,6 +473,7 @@ public class Agent } /// RPC data type for SessionAgentList operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentListResult { /// Available custom agents. @@ -471,6 +482,7 @@ public class SessionAgentListResult } /// RPC data type for SessionAgentList operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentListRequest { /// Target session identifier. @@ -495,6 +507,7 @@ public class SessionAgentGetCurrentResultAgent } /// RPC data type for SessionAgentGetCurrent operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentGetCurrentResult { /// Currently selected custom agent, or null if using the default agent. @@ -503,6 +516,7 @@ public class SessionAgentGetCurrentResult } /// RPC data type for SessionAgentGetCurrent operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentGetCurrentRequest { /// Target session identifier. @@ -527,6 +541,7 @@ public class SessionAgentSelectResultAgent } /// RPC data type for SessionAgentSelect operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentSelectResult { /// The newly selected custom agent. @@ -535,6 +550,7 @@ public class SessionAgentSelectResult } /// RPC data type for SessionAgentSelect operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentSelectRequest { /// Target session identifier. @@ -547,11 +563,13 @@ internal class SessionAgentSelectRequest } /// RPC data type for SessionAgentDeselect operations. +[Experimental(Diagnostics.Experimental)] public class SessionAgentDeselectResult { } /// RPC data type for SessionAgentDeselect operations. +[Experimental(Diagnostics.Experimental)] internal class SessionAgentDeselectRequest { /// Target session identifier. @@ -560,6 +578,7 @@ internal class SessionAgentDeselectRequest } /// RPC data type for SessionCompactionCompact operations. +[Experimental(Diagnostics.Experimental)] public class SessionCompactionCompactResult { /// Whether compaction completed successfully. @@ -576,6 +595,7 @@ public class SessionCompactionCompactResult } /// RPC data type for SessionCompactionCompact operations. +[Experimental(Diagnostics.Experimental)] internal class SessionCompactionCompactRequest { /// Target session identifier. @@ -1000,6 +1020,7 @@ public async Task CreateFileAsync(string path, } /// Provides session-scoped Fleet APIs. +[Experimental(Diagnostics.Experimental)] public class FleetApi { private readonly JsonRpc _rpc; @@ -1020,6 +1041,7 @@ public async Task StartAsync(string? prompt = null, Can } /// Provides session-scoped Agent APIs. +[Experimental(Diagnostics.Experimental)] public class AgentApi { private readonly JsonRpc _rpc; @@ -1061,6 +1083,7 @@ public async Task DeselectAsync(CancellationToken ca } /// Provides session-scoped Compaction APIs. +[Experimental(Diagnostics.Experimental)] public class CompactionApi { private readonly JsonRpc _rpc; diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index 5d2502c87..38eb0cf3a 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -20,6 +20,10 @@ true + + $(NoWarn);GHCP001 + + true diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj index fbc9f17c3..8e0dbf6b7 100644 --- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj +++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj @@ -2,6 +2,7 @@ false + $(NoWarn);GHCP001 diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index ffe87455e..401f38305 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -208,16 +208,19 @@ type SessionWorkspaceCreateFileParams struct { Path string `json:"path"` } +// Experimental: SessionFleetStartResult is part of an experimental API and may change or be removed. type SessionFleetStartResult struct { // Whether fleet mode was successfully activated Started bool `json:"started"` } +// Experimental: SessionFleetStartParams is part of an experimental API and may change or be removed. type SessionFleetStartParams struct { // Optional user prompt to combine with fleet instructions Prompt *string `json:"prompt,omitempty"` } +// Experimental: SessionAgentListResult is part of an experimental API and may change or be removed. type SessionAgentListResult struct { // Available custom agents Agents []AgentElement `json:"agents"` @@ -232,6 +235,7 @@ type AgentElement struct { Name string `json:"name"` } +// Experimental: SessionAgentGetCurrentResult is part of an experimental API and may change or be removed. type SessionAgentGetCurrentResult struct { // Currently selected custom agent, or null if using the default agent Agent *SessionAgentGetCurrentResultAgent `json:"agent"` @@ -246,6 +250,7 @@ type SessionAgentGetCurrentResultAgent struct { Name string `json:"name"` } +// Experimental: SessionAgentSelectResult is part of an experimental API and may change or be removed. type SessionAgentSelectResult struct { // The newly selected custom agent Agent SessionAgentSelectResultAgent `json:"agent"` @@ -261,14 +266,17 @@ type SessionAgentSelectResultAgent struct { Name string `json:"name"` } +// Experimental: SessionAgentSelectParams is part of an experimental API and may change or be removed. type SessionAgentSelectParams struct { // Name of the custom agent to select Name string `json:"name"` } +// Experimental: SessionAgentDeselectResult is part of an experimental API and may change or be removed. type SessionAgentDeselectResult struct { } +// Experimental: SessionCompactionCompactResult is part of an experimental API and may change or be removed. type SessionCompactionCompactResult struct { // Number of messages removed during compaction MessagesRemoved float64 `json:"messagesRemoved"` @@ -402,7 +410,9 @@ type ResultUnion struct { String *string } -type ServerModelsRpcApi struct{ client *jsonrpc2.Client } +type ServerModelsRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error) { raw, err := a.client.Request("models.list", map[string]interface{}{}) @@ -416,7 +426,9 @@ func (a *ServerModelsRpcApi) List(ctx context.Context) (*ModelsListResult, error return &result, nil } -type ServerToolsRpcApi struct{ client *jsonrpc2.Client } +type ServerToolsRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) (*ToolsListResult, error) { raw, err := a.client.Request("tools.list", params) @@ -430,7 +442,9 @@ func (a *ServerToolsRpcApi) List(ctx context.Context, params *ToolsListParams) ( return &result, nil } -type ServerAccountRpcApi struct{ client *jsonrpc2.Client } +type ServerAccountRpcApi struct { + client *jsonrpc2.Client +} func (a *ServerAccountRpcApi) GetQuota(ctx context.Context) (*AccountGetQuotaResult, error) { raw, err := a.client.Request("account.getQuota", map[string]interface{}{}) @@ -641,6 +655,7 @@ func (a *WorkspaceRpcApi) CreateFile(ctx context.Context, params *SessionWorkspa return &result, nil } +// Experimental: FleetRpcApi contains experimental APIs that may change or be removed. type FleetRpcApi struct { client *jsonrpc2.Client sessionID string @@ -664,6 +679,7 @@ func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams return &result, nil } +// Experimental: AgentRpcApi contains experimental APIs that may change or be removed. type AgentRpcApi struct { client *jsonrpc2.Client sessionID string @@ -724,6 +740,7 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult return &result, nil } +// Experimental: CompactionRpcApi contains experimental APIs that may change or be removed. type CompactionRpcApi struct { client *jsonrpc2.Client sessionID string diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index e5ba9ad4c..16907fdba 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -340,6 +340,7 @@ export interface SessionWorkspaceCreateFileParams { content: string; } +/** @experimental */ export interface SessionFleetStartResult { /** * Whether fleet mode was successfully activated @@ -347,6 +348,7 @@ export interface SessionFleetStartResult { started: boolean; } +/** @experimental */ export interface SessionFleetStartParams { /** * Target session identifier @@ -358,6 +360,7 @@ export interface SessionFleetStartParams { prompt?: string; } +/** @experimental */ export interface SessionAgentListResult { /** * Available custom agents @@ -378,6 +381,7 @@ export interface SessionAgentListResult { }[]; } +/** @experimental */ export interface SessionAgentListParams { /** * Target session identifier @@ -385,6 +389,7 @@ export interface SessionAgentListParams { sessionId: string; } +/** @experimental */ export interface SessionAgentGetCurrentResult { /** * Currently selected custom agent, or null if using the default agent @@ -405,6 +410,7 @@ export interface SessionAgentGetCurrentResult { } | null; } +/** @experimental */ export interface SessionAgentGetCurrentParams { /** * Target session identifier @@ -412,6 +418,7 @@ export interface SessionAgentGetCurrentParams { sessionId: string; } +/** @experimental */ export interface SessionAgentSelectResult { /** * The newly selected custom agent @@ -432,6 +439,7 @@ export interface SessionAgentSelectResult { }; } +/** @experimental */ export interface SessionAgentSelectParams { /** * Target session identifier @@ -443,8 +451,10 @@ export interface SessionAgentSelectParams { name: string; } +/** @experimental */ export interface SessionAgentDeselectResult {} +/** @experimental */ export interface SessionAgentDeselectParams { /** * Target session identifier @@ -452,6 +462,7 @@ export interface SessionAgentDeselectParams { sessionId: string; } +/** @experimental */ export interface SessionCompactionCompactResult { /** * Whether compaction completed successfully @@ -467,6 +478,7 @@ export interface SessionCompactionCompactResult { messagesRemoved: number; } +/** @experimental */ export interface SessionCompactionCompactParams { /** * Target session identifier @@ -660,10 +672,12 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin createFile: async (params: Omit): Promise => connection.sendRequest("session.workspace.createFile", { sessionId, ...params }), }, + /** @experimental */ fleet: { start: async (params: Omit): Promise => connection.sendRequest("session.fleet.start", { sessionId, ...params }), }, + /** @experimental */ agent: { list: async (): Promise => connection.sendRequest("session.agent.list", { sessionId }), @@ -674,6 +688,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin deselect: async (): Promise => connection.sendRequest("session.agent.deselect", { sessionId }), }, + /** @experimental */ compaction: { compact: async (): Promise => connection.sendRequest("session.compaction.compact", { sessionId }), diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 29b7463df..564ccf64e 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -724,6 +724,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFleetStartResult: started: bool @@ -741,6 +742,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFleetStartParams: prompt: str | None = None @@ -786,6 +788,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentListResult: agents: list[AgentElement] @@ -830,6 +833,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentGetCurrentResult: agent: SessionAgentGetCurrentResultAgent | None = None @@ -876,6 +880,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentSelectResult: agent: SessionAgentSelectResultAgent @@ -893,6 +898,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentSelectParams: name: str @@ -910,6 +916,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAgentDeselectResult: @staticmethod @@ -922,6 +929,7 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionCompactionCompactResult: messages_removed: float @@ -1666,6 +1674,7 @@ async def create_file(self, params: SessionWorkspaceCreateFileParams, *, timeout return SessionWorkspaceCreateFileResult.from_dict(await self._client.request("session.workspace.createFile", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class FleetApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -1677,6 +1686,7 @@ async def start(self, params: SessionFleetStartParams, *, timeout: float | None return SessionFleetStartResult.from_dict(await self._client.request("session.fleet.start", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class AgentApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -1697,6 +1707,7 @@ async def deselect(self, *, timeout: float | None = None) -> SessionAgentDeselec return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. class CompactionApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 3aeb0eef3..57e8fcbcb 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -16,6 +16,7 @@ import { getApiSchemaPath, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, EXCLUDED_EVENT_TYPES, REPO_ROOT, type ApiSchema, @@ -594,6 +595,7 @@ export async function generateSessionEvents(schemaPath?: string): Promise // ══════════════════════════════════════════════════════════════════════════════ let emittedRpcClasses = new Set(); +let experimentalRpcTypes = new Set(); let rpcKnownTypes = new Map(); let rpcEnumOutput: string[] = []; @@ -651,6 +653,9 @@ function emitRpcClass(className: string, schema: JSONSchema7, visibility: "publi const requiredSet = new Set(schema.required || []); const lines: string[] = []; lines.push(...xmlDocComment(schema.description || `RPC data type for ${className.replace(/Request$/, "").replace(/Result$/, "")} operations.`, "")); + if (experimentalRpcTypes.has(className)) { + lines.push(`[Experimental(Diagnostics.Experimental)]`); + } lines.push(`${visibility} class ${className}`, `{`); const props = Object.entries(schema.properties || {}); @@ -712,7 +717,7 @@ function emitServerRpcClasses(node: Record, classes: string[]): // Top-level methods (like ping) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitServerInstanceMethod(key, value, srLines, classes, " "); + emitServerInstanceMethod(key, value, srLines, classes, " ", false); } // Group properties @@ -737,6 +742,10 @@ function emitServerApiClass(className: string, node: Record, cl const lines: string[] = []; const displayName = className.replace(/^Server/, "").replace(/Api$/, ""); lines.push(`/// Provides server-scoped ${displayName} APIs.`); + const groupExperimental = isNodeFullyExperimental(node); + if (groupExperimental) { + lines.push(`[Experimental(Diagnostics.Experimental)]`); + } lines.push(`public class ${className}`); lines.push(`{`); lines.push(` private readonly JsonRpc _rpc;`); @@ -748,7 +757,7 @@ function emitServerApiClass(className: string, node: Record, cl for (const [key, value] of Object.entries(node)) { if (!isRpcMethod(value)) continue; - emitServerInstanceMethod(key, value, lines, classes, " "); + emitServerInstanceMethod(key, value, lines, classes, " ", groupExperimental); } lines.push(`}`); @@ -757,13 +766,17 @@ function emitServerApiClass(className: string, node: Record, cl function emitServerInstanceMethod( name: string, - method: { rpcMethod: string; params: JSONSchema7 | null; result: JSONSchema7 }, + method: RpcMethod, lines: string[], classes: string[], - indent: string + indent: string, + groupExperimental: boolean ): void { const methodName = toPascalCase(name); const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(resultClassName); + } const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); if (resultClass) classes.push(resultClass); @@ -773,12 +786,18 @@ function emitServerInstanceMethod( let requestClassName: string | null = null; if (paramEntries.length > 0) { requestClassName = `${typeToClassName(method.rpcMethod)}Request`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(requestClassName); + } const reqClass = emitRpcClass(requestClassName, method.params!, "internal", classes); if (reqClass) classes.push(reqClass); } lines.push(""); lines.push(`${indent}/// Calls "${method.rpcMethod}".`); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + } const sigParams: string[] = []; const bodyAssignments: string[] = []; @@ -817,7 +836,7 @@ function emitSessionRpcClasses(node: Record, classes: string[]) // Emit top-level session RPC methods directly on the SessionRpc class const topLevelLines: string[] = []; for (const [key, value] of topLevelMethods) { - emitSessionMethod(key, value as RpcMethod, topLevelLines, classes, " "); + emitSessionMethod(key, value as RpcMethod, topLevelLines, classes, " ", false); } srLines.push(...topLevelLines); @@ -830,9 +849,12 @@ function emitSessionRpcClasses(node: Record, classes: string[]) return result; } -function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string): void { +function emitSessionMethod(key: string, method: RpcMethod, lines: string[], classes: string[], indent: string, groupExperimental: boolean): void { const methodName = toPascalCase(key); const resultClassName = `${typeToClassName(method.rpcMethod)}Result`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(resultClassName); + } const resultClass = emitRpcClass(resultClassName, method.result, "public", classes); if (resultClass) classes.push(resultClass); @@ -847,12 +869,18 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas }); const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; + if (method.stability === "experimental") { + experimentalRpcTypes.add(requestClassName); + } if (method.params) { const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); if (reqClass) classes.push(reqClass); } lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + } const sigParams: string[] = []; const bodyAssignments = [`SessionId = _sessionId`]; @@ -872,12 +900,14 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas function emitSessionApiClass(className: string, node: Record, classes: string[]): string { const displayName = className.replace(/Api$/, ""); - const lines = [`/// Provides session-scoped ${displayName} APIs.`, `public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; + const groupExperimental = isNodeFullyExperimental(node); + const experimentalAttr = groupExperimental ? `[Experimental(Diagnostics.Experimental)]\n` : ""; + const lines = [`/// Provides session-scoped ${displayName} APIs.`, `${experimentalAttr}public class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; lines.push(` internal ${className}(JsonRpc rpc, string sessionId)`, ` {`, ` _rpc = rpc;`, ` _sessionId = sessionId;`, ` }`); for (const [key, value] of Object.entries(node)) { if (!isRpcMethod(value)) continue; - emitSessionMethod(key, value, lines, classes, " "); + emitSessionMethod(key, value, lines, classes, " ", groupExperimental); } lines.push(`}`); return lines.join("\n"); @@ -885,6 +915,7 @@ function emitSessionApiClass(className: string, node: Record, c function generateRpcCode(schema: ApiSchema): string { emittedRpcClasses.clear(); + experimentalRpcTypes.clear(); rpcKnownTypes.clear(); rpcEnumOutput = []; generatedEnums.clear(); // Clear shared enum deduplication map @@ -902,11 +933,19 @@ function generateRpcCode(schema: ApiSchema): string { // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using StreamJsonRpc; namespace GitHub.Copilot.SDK.Rpc; + +/// Diagnostic IDs for the Copilot SDK. +internal static class Diagnostics +{ + /// Indicates an experimental API that may change or be removed. + internal const string Experimental = "GHCP001"; +} `); for (const cls of classes) if (cls) lines.push(cls, ""); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 1ebc50797..c467761d0 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -17,6 +17,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -161,16 +162,35 @@ async function generateRpc(schemaPath?: string): Promise { lines.push(`package rpc`); lines.push(``); lines.push(`import (`); - lines.push(` "context"`); - lines.push(` "encoding/json"`); + lines.push(`\t"context"`); + lines.push(`\t"encoding/json"`); lines.push(``); - lines.push(` "github.com/github/copilot-sdk/go/internal/jsonrpc2"`); + lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); lines.push(`)`); lines.push(``); - // Add quicktype-generated types (skip package line) - const qtLines = qtResult.lines.filter((l) => !l.startsWith("package ")); - lines.push(...qtLines); + // Add quicktype-generated types (skip package line), annotating experimental types + const experimentalTypeNames = new Set(); + for (const method of allMethods) { + if (method.stability !== "experimental") continue; + experimentalTypeNames.add(toPascalCase(method.rpcMethod) + "Result"); + const baseName = toPascalCase(method.rpcMethod); + if (combinedSchema.definitions![baseName + "Params"]) { + experimentalTypeNames.add(baseName + "Params"); + } + } + let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n"); + // Strip trailing whitespace from quicktype output (gofmt requirement) + qtCode = qtCode.replace(/[ \t]+$/gm, ""); + for (const typeName of experimentalTypeNames) { + qtCode = qtCode.replace( + new RegExp(`^(type ${typeName} struct)`, "m"), + `// Experimental: ${typeName} is part of an experimental API and may change or be removed.\n$1` + ); + } + // Remove trailing blank lines from quicktype output before appending + qtCode = qtCode.replace(/\n+$/, ""); + lines.push(qtCode); lines.push(``); // Emit ServerRpc @@ -200,23 +220,39 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio for (const [groupName, groupNode] of groups) { const prefix = isSession ? "" : "Server"; const apiName = prefix + toPascalCase(groupName) + apiSuffix; - const fields = isSession ? "client *jsonrpc2.Client; sessionID string" : "client *jsonrpc2.Client"; - lines.push(`type ${apiName} struct { ${fields} }`); + const groupExperimental = isNodeFullyExperimental(groupNode as Record); + if (groupExperimental) { + lines.push(`// Experimental: ${apiName} contains experimental APIs that may change or be removed.`); + } + lines.push(`type ${apiName} struct {`); + if (isSession) { + lines.push(`\tclient *jsonrpc2.Client`); + lines.push(`\tsessionID string`); + } else { + lines.push(`\tclient *jsonrpc2.Client`); + } + lines.push(`}`); lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, apiName, key, value, isSession); + emitMethod(lines, apiName, key, value, isSession, groupExperimental); } } + // Compute field name lengths for gofmt-compatible column alignment + const groupPascalNames = groups.map(([g]) => toPascalCase(g)); + const allFieldNames = isSession ? ["client", "sessionID", ...groupPascalNames] : ["client", ...groupPascalNames]; + const maxFieldLen = Math.max(...allFieldNames.map((n) => n.length)); + const pad = (name: string) => name.padEnd(maxFieldLen); + // Emit wrapper struct lines.push(`// ${wrapperName} provides typed ${isSession ? "session" : "server"}-scoped RPC methods.`); lines.push(`type ${wrapperName} struct {`); - lines.push(` client *jsonrpc2.Client`); - if (isSession) lines.push(` sessionID string`); + lines.push(`\t${pad("client")} *jsonrpc2.Client`); + if (isSession) lines.push(`\t${pad("sessionID")} string`); for (const [groupName] of groups) { const prefix = isSession ? "" : "Server"; - lines.push(` ${toPascalCase(groupName)} *${prefix}${toPascalCase(groupName)}${apiSuffix}`); + lines.push(`\t${pad(toPascalCase(groupName))} *${prefix}${toPascalCase(groupName)}${apiSuffix}`); } lines.push(`}`); lines.push(``); @@ -224,27 +260,31 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods (server only) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, wrapperName, key, value, isSession); + emitMethod(lines, wrapperName, key, value, isSession, false); } + // Compute key alignment for constructor composite literal (gofmt aligns key: value) + const maxKeyLen = Math.max(...groupPascalNames.map((n) => n.length + 1)); // +1 for colon + const padKey = (name: string) => (name + ":").padEnd(maxKeyLen + 1); // +1 for min trailing space + // Constructor const ctorParams = isSession ? "client *jsonrpc2.Client, sessionID string" : "client *jsonrpc2.Client"; const ctorFields = isSession ? "client: client, sessionID: sessionID," : "client: client,"; lines.push(`func New${wrapperName}(${ctorParams}) *${wrapperName} {`); - lines.push(` return &${wrapperName}{${ctorFields}`); + lines.push(`\treturn &${wrapperName}{${ctorFields}`); for (const [groupName] of groups) { const prefix = isSession ? "" : "Server"; const apiInit = isSession ? `&${toPascalCase(groupName)}${apiSuffix}{client: client, sessionID: sessionID}` : `&${prefix}${toPascalCase(groupName)}${apiSuffix}{client: client}`; - lines.push(` ${toPascalCase(groupName)}: ${apiInit},`); + lines.push(`\t\t${padKey(toPascalCase(groupName))}${apiInit},`); } - lines.push(` }`); + lines.push(`\t}`); lines.push(`}`); lines.push(``); } -function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { const methodName = toPascalCase(name); const resultType = toPascalCase(method.rpcMethod) + "Result"; @@ -254,6 +294,9 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; const paramsType = hasParams ? toPascalCase(method.rpcMethod) + "Params" : ""; + if (method.stability === "experimental" && !groupExperimental) { + lines.push(`// Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`); + } const sig = hasParams ? `func (a *${receiver}) ${methodName}(ctx context.Context, params *${paramsType}) (*${resultType}, error)` : `func (a *${receiver}) ${methodName}(ctx context.Context) (*${resultType}, error)`; @@ -261,33 +304,37 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc lines.push(sig + ` {`); if (isSession) { - lines.push(` req := map[string]interface{}{"sessionId": a.sessionID}`); + lines.push(`\treq := map[string]interface{}{"sessionId": a.sessionID}`); if (hasParams) { - lines.push(` if params != nil {`); + lines.push(`\tif params != nil {`); for (const pName of nonSessionParams) { const goField = toGoFieldName(pName); const isOptional = !requiredParams.has(pName); if (isOptional) { // Optional fields are pointers - only add when non-nil and dereference - lines.push(` if params.${goField} != nil {`); - lines.push(` req["${pName}"] = *params.${goField}`); - lines.push(` }`); + lines.push(`\t\tif params.${goField} != nil {`); + lines.push(`\t\t\treq["${pName}"] = *params.${goField}`); + lines.push(`\t\t}`); } else { - lines.push(` req["${pName}"] = params.${goField}`); + lines.push(`\t\treq["${pName}"] = params.${goField}`); } } - lines.push(` }`); + lines.push(`\t}`); } - lines.push(` raw, err := a.client.Request("${method.rpcMethod}", req)`); + lines.push(`\traw, err := a.client.Request("${method.rpcMethod}", req)`); } else { const arg = hasParams ? "params" : "map[string]interface{}{}"; - lines.push(` raw, err := a.client.Request("${method.rpcMethod}", ${arg})`); + lines.push(`\traw, err := a.client.Request("${method.rpcMethod}", ${arg})`); } - lines.push(` if err != nil { return nil, err }`); - lines.push(` var result ${resultType}`); - lines.push(` if err := json.Unmarshal(raw, &result); err != nil { return nil, err }`); - lines.push(` return &result, nil`); + 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`); lines.push(`}`); lines.push(``); } diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 65563d741..3dfa52535 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -15,6 +15,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -215,6 +216,23 @@ async function generateRpc(schemaPath?: string): Promise { // Modernize to Python 3.11+ syntax typesCode = modernizePython(typesCode); + // Annotate experimental data types + const experimentalTypeNames = new Set(); + for (const method of allMethods) { + if (method.stability !== "experimental") continue; + experimentalTypeNames.add(toPascalCase(method.rpcMethod) + "Result"); + const baseName = toPascalCase(method.rpcMethod); + if (combinedSchema.definitions![baseName + "Params"]) { + experimentalTypeNames.add(baseName + "Params"); + } + } + for (const typeName of experimentalTypeNames) { + typesCode = typesCode.replace( + new RegExp(`^(@dataclass\\n)?class ${typeName}[:(]`, "m"), + (match) => `# Experimental: this type is part of an experimental API and may change or be removed.\n${match}` + ); + } + const lines: string[] = []; lines.push(`""" AUTO-GENERATED FILE - DO NOT EDIT @@ -259,12 +277,19 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio for (const [groupName, groupNode] of groups) { const prefix = isSession ? "" : "Server"; const apiName = prefix + toPascalCase(groupName) + "Api"; + const groupExperimental = isNodeFullyExperimental(groupNode as Record); if (isSession) { + if (groupExperimental) { + lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + } lines.push(`class ${apiName}:`); lines.push(` def __init__(self, client: "JsonRpcClient", session_id: str):`); lines.push(` self._client = client`); lines.push(` self._session_id = session_id`); } else { + if (groupExperimental) { + lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + } lines.push(`class ${apiName}:`); lines.push(` def __init__(self, client: "JsonRpcClient"):`); lines.push(` self._client = client`); @@ -272,7 +297,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, groupExperimental); } lines.push(``); } @@ -301,12 +326,12 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, false); } lines.push(``); } -function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void { const methodName = toSnakeCase(name); const resultType = toPascalCase(method.rpcMethod) + "Result"; @@ -322,6 +347,10 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: lines.push(sig); + if (method.stability === "experimental" && !groupExperimental) { + lines.push(` """.. warning:: This API is experimental and may change or be removed in future versions."""`); + } + // Build request body with proper serialization/deserialization if (isSession) { if (hasParams) { diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 77c31019a..8d23b428f 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -15,6 +15,7 @@ import { postProcessSchema, writeGeneratedFile, isRpcMethod, + isNodeFullyExperimental, type ApiSchema, type RpcMethod, } from "./utils.js"; @@ -91,6 +92,9 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; bannerComment: "", additionalProperties: false, }); + if (method.stability === "experimental") { + lines.push("/** @experimental */"); + } lines.push(compiled.trim()); lines.push(""); @@ -99,6 +103,9 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; bannerComment: "", additionalProperties: false, }); + if (method.stability === "experimental") { + lines.push("/** @experimental */"); + } lines.push(paramsCompiled.trim()); lines.push(""); } @@ -129,7 +136,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; console.log(` ✓ ${outPath}`); } -function emitGroup(node: Record, indent: string, isSession: boolean): string[] { +function emitGroup(node: Record, indent: string, isSession: boolean, parentExperimental = false): string[] { const lines: string[] = []; for (const [key, value] of Object.entries(node)) { if (isRpcMethod(value)) { @@ -160,11 +167,18 @@ function emitGroup(node: Record, indent: string, isSession: boo } } + if ((value as RpcMethod).stability === "experimental" && !parentExperimental) { + lines.push(`${indent}/** @experimental */`); + } lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); } else if (typeof value === "object" && value !== null) { + const groupExperimental = isNodeFullyExperimental(value as Record); + if (groupExperimental) { + lines.push(`${indent}/** @experimental */`); + } lines.push(`${indent}${key}: {`); - lines.push(...emitGroup(value as Record, indent + " ", isSession)); + lines.push(...emitGroup(value as Record, indent + " ", isSession, groupExperimental)); lines.push(`${indent}},`); } } diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 88ca68de8..2c13b1d96 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -126,6 +126,7 @@ export interface RpcMethod { rpcMethod: string; params: JSONSchema7 | null; result: JSONSchema7; + stability?: string; } export interface ApiSchema { @@ -136,3 +137,18 @@ export interface ApiSchema { export function isRpcMethod(node: unknown): node is RpcMethod { return typeof node === "object" && node !== null && "rpcMethod" in node; } + +/** Returns true when every leaf RPC method inside `node` is marked experimental. */ +export function isNodeFullyExperimental(node: Record): boolean { + const methods: RpcMethod[] = []; + (function collect(n: Record) { + for (const value of Object.values(n)) { + if (isRpcMethod(value)) { + methods.push(value); + } else if (typeof value === "object" && value !== null) { + collect(value as Record); + } + } + })(node); + return methods.length > 0 && methods.every(m => m.stability === "experimental"); +}