From 1816f1d8e8f2522175ea0c4ea17f42350507e964 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 09:17:18 -0400 Subject: [PATCH 1/9] Support experimental schema types in codegen Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 7 --- dotnet/src/Types.cs | 7 +++ rust/src/generated/rpc.rs | 3 +- scripts/codegen/csharp.ts | 112 +++++++++++++++++++++------------- scripts/codegen/go.ts | 69 +++++++++++++++++---- scripts/codegen/python.ts | 43 ++++++++++--- scripts/codegen/rust.ts | 48 ++++++++++++++- scripts/codegen/typescript.ts | 55 ++++++++++++----- scripts/codegen/utils.ts | 5 ++ 9 files changed, 261 insertions(+), 88 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index e643cc8ef..29a3ab89e 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -17,13 +17,6 @@ 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 sealed class PingResult { diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index bd3ba1b78..82690931a 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -42,6 +42,13 @@ internal static void WriteValue(Utf8JsonWriter writer, string value, Type typeTo } } +/// 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"; +} + /// /// Represents the connection state of the Copilot client. /// diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 5a0b48434..3c1956e28 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -8,7 +8,8 @@ #![allow(missing_docs)] #![allow(clippy::too_many_arguments)] -use super::api_types::{rpc_methods, *}; +use super::api_types::rpc_methods; +use super::api_types::*; use crate::session::Session; use crate::{Client, Error}; diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index f82a5cd03..34d6d56ea 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -28,6 +28,7 @@ import { isNodeFullyExperimental, isNodeFullyDeprecated, isSchemaDeprecated, + isSchemaExperimental, isObjectSchema, isVoidSchema, getNullableInner, @@ -308,6 +309,9 @@ const COPYRIGHT = `/*----------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/`; +const EXPERIMENTAL_ATTRIBUTE = "[Experimental(Diagnostics.Experimental)]"; +const OBSOLETE_ATTRIBUTE = `[Obsolete("This member is deprecated and will be removed in a future version.")]`; + // ══════════════════════════════════════════════════════════════════════════════ // SESSION EVENTS // ══════════════════════════════════════════════════════════════════════════════ @@ -318,6 +322,8 @@ interface EventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventExperimental: boolean; + dataExperimental: boolean; } let generatedEnums = new Map(); @@ -333,7 +339,8 @@ function getOrCreateEnum( enumOutput: string[], description?: string, explicitName?: string, - deprecated?: boolean + deprecated?: boolean, + experimental?: boolean ): string { const enumName = explicitName ?? `${parentClassName}${propName}`; const existing = generatedEnums.get(enumName); @@ -342,7 +349,8 @@ function getOrCreateEnum( const lines: string[] = []; lines.push(...xmlDocEnumComment(description, "")); - if (deprecated) lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (deprecated) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`[JsonConverter(typeof(Converter))]`); lines.push(`[DebuggerDisplay("{Value,nq}")]`); lines.push(`public readonly struct ${enumName} : IEquatable<${enumName}>`); @@ -412,6 +420,8 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${baseName}Data`, dataSchema, dataDescription: dataSchema?.description, + eventExperimental: isSchemaExperimental(variant), + dataExperimental: isSchemaExperimental(dataSchema), }; }); } @@ -487,13 +497,14 @@ function generateDiscriminatedUnionClass( nestedClasses: Map, enumOutput: string[], description?: string, - propertyResolver?: PropertyTypeResolver + propertyResolver?: PropertyTypeResolver, + experimental = false ): string { if (isBooleanDiscriminator(discriminatorInfo)) { - return generateFlattenedBooleanDiscriminatedClass(baseClassName, discriminatorInfo, knownTypes, nestedClasses, enumOutput, description, propertyResolver); + return generateFlattenedBooleanDiscriminatedClass(baseClassName, discriminatorInfo, knownTypes, nestedClasses, enumOutput, description, propertyResolver, experimental); } - return generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, description, propertyResolver); + return generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, knownTypes, nestedClasses, enumOutput, description, propertyResolver, experimental); } function generateFlattenedBooleanDiscriminatedClass( @@ -503,7 +514,8 @@ function generateFlattenedBooleanDiscriminatedClass( nestedClasses: Map, enumOutput: string[], description?: string, - propertyResolver?: PropertyTypeResolver + propertyResolver?: PropertyTypeResolver, + experimental = false ): string { const resolver = propertyResolver ?? resolveSessionPropertyType; const renamedBase = applyTypeRename(baseClassName); @@ -532,6 +544,7 @@ function generateFlattenedBooleanDiscriminatedClass( } lines.push(...xmlDocCommentWithFallback(description, `Data type discriminated by ${escapeXml(discriminatorInfo.property)}.`, "")); + if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); lines.push(`public partial class ${renamedBase}`); lines.push(`{`); lines.push(` /// The boolean discriminator.`); @@ -547,7 +560,7 @@ function generateFlattenedBooleanDiscriminatedClass( lines.push(""); lines.push(...xmlDocPropertyComment(info.schema.description, propName, " ")); lines.push(...emitDataAnnotations(info.schema, " ")); - if (isSchemaDeprecated(info.schema)) lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaDeprecated(info.schema)) lines.push(` ${OBSOLETE_ATTRIBUTE}`); if (isDurationProperty(info.schema)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); @@ -570,7 +583,8 @@ function generatePolymorphicClasses( nestedClasses: Map, enumOutput: string[], description?: string, - propertyResolver?: PropertyTypeResolver + propertyResolver?: PropertyTypeResolver, + experimental = false ): string { const resolver = propertyResolver ?? resolveSessionPropertyType; const lines: string[] = []; @@ -578,6 +592,7 @@ function generatePolymorphicClasses( const renamedBase = applyTypeRename(baseClassName); lines.push(...xmlDocCommentWithFallback(description, `Polymorphic base type discriminated by ${escapeXml(discriminatorProperty)}.`, "")); + if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); lines.push(`[JsonPolymorphic(`); lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`); lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`); @@ -624,7 +639,8 @@ function generateDerivedClass( const required = new Set(schema.required || []); lines.push(...xmlDocCommentWithFallback(schema.description, `The ${escapeXml(discriminatorValue)} variant of .`, "")); - if (isSchemaDeprecated(schema)) lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaExperimental(schema)) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (isSchemaDeprecated(schema)) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`public partial class ${className} : ${baseClassName}`); lines.push(`{`); lines.push(` /// `); @@ -643,7 +659,7 @@ function generateDerivedClass( lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " ")); - if (isSchemaDeprecated(propSchema as JSONSchema7)) lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaDeprecated(propSchema as JSONSchema7)) lines.push(` ${OBSOLETE_ATTRIBUTE}`); if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); @@ -667,7 +683,8 @@ function generateNestedClass( const required = new Set(schema.required || []); const lines: string[] = []; lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, "")); - if (isSchemaDeprecated(schema)) lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaExperimental(schema)) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (isSchemaDeprecated(schema)) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`public partial class ${className}`, `{`); for (const [propName, propSchema] of Object.entries(schema.properties || {}).sort(([a], [b]) => a.localeCompare(b))) { @@ -679,7 +696,7 @@ function generateNestedClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ")); - if (isSchemaDeprecated(prop)) lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaDeprecated(prop)) lines.push(` ${OBSOLETE_ATTRIBUTE}`); if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); @@ -709,7 +726,7 @@ function resolveSessionPropertyType( } if (refSchema.enum && Array.isArray(refSchema.enum)) { - const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema)); + const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema)); return isRequired ? enumName : `${enumName}?`; } @@ -743,7 +760,7 @@ function resolveSessionPropertyType( const hasNull = propSchema.anyOf.length > nonNull.length; const baseClassName = (propSchema.title as string) ?? `${parentClassName}${propName}`; const renamedBase = applyTypeRename(baseClassName); - const polymorphicCode = generateDiscriminatedUnionClass(baseClassName, discriminatorInfo, variants, knownTypes, nestedClasses, enumOutput, propSchema.description); + const polymorphicCode = generateDiscriminatedUnionClass(baseClassName, discriminatorInfo, variants, knownTypes, nestedClasses, enumOutput, propSchema.description, undefined, isSchemaExperimental(propSchema)); nestedClasses.set(renamedBase, polymorphicCode); return isRequired && !hasNull ? renamedBase : `${renamedBase}?`; } @@ -751,7 +768,7 @@ function resolveSessionPropertyType( return !isRequired ? "object?" : "object"; } if (propSchema.enum && Array.isArray(propSchema.enum)) { - const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, propSchema.title as string | undefined, isSchemaDeprecated(propSchema)); + const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); return isRequired ? enumName : `${enumName}?`; } if (propSchema.type === "object" && propSchema.properties) { @@ -798,8 +815,11 @@ function generateDataClass(variant: EventVariant, knownTypes: Map.`, "")); } + if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { + lines.push(EXPERIMENTAL_ATTRIBUTE); + } if (isSchemaDeprecated(variant.dataSchema)) { - lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(OBSOLETE_ATTRIBUTE); } lines.push(`public partial class ${variant.dataClassName}`, `{`); @@ -811,7 +831,7 @@ function generateDataClass(variant: EventVariant, knownTypes: MapRepresents the ${escapeXml(variant.typeName)} event.`); } + if (variant.eventExperimental) { + lines.push(EXPERIMENTAL_ATTRIBUTE); + } lines.push(`public partial class ${variant.className} : SessionEvent`, `{`); lines.push(` /// `); lines.push(` [JsonIgnore]`, ` public override string Type => "${variant.typeName}";`, ""); @@ -1040,7 +1063,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam } if (refSchema.enum && Array.isArray(refSchema.enum)) { - const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema)); + const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema)); return isRequired ? enumName : `${enumName}?`; } @@ -1083,7 +1106,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam } return result; }; - const polymorphicCode = generateDiscriminatedUnionClass(baseClassName, discriminatorInfo, variants, rpcKnownTypes, nestedMap, rpcEnumOutput, schema.description, rpcPropertyResolver); + const polymorphicCode = generateDiscriminatedUnionClass(baseClassName, discriminatorInfo, variants, rpcKnownTypes, nestedMap, rpcEnumOutput, schema.description, rpcPropertyResolver, isSchemaExperimental(schema)); classes.push(polymorphicCode); for (const nested of nestedMap.values()) classes.push(nested); } @@ -1101,6 +1124,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam schema.description, schema.title as string | undefined, isSchemaDeprecated(schema), + isSchemaExperimental(schema), ); return isRequired ? enumName : `${enumName}?`; } @@ -1163,11 +1187,11 @@ function emitRpcClass( const requiredSet = new Set(effectiveSchema.required || []); const lines: string[] = []; lines.push(...xmlDocComment(schema.description || effectiveSchema.description || `RPC data type for ${className.replace(/(Request|Result|Params)$/, "")} operations.`, "")); - if (experimentalRpcTypes.has(className)) { - lines.push(`[Experimental(Diagnostics.Experimental)]`); + if (experimentalRpcTypes.has(className) || isSchemaExperimental(schema) || isSchemaExperimental(effectiveSchema)) { + lines.push(EXPERIMENTAL_ATTRIBUTE); } if (isSchemaDeprecated(schema) || isSchemaDeprecated(effectiveSchema)) { - lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(OBSOLETE_ATTRIBUTE); } lines.push(`${visibility} sealed class ${className}`, `{`); @@ -1182,7 +1206,7 @@ function emitRpcClass( lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); lines.push(...emitDataAnnotations(prop, " ")); - if (isSchemaDeprecated(prop)) lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`); + if (isSchemaDeprecated(prop)) lines.push(` ${OBSOLETE_ATTRIBUTE}`); if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); lines.push(` [JsonPropertyName("${propName}")]`); @@ -1214,7 +1238,7 @@ function emitRpcClass( */ 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)); + const enumName = getOrCreateEnum("", typeName, schema.enum as string[], rpcEnumOutput, schema.description, typeName, isSchemaDeprecated(schema), isSchemaExperimental(schema)); emittedRpcEnumResultTypes.add(enumName); return enumName; } @@ -1282,10 +1306,10 @@ function emitServerApiClass(className: string, node: Record, cl const groupExperimental = isNodeFullyExperimental(node); const groupDeprecated = isNodeFullyDeprecated(node); if (groupExperimental) { - lines.push(`[Experimental(Diagnostics.Experimental)]`); + lines.push(EXPERIMENTAL_ATTRIBUTE); } if (groupDeprecated) { - lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(OBSOLETE_ATTRIBUTE); } lines.push(`public sealed class ${className}`); lines.push(`{`); @@ -1371,10 +1395,10 @@ function emitServerInstanceMethod( lines.push(""); lines.push(`${indent}/// Calls "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + lines.push(`${indent}${EXPERIMENTAL_ATTRIBUTE}`); } if (method.deprecated && !groupDeprecated) { - lines.push(`${indent}[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(`${indent}${OBSOLETE_ATTRIBUTE}`); } const sigParams: string[] = []; @@ -1477,10 +1501,10 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(`${indent}[Experimental(Diagnostics.Experimental)]`); + lines.push(`${indent}${EXPERIMENTAL_ATTRIBUTE}`); } if (method.deprecated && !groupDeprecated) { - lines.push(`${indent}[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(`${indent}${OBSOLETE_ATTRIBUTE}`); } const sigParams: string[] = []; const bodyAssignments = [`SessionId = _sessionId`]; @@ -1509,8 +1533,8 @@ function emitSessionApiClass(className: string, node: Record, c const displayName = className.replace(/Api$/, ""); const groupExperimental = isNodeFullyExperimental(node); const groupDeprecated = isNodeFullyDeprecated(node); - const experimentalAttr = groupExperimental ? `[Experimental(Diagnostics.Experimental)]\n` : ""; - const deprecatedAttr = groupDeprecated ? `[Obsolete("This member is deprecated and will be removed in a future version.")]\n` : ""; + const experimentalAttr = groupExperimental ? `${EXPERIMENTAL_ATTRIBUTE}\n` : ""; + const deprecatedAttr = groupDeprecated ? `${OBSOLETE_ATTRIBUTE}\n` : ""; const subGroups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const lines = [`/// Provides session-scoped ${displayName} APIs.`, `${experimentalAttr}${deprecatedAttr}public sealed class ${className}`, `{`, ` private readonly JsonRpc _rpc;`, ` private readonly string _sessionId;`, ""]; @@ -1597,10 +1621,10 @@ function emitClientSessionApiRegistration(clientSchema: Record, const groupDeprecated = isNodeFullyDeprecated(groupNode); lines.push(`/// Handles \`${groupName}\` client session API methods.`); if (groupExperimental) { - lines.push(`[Experimental(Diagnostics.Experimental)]`); + lines.push(EXPERIMENTAL_ATTRIBUTE); } if (groupDeprecated) { - lines.push(`[Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(OBSOLETE_ATTRIBUTE); } lines.push(`public interface ${interfaceName}`); lines.push(`{`); @@ -1611,10 +1635,10 @@ function emitClientSessionApiRegistration(clientSchema: Record, const taskType = resultTaskType(method); lines.push(` /// Handles "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(` [Experimental(Diagnostics.Experimental)]`); + lines.push(` ${EXPERIMENTAL_ATTRIBUTE}`); } if (method.deprecated && !groupDeprecated) { - lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`); + lines.push(` ${OBSOLETE_ATTRIBUTE}`); } if (hasParams) { lines.push(` ${taskType} ${clientHandlerMethodName(method.rpcMethod)}(${paramsTypeName(method)} request, CancellationToken cancellationToken = default);`); @@ -1689,6 +1713,13 @@ function generateRpcCode(schema: ApiSchema): string { rpcEnumOutput = []; generatedEnums.clear(); // Clear shared enum deduplication map rpcDefinitions = collectDefinitionCollections(schema as Record); + for (const defs of [rpcDefinitions.definitions, rpcDefinitions.$defs]) { + for (const [name, def] of Object.entries(defs ?? {})) { + if (typeof def === "object" && def !== null && isSchemaExperimental(def as JSONSchema7)) { + experimentalRpcTypes.add(typeToClassName(name)); + } + } + } const classes: string[] = []; let serverRpcParts: string[] = []; @@ -1717,13 +1748,6 @@ using System.Text.Json; using System.Text.Json.Serialization; 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 4ea260852..43e7b710c 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -27,6 +27,7 @@ import { isNodeFullyExperimental, isRpcMethod, isSchemaDeprecated, + isSchemaExperimental, isVoidSchema, postProcessSchema, refTypeName, @@ -284,6 +285,8 @@ interface GoEventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventExperimental: boolean; + dataExperimental: boolean; } interface GoEventEnvelopeProperty extends SessionEventEnvelopeProperty { @@ -364,6 +367,8 @@ function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, dataDescription: dataSchema.description, + eventExperimental: isSchemaExperimental(variant), + dataExperimental: isSchemaExperimental(dataSchema), }; }); } @@ -496,7 +501,8 @@ function getOrCreateGoEnum( values: string[], ctx: GoCodegenCtx, description?: string, - deprecated?: boolean + deprecated?: boolean, + experimental?: boolean ): string { const existing = ctx.enumsByName.get(enumName); if (existing) return existing; @@ -505,6 +511,9 @@ function getOrCreateGoEnum( if (description) { pushGoCommentForContext(lines, description, ctx); } + if (experimental) { + pushGoCommentForContext(lines, `Experimental: ${enumName} is part of an experimental API and may change or be removed.`, ctx); + } if (deprecated) { pushGoCommentForContext(lines, `Deprecated: ${enumName} is deprecated and will be removed in a future version.`, ctx); } @@ -589,7 +598,7 @@ function resolveGoPropertyType( const resolved = resolveRef(propSchema.$ref, ctx.definitions); if (resolved) { if (resolved.enum) { - const enumType = getOrCreateGoEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved)); + const enumType = getOrCreateGoEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved), isSchemaExperimental(resolved)); return isRequired ? enumType : `*${enumType}`; } if (isNamedGoObjectSchema(resolved)) { @@ -643,7 +652,7 @@ function resolveGoPropertyType( // Handle enum if (propSchema.enum && Array.isArray(propSchema.enum)) { - const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, propSchema.enum as string[], ctx, propSchema.description, isSchemaDeprecated(propSchema)); + const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, propSchema.enum as string[], ctx, propSchema.description, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); return isRequired ? enumType : `*${enumType}`; } @@ -653,7 +662,7 @@ function resolveGoPropertyType( if (typeof propSchema.const !== "string") { return resolveGoPropertyType(schemaForConstValue(propSchema.const), parentTypeName, jsonPropName, isRequired, ctx); } - const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, [propSchema.const], ctx, propSchema.description, isSchemaDeprecated(propSchema)); + const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, [propSchema.const], ctx, propSchema.description, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema)); return isRequired ? enumType : `*${enumType}`; } @@ -871,6 +880,9 @@ function emitGoStruct( if (desc) { pushGoCommentForContext(lines, desc, ctx); } + if (isSchemaExperimental(schema)) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); } @@ -1406,7 +1418,8 @@ function emitGoFlatDiscriminatedUnion( typeName: string, discriminator: GoDiscriminatorInfo, ctx: GoCodegenCtx, - description?: string + description?: string, + experimental = false ): void { if (ctx.generatedNames.has(typeName)) return; ctx.generatedNames.add(typeName); @@ -1422,7 +1435,9 @@ function emitGoFlatDiscriminatedUnion( typeName + discGoName, discValues, ctx, - `${discGoName} discriminator for ${typeName}.` + `${discGoName} discriminator for ${typeName}.`, + false, + experimental ); const unmarshalFuncName = goUnexportedFunctionName("unmarshal", typeName); @@ -1434,6 +1449,9 @@ function emitGoFlatDiscriminatedUnion( if (description) { pushGoCommentForContext(lines, description, ctx); } + if (experimental) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } lines.push(`type ${typeName} interface {`); lines.push(`\t${markerName}()`); lines.push(`\t${discriminatorMethodName}() ${discEnumName}`); @@ -1592,7 +1610,8 @@ function emitGoRequiredFieldDiscriminatedUnion( typeName: string, discriminator: GoRequiredFieldDiscriminatorInfo, ctx: GoCodegenCtx, - description?: string + description?: string, + experimental = false ): void { if (ctx.generatedNames.has(typeName)) return; ctx.generatedNames.add(typeName); @@ -1607,6 +1626,9 @@ function emitGoRequiredFieldDiscriminatedUnion( if (description) { pushGoCommentForContext(lines, description, ctx); } + if (experimental) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } lines.push(`type ${typeName} interface {`); lines.push(`\t${markerName}()`); lines.push(`}`); @@ -1870,7 +1892,8 @@ function emitGoFlattenedObjectUnion( typeName: string, variants: JSONSchema7[], ctx: GoCodegenCtx, - description?: string + description?: string, + experimental = false ): void { if (ctx.generatedNames.has(typeName)) return; ctx.generatedNames.add(typeName); @@ -1905,6 +1928,9 @@ function emitGoFlattenedObjectUnion( if (description) { pushGoCommentForContext(lines, description, ctx); } + if (experimental) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } lines.push(`type ${typeName} struct {`); const fields: GoStructField[] = []; @@ -2105,6 +2131,9 @@ function emitGoPrimitiveUnionInterface(typeName: string, schema: JSONSchema7, ct if (schema.description) { pushGoCommentForContext(lines, schema.description, ctx); } + if (isSchemaExperimental(schema)) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); } @@ -2258,6 +2287,9 @@ function emitGoUntaggedUnionInterface(typeName: string, schema: JSONSchema7, ctx if (schema.description) { pushGoCommentForContext(lines, schema.description, ctx); } + if (isSchemaExperimental(schema)) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); } @@ -2331,16 +2363,16 @@ function planGoUnion(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx, i function emitGoUnionPlan(plan: GoUnionPlan, ctx: GoCodegenCtx): void { switch (plan.kind) { case "discriminated": - emitGoFlatDiscriminatedUnion(plan.typeName, plan.discriminator, ctx, plan.description); + emitGoFlatDiscriminatedUnion(plan.typeName, plan.discriminator, ctx, plan.description, isSchemaExperimental(plan.schema)); return; case "requiredFieldDiscriminated": - emitGoRequiredFieldDiscriminatedUnion(plan.typeName, plan.discriminator, ctx, plan.description); + emitGoRequiredFieldDiscriminatedUnion(plan.typeName, plan.discriminator, ctx, plan.description, isSchemaExperimental(plan.schema)); return; case "primitive": emitGoPrimitiveUnionInterface(plan.typeName, plan.schema, ctx, plan.variants); return; case "flattenedObject": - emitGoFlattenedObjectUnion(plan.typeName, plan.variants, ctx, plan.description); + emitGoFlattenedObjectUnion(plan.typeName, plan.variants, ctx, plan.description, isSchemaExperimental(plan.schema)); return; case "untagged": emitGoUntaggedUnionInterface(plan.typeName, plan.schema, ctx, plan.variants); @@ -2373,6 +2405,9 @@ function emitGoUnionWrapperStruct(typeName: string, schema: JSONSchema7, ctx: Go if (schema.description) { pushGoCommentForContext(lines, schema.description, ctx); } + if (isSchemaExperimental(schema)) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); } @@ -2436,6 +2471,9 @@ function emitGoAlias(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx): if (schema.description) { pushGoCommentForContext(lines, schema.description, ctx); } + if (isSchemaExperimental(schema)) { + pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); } @@ -2448,7 +2486,7 @@ function emitGoRpcDefinition(definitionName: string, schema: JSONSchema7, ctx: G const effectiveSchema = resolveObjectSchema(schema, ctx.definitions) ?? resolveSchema(schema, ctx.definitions) ?? schema; if (isStringEnumDefinition(effectiveSchema)) { - getOrCreateGoEnum(typeName, effectiveSchema.enum, ctx, effectiveSchema.description, isSchemaDeprecated(effectiveSchema)); + getOrCreateGoEnum(typeName, effectiveSchema.enum, ctx, effectiveSchema.description, isSchemaDeprecated(effectiveSchema), isSchemaExperimental(effectiveSchema)); return typeName; } @@ -2630,6 +2668,9 @@ function generateGoSessionEventsCode(schema: JSONSchema7): GoGeneratedTypeCode { } else { pushGoCommentForContext(lines, `${variant.dataClassName} holds the payload for ${variant.typeName} events.`, ctx); } + if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { + pushGoCommentForContext(lines, `Experimental: ${variant.dataClassName} is part of an experimental API and may change or be removed.`, ctx); + } lines.push(`type ${variant.dataClassName} struct {`); const fields: GoStructField[] = []; @@ -2690,6 +2731,10 @@ function generateGoSessionEventsCode(schema: JSONSchema7): GoGeneratedTypeCode { })) .sort((left, right) => left.constName.localeCompare(right.constName)); for (const { constName, typeName } of eventTypeConsts) { + const variant = variants.find((candidate) => candidate.typeName === typeName); + if (variant?.eventExperimental) { + eventTypeEnum.push(`\t// Experimental: ${constName} identifies an experimental event that may change or be removed.`); + } eventTypeEnum.push(`\t${constName} SessionEventType = "${typeName}"`); } eventTypeEnum.push(`)`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index f9327f9d8..58515a51f 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -24,6 +24,7 @@ import { isNodeFullyExperimental, isNodeFullyDeprecated, isSchemaDeprecated, + isSchemaExperimental, postProcessSchema, stripBooleanLiterals, writeGeneratedFile, @@ -478,6 +479,8 @@ interface PyEventVariant { dataClassName: string; dataSchema: JSONSchema7; dataDescription?: string; + eventExperimental: boolean; + dataExperimental: boolean; } interface PyEventEnvelopeProperty extends SessionEventEnvelopeProperty { @@ -650,6 +653,8 @@ function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, dataDescription: dataSchema.description, + eventExperimental: isSchemaExperimental(variant), + dataExperimental: isSchemaExperimental(dataSchema), }; }); } @@ -721,7 +726,8 @@ function getOrCreatePyEnum( values: string[], ctx: PyCodegenCtx, description?: string, - deprecated?: boolean + deprecated?: boolean, + experimental?: boolean ): string { const existing = ctx.enumsByName.get(enumName); if (existing) { @@ -729,6 +735,9 @@ function getOrCreatePyEnum( } const lines: string[] = []; + if (experimental) { + lines.push(`# Experimental: this enum is part of an experimental API and may change or be removed.`); + } if (deprecated) { lines.push(`# Deprecated: this enum is deprecated and will be removed in a future version.`); } @@ -761,7 +770,7 @@ function resolvePyPropertyType( const resolved = resolveSchema(propSchema, ctx.definitions); if (resolved && resolved !== propSchema) { if (resolved.enum && Array.isArray(resolved.enum) && resolved.enum.every((value) => typeof value === "string")) { - const enumType = getOrCreatePyEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved)); + const enumType = getOrCreatePyEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved), isSchemaExperimental(resolved)); const enumResolved: PyResolvedType = { annotation: enumType, fromExpr: (expr) => `parse_enum(${enumType}, ${expr})`, @@ -820,7 +829,8 @@ function resolvePyPropertyType( discriminator.property, discriminator.mapping, ctx, - propSchema.description + propSchema.description, + isSchemaExperimental(propSchema) ); const resolved: PyResolvedType = { annotation: nestedName, @@ -840,7 +850,8 @@ function resolvePyPropertyType( propSchema.enum as string[], ctx, propSchema.description, - isSchemaDeprecated(propSchema) + isSchemaDeprecated(propSchema), + isSchemaExperimental(propSchema) ); const resolved: PyResolvedType = { annotation: enumType, @@ -963,7 +974,8 @@ function resolvePyPropertyType( discriminator.property, discriminator.mapping, ctx, - items.description + items.description, + isSchemaExperimental(items) ); const resolved: PyResolvedType = { annotation: `list[${itemTypeName}]`, @@ -1063,6 +1075,9 @@ function emitPyClass( }); const lines: string[] = []; + if (isSchemaExperimental(schema)) { + lines.push(`# Experimental: this type is part of an experimental API and may change or be removed.`); + } if (isSchemaDeprecated(schema)) { lines.push(`# Deprecated: this type is deprecated and will be removed in a future version.`); } @@ -1131,7 +1146,8 @@ function emitPyFlatDiscriminatedUnion( discriminatorProp: string, mapping: Map, ctx: PyCodegenCtx, - description?: string + description?: string, + experimental = false ): void { if (ctx.generatedNames.has(typeName)) { return; @@ -1173,7 +1189,9 @@ function emitPyFlatDiscriminatedUnion( typeName + toPascalCase(discriminatorProp), [...mapping.keys()], ctx, - description ? `${description} discriminator` : `${typeName} discriminator` + description ? `${description} discriminator` : `${typeName} discriminator`, + false, + experimental ); const fieldEntries: Array<[string, JSONSchema7, boolean]> = [ @@ -1219,6 +1237,9 @@ function emitPyFlatDiscriminatedUnion( }); const lines: string[] = []; + if (experimental) { + lines.push(`# Experimental: this type is part of an experimental API and may change or be removed.`); + } lines.push(`@dataclass`); lines.push(`class ${typeName}:`); if (description) { @@ -1288,6 +1309,9 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { const eventTypeLines: string[] = []; eventTypeLines.push(`class SessionEventType(Enum):`); for (const variant of variants) { + if (variant.eventExperimental) { + eventTypeLines.push(` # Experimental: this event is part of an experimental API and may change or be removed.`); + } eventTypeLines.push(` ${toEnumMemberName(variant.typeName)} = ${JSON.stringify(variant.typeName)}`); } eventTypeLines.push(` UNKNOWN = "unknown"`); @@ -1740,6 +1764,11 @@ async function generateRpc(schemaPath?: string): Promise { // Annotate experimental data types const experimentalTypeNames = new Set(); + for (const [definitionName, definition] of Object.entries(allDefinitions)) { + if (typeof definition === "object" && definition !== null && isSchemaExperimental(definition as JSONSchema7)) { + experimentalTypeNames.add(definitionName); + } + } for (const method of allMethods) { if (method.stability !== "experimental") continue; experimentalTypeNames.add(pythonResultTypeName(method)); diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index c9ed49aca..1b2e5c53c 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -26,6 +26,7 @@ import { isObjectSchema, isRpcMethod, isSchemaDeprecated, + isSchemaExperimental, isVoidSchema, postProcessSchema, refTypeName, @@ -224,6 +225,7 @@ function tryEmitRustDiscriminatedUnion( lines.push(`/// ${line}`); } } + pushRustExperimentalDocs(lines, isSchemaExperimental(schema)); lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]"); lines.push("#[serde(untagged)]"); lines.push(`pub enum ${enumName} {`); @@ -248,6 +250,25 @@ function makeCtx(definitions?: DefinitionCollections): RustCodegenCtx { }; } +function pushRustExperimentalDocs( + lines: string[], + experimental: boolean, + indent = "", +): void { + if (!experimental) return; + lines.push(`${indent}///`); + lines.push(`${indent}///
`); + lines.push(`${indent}///`); + lines.push( + `${indent}/// **Experimental.** This type is part of an experimental wire-protocol surface`, + ); + lines.push( + `${indent}/// and may change or be removed in future SDK or CLI releases.`, + ); + lines.push(`${indent}///`); + lines.push(`${indent}///
`); +} + // ── Type resolution ───────────────────────────────────────────────────────── /** @@ -276,6 +297,7 @@ function resolveRustType( resolved.enum as string[], ctx, resolved.description, + isSchemaExperimental(resolved), ); return wrapOption(typeName, isRequired); } @@ -377,6 +399,7 @@ function resolveRustType( propSchema.enum as string[], ctx, propSchema.description, + isSchemaExperimental(propSchema), ); return wrapOption(enumName, isRequired); } @@ -512,6 +535,7 @@ function emitRustStruct( lines.push(`/// ${line}`); } } + pushRustExperimentalDocs(lines, isSchemaExperimental(schema)); if (isSchemaDeprecated(schema)) { lines.push("#[deprecated]"); } @@ -575,6 +599,7 @@ function emitRustStringEnum( values: string[], ctx: RustCodegenCtx, description?: string, + experimental = false, ): void { if (ctx.generatedNames.has(enumName)) return; ctx.generatedNames.add(enumName); @@ -585,6 +610,7 @@ function emitRustStringEnum( lines.push(`/// ${line}`); } } + pushRustExperimentalDocs(lines, experimental); lines.push("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]"); lines.push(`pub enum ${enumName} {`); @@ -644,6 +670,10 @@ interface EventVariant { dataSchema: JSONSchema7; /** Description of the event */ description?: string; + /** Whether the event definition is experimental. */ + eventExperimental: boolean; + /** Whether the event data definition is experimental. */ + dataExperimental: boolean; } function extractEventVariants(schema: JSONSchema7): EventVariant[] { @@ -688,6 +718,8 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] { dataClassName: `${toPascalCase(typeName)}Data`, dataSchema, description: resolvedVariant.description || dataSchema.description, + eventExperimental: isSchemaExperimental(resolvedVariant), + dataExperimental: isSchemaExperimental(dataSchema), }; }) .filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName)); @@ -717,6 +749,11 @@ function generateSessionEventsCode(schema: JSONSchema7): string { ); typeEnumLines.push("pub enum SessionEventType {"); for (const variant of variants) { + pushRustExperimentalDocs( + typeEnumLines, + variant.eventExperimental, + " ", + ); typeEnumLines.push(` #[serde(rename = "${variant.typeName}")]`); typeEnumLines.push(` ${variant.variantName},`); } @@ -738,6 +775,11 @@ function generateSessionEventsCode(schema: JSONSchema7): string { dataEnumLines.push(`#[serde(tag = "type", content = "data")]`); dataEnumLines.push("pub enum SessionEventData {"); for (const variant of variants) { + pushRustExperimentalDocs( + dataEnumLines, + variant.eventExperimental || variant.dataExperimental, + " ", + ); dataEnumLines.push(` #[serde(rename = "${variant.typeName}")]`); dataEnumLines.push(` ${variant.variantName}(${variant.dataClassName}),`); } @@ -880,6 +922,7 @@ function generateApiTypesCode(apiSchema: ApiSchema): string { schema.enum as string[], ctx, schema.description, + isSchemaExperimental(schema), ); } else if (isObjectSchema(schema)) { emitRustStruct(name, schema, ctx, schema.description); @@ -1349,8 +1392,9 @@ async function rustfmt(filePath: string): Promise { async function generate(): Promise { console.log("Loading schemas..."); - const sessionEventsSchemaPath = await getSessionEventsSchemaPath(); - const apiSchemaPath = await getApiSchemaPath(process.argv[2]); + const sessionEventsSchemaPath = + process.argv[2] || (await getSessionEventsSchemaPath()); + const apiSchemaPath = await getApiSchemaPath(process.argv[3]); const sessionEventsRaw = JSON.parse( await fs.readFile(sessionEventsSchemaPath, "utf-8"), diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 5fdb829ee..ef5c48465 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -26,6 +26,7 @@ import { isNodeFullyExperimental, isNodeFullyDeprecated, isVoidSchema, + isSchemaExperimental, type ApiSchema, type DefinitionCollections, type RpcMethod, @@ -35,6 +36,33 @@ function toPascalCase(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function experimentalDefinitionNames(definitions: DefinitionCollections): Set { + const names = new Set(); + for (const defs of [definitions.definitions, definitions.$defs]) { + for (const [name, def] of Object.entries(defs ?? {})) { + if (typeof def === "object" && def !== null && isSchemaExperimental(def as JSONSchema7)) { + names.add(name); + } + } + } + return names; +} + +function annotateTypeScriptTypes(code: string, typeNames: Iterable, annotation: string): string { + let annotated = code; + for (const typeName of typeNames) { + annotated = annotated.replace( + new RegExp(`(^|\\n)(export (?:interface|type|enum) ${escapeRegExp(typeName)}\\b)`, "m"), + `$1${annotation}\n$2` + ); + } + return annotated; +} + function appendUniqueExportBlocks(output: string[], compiled: string, seenBlocks: Map): void { for (const block of splitExportBlocks(compiled)) { const nameMatch = /^export\s+(?:interface|type)\s+(\w+)/m.exec(block); @@ -212,7 +240,8 @@ async function generateSessionEvents(schemaPath?: string): Promise { additionalProperties: false, }); - const outPath = await writeGeneratedFile("nodejs/src/generated/session-events.ts", ts); + const annotatedTs = annotateTypeScriptTypes(ts, experimentalDefinitionNames(definitionCollections), "/** @experimental */"); + const outPath = await writeGeneratedFile("nodejs/src/generated/session-events.ts", annotatedTs); console.log(` ✓ ${outPath}`); } @@ -335,7 +364,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; ); // Track which type names come from experimental methods for JSDoc annotations. - const experimentalTypes = new Set(); + const experimentalTypes = experimentalDefinitionNames(collectDefinitionCollections(combinedSchema as Record)); // Track which type names come from deprecated methods for JSDoc annotations. const deprecatedTypes = new Set(); // Types are tagged @internal directly via `visibility: "internal"` on the JSON Schema @@ -352,11 +381,12 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; for (const method of [...allMethods, ...clientSessionMethods]) { const resultSchema = getMethodResultSchema(method); if (!isVoidSchema(resultSchema) && !getNullableInner(resultSchema)) { + const resultSource = schemaSourceForNamedDefinition(method.result, resultSchema); combinedSchema.definitions![resultTypeName(method)] = withRootTitle( - schemaSourceForNamedDefinition(method.result, resultSchema), + resultSource, resultTypeName(method) ); - if (method.stability === "experimental") { + if (method.stability === "experimental" || isSchemaExperimental(resultSource)) { experimentalTypes.add(resultTypeName(method)); } if (method.deprecated && !method.result?.$ref) { @@ -379,7 +409,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; filtered, paramsTypeName(method) ); - if (method.stability === "experimental") { + if (method.stability === "experimental" || isSchemaExperimental(filtered)) { experimentalTypes.add(paramsTypeName(method)); } if (method.deprecated) { @@ -387,11 +417,12 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; } } } else { + const paramsSource = schemaSourceForNamedDefinition(method.params, resolvedParams); combinedSchema.definitions![paramsTypeName(method)] = withRootTitle( - schemaSourceForNamedDefinition(method.params, resolvedParams), + paramsSource, paramsTypeName(method) ); - if (method.stability === "experimental") { + if (method.stability === "experimental" || isSchemaExperimental(paramsSource)) { experimentalTypes.add(paramsTypeName(method)); } if (method.deprecated && !method.params?.$ref) { @@ -420,14 +451,8 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; .trim(); if (strippedTs) { - // Add @experimental JSDoc annotations for types from experimental methods - let annotatedTs = strippedTs; - for (const expType of experimentalTypes) { - annotatedTs = annotatedTs.replace( - new RegExp(`(^|\\n)(export (?:interface|type) ${expType}\\b)`, "m"), - `$1/** @experimental */\n$2` - ); - } + // Add @experimental JSDoc annotations for types from experimental methods or schemas. + let annotatedTs = annotateTypeScriptTypes(strippedTs, experimentalTypes, "/** @experimental */"); // Add @deprecated JSDoc annotations for types from deprecated methods for (const depType of deprecatedTypes) { annotatedTs = annotatedTs.replace( diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index a071dc6ae..85d7c1acf 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -445,6 +445,11 @@ export function isSchemaDeprecated(schema: JSONSchema7 | null | undefined): bool return typeof schema === "object" && schema !== null && (schema as Record).deprecated === true; } +/** Returns true when a JSON Schema node is marked as experimental. */ +export function isSchemaExperimental(schema: JSONSchema7 | null | undefined): boolean { + return typeof schema === "object" && schema !== null && (schema as Record).stability === "experimental"; +} + // ── $ref resolution ───────────────────────────────────────────────────────── /** Extract the generated type name from a `$ref` path (e.g. "#/definitions/Model" → "Model"). */ From ba3679ad982d92732738fd6edad9339c059584ae Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 09:21:43 -0400 Subject: [PATCH 2/9] Stabilize Rust generated imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/src/generated/rpc.rs | 3 +-- scripts/codegen/rust.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 3c1956e28..5a0b48434 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -8,8 +8,7 @@ #![allow(missing_docs)] #![allow(clippy::too_many_arguments)] -use super::api_types::rpc_methods; -use super::api_types::*; +use super::api_types::{rpc_methods, *}; use crate::session::Session; use crate::{Client, Error}; diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 1b2e5c53c..0afec08f5 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -1315,8 +1315,7 @@ function generateRpcCode(apiSchema: ApiSchema): string { out.push("#![allow(missing_docs)]"); out.push("#![allow(clippy::too_many_arguments)]"); out.push(""); - out.push("use super::api_types::*;"); - out.push("use super::api_types::rpc_methods;"); + out.push("use super::api_types::{rpc_methods, *};"); out.push("use crate::session::Session;"); out.push("use crate::{Client, Error};"); out.push(""); From eb4489ae593f78f4ed36d0f45cec8610c64f03bc Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 09:55:58 -0400 Subject: [PATCH 3/9] Preserve Rust codegen API schema argument Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/rust.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0afec08f5..280664448 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -4,7 +4,10 @@ * Reads api.schema.json and session-events.schema.json, emits idiomatic Rust * types to rust/src/generated/. * - * Usage: npx tsx scripts/codegen/rust.ts + * Usage: + * npx tsx scripts/codegen/rust.ts + * npx tsx scripts/codegen/rust.ts + * npx tsx scripts/codegen/rust.ts */ import { execFile } from "child_process"; @@ -1388,12 +1391,30 @@ async function rustfmt(filePath: string): Promise { // ── Main ──────────────────────────────────────────────────────────────────── +function parseSchemaArgs(): { + sessionEventsSchemaPath?: string; + apiSchemaPath?: string; +} { + const [firstArg, secondArg] = process.argv.slice(2); + if (secondArg) { + return { + sessionEventsSchemaPath: firstArg, + apiSchemaPath: secondArg, + }; + } + + return { + apiSchemaPath: firstArg, + }; +} + async function generate(): Promise { console.log("Loading schemas..."); + const schemaArgs = parseSchemaArgs(); const sessionEventsSchemaPath = - process.argv[2] || (await getSessionEventsSchemaPath()); - const apiSchemaPath = await getApiSchemaPath(process.argv[3]); + schemaArgs.sessionEventsSchemaPath || (await getSessionEventsSchemaPath()); + const apiSchemaPath = await getApiSchemaPath(schemaArgs.apiSchemaPath); const sessionEventsRaw = JSON.parse( await fs.readFile(sessionEventsSchemaPath, "utf-8"), From caa5ee0804907d99b9d72131912dc6f4f92b8101 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 09:59:31 -0400 Subject: [PATCH 4/9] Retry CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From c98d00be71e7e61c2c9d3dbe14a830ef17124eb6 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 10:20:20 -0400 Subject: [PATCH 5/9] Retry CI after CodeQL 429 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From b8178227fdfd8146f037c61abe05aa9462e91a54 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 11:50:23 -0400 Subject: [PATCH 6/9] Deduplicate experimental marker emission Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/csharp.ts | 36 +++++++++++++--------- scripts/codegen/go.ts | 58 +++++++++++++++++++++++++---------- scripts/codegen/python.ts | 28 ++++++++++++----- scripts/codegen/typescript.ts | 14 ++++++--- 4 files changed, 94 insertions(+), 42 deletions(-) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 34d6d56ea..edfdd81b1 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -312,6 +312,14 @@ const COPYRIGHT = `/*----------------------------------------------------------- const EXPERIMENTAL_ATTRIBUTE = "[Experimental(Diagnostics.Experimental)]"; const OBSOLETE_ATTRIBUTE = `[Obsolete("This member is deprecated and will be removed in a future version.")]`; +function experimentalAttribute(indent = ""): string { + return `${indent}${EXPERIMENTAL_ATTRIBUTE}`; +} + +function pushExperimentalAttribute(lines: string[], indent = ""): void { + lines.push(experimentalAttribute(indent)); +} + // ══════════════════════════════════════════════════════════════════════════════ // SESSION EVENTS // ══════════════════════════════════════════════════════════════════════════════ @@ -349,7 +357,7 @@ function getOrCreateEnum( const lines: string[] = []; lines.push(...xmlDocEnumComment(description, "")); - if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (experimental) pushExperimentalAttribute(lines); if (deprecated) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`[JsonConverter(typeof(Converter))]`); lines.push(`[DebuggerDisplay("{Value,nq}")]`); @@ -544,7 +552,7 @@ function generateFlattenedBooleanDiscriminatedClass( } lines.push(...xmlDocCommentWithFallback(description, `Data type discriminated by ${escapeXml(discriminatorInfo.property)}.`, "")); - if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (experimental) pushExperimentalAttribute(lines); lines.push(`public partial class ${renamedBase}`); lines.push(`{`); lines.push(` /// The boolean discriminator.`); @@ -592,7 +600,7 @@ function generatePolymorphicClasses( const renamedBase = applyTypeRename(baseClassName); lines.push(...xmlDocCommentWithFallback(description, `Polymorphic base type discriminated by ${escapeXml(discriminatorProperty)}.`, "")); - if (experimental) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (experimental) pushExperimentalAttribute(lines); lines.push(`[JsonPolymorphic(`); lines.push(` TypeDiscriminatorPropertyName = "${discriminatorProperty}",`); lines.push(` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]`); @@ -639,7 +647,7 @@ function generateDerivedClass( const required = new Set(schema.required || []); lines.push(...xmlDocCommentWithFallback(schema.description, `The ${escapeXml(discriminatorValue)} variant of .`, "")); - if (isSchemaExperimental(schema)) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (isSchemaExperimental(schema)) pushExperimentalAttribute(lines); if (isSchemaDeprecated(schema)) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`public partial class ${className} : ${baseClassName}`); lines.push(`{`); @@ -683,7 +691,7 @@ function generateNestedClass( const required = new Set(schema.required || []); const lines: string[] = []; lines.push(...xmlDocCommentWithFallback(schema.description, `Nested data type for ${className}.`, "")); - if (isSchemaExperimental(schema)) lines.push(EXPERIMENTAL_ATTRIBUTE); + if (isSchemaExperimental(schema)) pushExperimentalAttribute(lines); if (isSchemaDeprecated(schema)) lines.push(OBSOLETE_ATTRIBUTE); lines.push(`public partial class ${className}`, `{`); @@ -816,7 +824,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map.`, "")); } if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { - lines.push(EXPERIMENTAL_ATTRIBUTE); + pushExperimentalAttribute(lines); } if (isSchemaDeprecated(variant.dataSchema)) { lines.push(OBSOLETE_ATTRIBUTE); @@ -932,7 +940,7 @@ namespace GitHub.Copilot.SDK; lines.push(`/// Represents the ${escapeXml(variant.typeName)} event.`); } if (variant.eventExperimental) { - lines.push(EXPERIMENTAL_ATTRIBUTE); + pushExperimentalAttribute(lines); } lines.push(`public partial class ${variant.className} : SessionEvent`, `{`); lines.push(` /// `); @@ -1188,7 +1196,7 @@ function emitRpcClass( const lines: string[] = []; lines.push(...xmlDocComment(schema.description || effectiveSchema.description || `RPC data type for ${className.replace(/(Request|Result|Params)$/, "")} operations.`, "")); if (experimentalRpcTypes.has(className) || isSchemaExperimental(schema) || isSchemaExperimental(effectiveSchema)) { - lines.push(EXPERIMENTAL_ATTRIBUTE); + pushExperimentalAttribute(lines); } if (isSchemaDeprecated(schema) || isSchemaDeprecated(effectiveSchema)) { lines.push(OBSOLETE_ATTRIBUTE); @@ -1306,7 +1314,7 @@ function emitServerApiClass(className: string, node: Record, cl const groupExperimental = isNodeFullyExperimental(node); const groupDeprecated = isNodeFullyDeprecated(node); if (groupExperimental) { - lines.push(EXPERIMENTAL_ATTRIBUTE); + pushExperimentalAttribute(lines); } if (groupDeprecated) { lines.push(OBSOLETE_ATTRIBUTE); @@ -1395,7 +1403,7 @@ function emitServerInstanceMethod( lines.push(""); lines.push(`${indent}/// Calls "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(`${indent}${EXPERIMENTAL_ATTRIBUTE}`); + pushExperimentalAttribute(lines, indent); } if (method.deprecated && !groupDeprecated) { lines.push(`${indent}${OBSOLETE_ATTRIBUTE}`); @@ -1501,7 +1509,7 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas lines.push("", `${indent}/// Calls "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(`${indent}${EXPERIMENTAL_ATTRIBUTE}`); + pushExperimentalAttribute(lines, indent); } if (method.deprecated && !groupDeprecated) { lines.push(`${indent}${OBSOLETE_ATTRIBUTE}`); @@ -1533,7 +1541,7 @@ function emitSessionApiClass(className: string, node: Record, c const displayName = className.replace(/Api$/, ""); const groupExperimental = isNodeFullyExperimental(node); const groupDeprecated = isNodeFullyDeprecated(node); - const experimentalAttr = groupExperimental ? `${EXPERIMENTAL_ATTRIBUTE}\n` : ""; + const experimentalAttr = groupExperimental ? `${experimentalAttribute()}\n` : ""; const deprecatedAttr = groupDeprecated ? `${OBSOLETE_ATTRIBUTE}\n` : ""; const subGroups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); @@ -1621,7 +1629,7 @@ function emitClientSessionApiRegistration(clientSchema: Record, const groupDeprecated = isNodeFullyDeprecated(groupNode); lines.push(`/// Handles \`${groupName}\` client session API methods.`); if (groupExperimental) { - lines.push(EXPERIMENTAL_ATTRIBUTE); + pushExperimentalAttribute(lines); } if (groupDeprecated) { lines.push(OBSOLETE_ATTRIBUTE); @@ -1635,7 +1643,7 @@ function emitClientSessionApiRegistration(clientSchema: Record, const taskType = resultTaskType(method); lines.push(` /// Handles "${method.rpcMethod}".`); if (method.stability === "experimental" && !groupExperimental) { - lines.push(` ${EXPERIMENTAL_ATTRIBUTE}`); + pushExperimentalAttribute(lines, " "); } if (method.deprecated && !groupDeprecated) { lines.push(` ${OBSOLETE_ATTRIBUTE}`); diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 43e7b710c..5779f2d3b 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -108,6 +108,30 @@ function pushGoCommentForContext(lines: string[], text: string, ctx: GoCodegenCt pushGoComment(lines, text, indent, ctx.wrapComments !== false); } +function goExperimentalTypeComment(typeName: string): string { + return `Experimental: ${typeName} is part of an experimental API and may change or be removed.`; +} + +function pushGoExperimentalTypeComment(lines: string[], typeName: string, ctx: GoCodegenCtx): void { + pushGoCommentForContext(lines, goExperimentalTypeComment(typeName), ctx); +} + +function pushGoExperimentalEventComment(lines: string[], constName: string, indent = ""): void { + pushGoComment(lines, `Experimental: ${constName} identifies an experimental event that may change or be removed.`, indent); +} + +function pushGoExperimentalApiComment(lines: string[], name: string, indent = ""): void { + pushGoComment(lines, `Experimental: ${name} contains experimental APIs that may change or be removed.`, indent); +} + +function pushGoExperimentalSubApiComment(lines: string[], name: string, indent = ""): void { + pushGoComment(lines, `Experimental: ${name} returns experimental APIs that may change or be removed.`, indent); +} + +function pushGoExperimentalMethodComment(lines: string[], methodName: string, indent = ""): void { + pushGoComment(lines, `Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`, indent); +} + function goCommentLines(text: string, indent = "", wrap = true): string[] { const prefix = `${indent}//`; const lines: string[] = []; @@ -512,7 +536,7 @@ function getOrCreateGoEnum( pushGoCommentForContext(lines, description, ctx); } if (experimental) { - pushGoCommentForContext(lines, `Experimental: ${enumName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, enumName, ctx); } if (deprecated) { pushGoCommentForContext(lines, `Deprecated: ${enumName} is deprecated and will be removed in a future version.`, ctx); @@ -881,7 +905,7 @@ function emitGoStruct( pushGoCommentForContext(lines, desc, ctx); } if (isSchemaExperimental(schema)) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); @@ -1450,7 +1474,7 @@ function emitGoFlatDiscriminatedUnion( pushGoCommentForContext(lines, description, ctx); } if (experimental) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } lines.push(`type ${typeName} interface {`); lines.push(`\t${markerName}()`); @@ -1627,7 +1651,7 @@ function emitGoRequiredFieldDiscriminatedUnion( pushGoCommentForContext(lines, description, ctx); } if (experimental) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } lines.push(`type ${typeName} interface {`); lines.push(`\t${markerName}()`); @@ -1929,7 +1953,7 @@ function emitGoFlattenedObjectUnion( pushGoCommentForContext(lines, description, ctx); } if (experimental) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } lines.push(`type ${typeName} struct {`); @@ -2132,7 +2156,7 @@ function emitGoPrimitiveUnionInterface(typeName: string, schema: JSONSchema7, ct pushGoCommentForContext(lines, schema.description, ctx); } if (isSchemaExperimental(schema)) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); @@ -2288,7 +2312,7 @@ function emitGoUntaggedUnionInterface(typeName: string, schema: JSONSchema7, ctx pushGoCommentForContext(lines, schema.description, ctx); } if (isSchemaExperimental(schema)) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); @@ -2406,7 +2430,7 @@ function emitGoUnionWrapperStruct(typeName: string, schema: JSONSchema7, ctx: Go pushGoCommentForContext(lines, schema.description, ctx); } if (isSchemaExperimental(schema)) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); @@ -2472,7 +2496,7 @@ function emitGoAlias(typeName: string, schema: JSONSchema7, ctx: GoCodegenCtx): pushGoCommentForContext(lines, schema.description, ctx); } if (isSchemaExperimental(schema)) { - pushGoCommentForContext(lines, `Experimental: ${typeName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, typeName, ctx); } if (isSchemaDeprecated(schema)) { pushGoCommentForContext(lines, `Deprecated: ${typeName} is deprecated and will be removed in a future version.`, ctx); @@ -2669,7 +2693,7 @@ function generateGoSessionEventsCode(schema: JSONSchema7): GoGeneratedTypeCode { pushGoCommentForContext(lines, `${variant.dataClassName} holds the payload for ${variant.typeName} events.`, ctx); } if (variant.dataExperimental || isSchemaExperimental(variant.dataSchema)) { - pushGoCommentForContext(lines, `Experimental: ${variant.dataClassName} is part of an experimental API and may change or be removed.`, ctx); + pushGoExperimentalTypeComment(lines, variant.dataClassName, ctx); } lines.push(`type ${variant.dataClassName} struct {`); @@ -2733,7 +2757,7 @@ function generateGoSessionEventsCode(schema: JSONSchema7): GoGeneratedTypeCode { for (const { constName, typeName } of eventTypeConsts) { const variant = variants.find((candidate) => candidate.typeName === typeName); if (variant?.eventExperimental) { - eventTypeEnum.push(`\t// Experimental: ${constName} identifies an experimental event that may change or be removed.`); + pushGoExperimentalEventComment(eventTypeEnum, constName, "\t"); } eventTypeEnum.push(`\t${constName} SessionEventType = "${typeName}"`); } @@ -3064,7 +3088,7 @@ async function generateRpc(schemaPath?: string): Promise { for (const typeName of experimentalTypeNames) { generatedTypeCode = generatedTypeCode.replace( new RegExp(`^(type ${typeName} struct)`, "m"), - `// Experimental: ${typeName} is part of an experimental API and may change or be removed.\n$1` + `// ${goExperimentalTypeComment(typeName)}\n$1` ); } @@ -3180,7 +3204,7 @@ function emitApiGroup( pushGoComment(lines, `Deprecated: ${apiName} contains deprecated APIs that will be removed in a future version.`); } if (groupExperimental) { - pushGoComment(lines, `Experimental: ${apiName} contains experimental APIs that may change or be removed.`); + pushGoExperimentalApiComment(lines, apiName); } lines.push(`type ${apiName} ${serviceName}`); lines.push(``); @@ -3197,7 +3221,7 @@ function emitApiGroup( emitApiGroup(lines, subApiName, subGroupNode as Record, isSession, serviceName, resolveType, fields, subGroupExperimental, subGroupDeprecated); if (subGroupExperimental) { - pushGoComment(lines, `Experimental: ${toPascalCase(subGroupName)} returns experimental APIs that may change or be removed.`); + pushGoExperimentalSubApiComment(lines, toPascalCase(subGroupName)); } lines.push(`func (s *${apiName}) ${toPascalCase(subGroupName)}() *${subApiName} {`); lines.push(`\treturn (*${subApiName})(s)`); @@ -3307,7 +3331,7 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc pushGoComment(lines, `Deprecated: ${methodName} is deprecated and will be removed in a future version.`); } if (method.stability === "experimental" && !groupExperimental) { - pushGoComment(lines, `Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`); + pushGoExperimentalMethodComment(lines, methodName); } if (method.visibility === "internal") { pushGoComment(lines, `Internal: ${methodName} is part of the SDK's internal handshake/plumbing; external callers should not use it.`); @@ -3397,7 +3421,7 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< pushGoComment(lines, `Deprecated: ${interfaceName} contains deprecated APIs that will be removed in a future version.`); } if (groupExperimental) { - pushGoComment(lines, `Experimental: ${interfaceName} contains experimental APIs that may change or be removed.`); + pushGoExperimentalApiComment(lines, interfaceName); } lines.push(`type ${interfaceName} interface {`); for (const method of methods) { @@ -3405,7 +3429,7 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< pushGoComment(lines, `Deprecated: ${clientHandlerMethodName(method.rpcMethod)} is deprecated and will be removed in a future version.`, "\t"); } if (method.stability === "experimental" && !groupExperimental) { - pushGoComment(lines, `Experimental: ${clientHandlerMethodName(method.rpcMethod)} is an experimental API and may change or be removed in future versions.`, "\t"); + pushGoExperimentalMethodComment(lines, clientHandlerMethodName(method.rpcMethod), "\t"); } const paramsType = resolveType(goParamsTypeName(method)); const resultSchema = getMethodResultSchema(method); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 58515a51f..46e9a9963 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -44,6 +44,20 @@ import { // ── Utilities ─────────────────────────────────────────────────────────────── +type PyExperimentalSubject = "type" | "enum" | "event"; + +function pyExperimentalComment(subject: PyExperimentalSubject, indent = ""): string { + return `${indent}# Experimental: this ${subject} is part of an experimental API and may change or be removed.`; +} + +function pushPyExperimentalComment(lines: string[], subject: PyExperimentalSubject, indent = ""): void { + lines.push(pyExperimentalComment(subject, indent)); +} + +function pushPyExperimentalApiGroupComment(lines: string[]): void { + lines.push("# Experimental: this API group is experimental and may change or be removed."); +} + /** * Modernize quicktype's Python 3.7 output to Python 3.11+ syntax: * - Optional[T] → T | None @@ -736,7 +750,7 @@ function getOrCreatePyEnum( const lines: string[] = []; if (experimental) { - lines.push(`# Experimental: this enum is part of an experimental API and may change or be removed.`); + pushPyExperimentalComment(lines, "enum"); } if (deprecated) { lines.push(`# Deprecated: this enum is deprecated and will be removed in a future version.`); @@ -1076,7 +1090,7 @@ function emitPyClass( const lines: string[] = []; if (isSchemaExperimental(schema)) { - lines.push(`# Experimental: this type is part of an experimental API and may change or be removed.`); + pushPyExperimentalComment(lines, "type"); } if (isSchemaDeprecated(schema)) { lines.push(`# Deprecated: this type is deprecated and will be removed in a future version.`); @@ -1238,7 +1252,7 @@ function emitPyFlatDiscriminatedUnion( const lines: string[] = []; if (experimental) { - lines.push(`# Experimental: this type is part of an experimental API and may change or be removed.`); + pushPyExperimentalComment(lines, "type"); } lines.push(`@dataclass`); lines.push(`class ${typeName}:`); @@ -1310,7 +1324,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { eventTypeLines.push(`class SessionEventType(Enum):`); for (const variant of variants) { if (variant.eventExperimental) { - eventTypeLines.push(` # Experimental: this event is part of an experimental API and may change or be removed.`); + pushPyExperimentalComment(eventTypeLines, "event", " "); } eventTypeLines.push(` ${toEnumMemberName(variant.typeName)} = ${JSON.stringify(variant.typeName)}`); } @@ -1780,7 +1794,7 @@ async function generateRpc(schemaPath?: string): Promise { 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}` + (match) => `${pyExperimentalComment("type")}\n${match}` ); } @@ -1945,7 +1959,7 @@ function emitPyApiGroup( lines.push(`# Deprecated: this API group is deprecated and will be removed in a future version.`); } if (groupExperimental) { - lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + pushPyExperimentalApiGroupComment(lines); } lines.push(`class ${apiName}:`); if (isSession) { @@ -2134,7 +2148,7 @@ function emitClientSessionApiRegistration( lines.push(`# Deprecated: this API group is deprecated and will be removed in a future version.`); } if (groupExperimental) { - lines.push(`# Experimental: this API group is experimental and may change or be removed.`); + pushPyExperimentalApiGroupComment(lines); } lines.push(`class ${handlerName}(Protocol):`); for (const [methodName, value] of Object.entries(groupNode as Record)) { diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index ef5c48465..0e6922e13 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -32,6 +32,12 @@ import { type RpcMethod, } from "./utils.js"; +const TS_EXPERIMENTAL_JSDOC = "/** @experimental */"; + +function tsExperimentalJSDoc(indent = ""): string { + return `${indent}${TS_EXPERIMENTAL_JSDOC}`; +} + function toPascalCase(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } @@ -240,7 +246,7 @@ async function generateSessionEvents(schemaPath?: string): Promise { additionalProperties: false, }); - const annotatedTs = annotateTypeScriptTypes(ts, experimentalDefinitionNames(definitionCollections), "/** @experimental */"); + const annotatedTs = annotateTypeScriptTypes(ts, experimentalDefinitionNames(definitionCollections), TS_EXPERIMENTAL_JSDOC); const outPath = await writeGeneratedFile("nodejs/src/generated/session-events.ts", annotatedTs); console.log(` ✓ ${outPath}`); } @@ -452,7 +458,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; if (strippedTs) { // Add @experimental JSDoc annotations for types from experimental methods or schemas. - let annotatedTs = annotateTypeScriptTypes(strippedTs, experimentalTypes, "/** @experimental */"); + let annotatedTs = annotateTypeScriptTypes(strippedTs, experimentalTypes, TS_EXPERIMENTAL_JSDOC); // Add @deprecated JSDoc annotations for types from deprecated methods for (const depType of deprecatedTypes) { annotatedTs = annotatedTs.replace( @@ -592,7 +598,7 @@ function emitGroup( lines.push(`${indent}/** @deprecated */`); } if ((value as RpcMethod).stability === "experimental" && !parentExperimental) { - lines.push(`${indent}/** @experimental */`); + lines.push(tsExperimentalJSDoc(indent)); } lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); @@ -613,7 +619,7 @@ function emitGroup( lines.push(`${indent}/** @deprecated */`); } if (groupExperimental) { - lines.push(`${indent}/** @experimental */`); + lines.push(tsExperimentalJSDoc(indent)); } lines.push(`${indent}${key}: {`); lines.push(...childLines); From 61abbe6171c948216bb2571618412fb4900747f2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 12:36:41 -0400 Subject: [PATCH 7/9] Remove unused Python event experimental state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/python.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 46e9a9963..63de41e20 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -494,7 +494,6 @@ interface PyEventVariant { dataSchema: JSONSchema7; dataDescription?: string; eventExperimental: boolean; - dataExperimental: boolean; } interface PyEventEnvelopeProperty extends SessionEventEnvelopeProperty { @@ -668,7 +667,6 @@ function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { dataSchema, dataDescription: dataSchema.description, eventExperimental: isSchemaExperimental(variant), - dataExperimental: isSchemaExperimental(dataSchema), }; }); } From 1070a7cc9cd9a40ed3619e9edf2c966e38495d2a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 13:25:05 -0400 Subject: [PATCH 8/9] Align event data experimental metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/python.ts | 2 ++ scripts/codegen/rust.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 63de41e20..46e9a9963 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -494,6 +494,7 @@ interface PyEventVariant { dataSchema: JSONSchema7; dataDescription?: string; eventExperimental: boolean; + dataExperimental: boolean; } interface PyEventEnvelopeProperty extends SessionEventEnvelopeProperty { @@ -667,6 +668,7 @@ function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] { dataSchema, dataDescription: dataSchema.description, eventExperimental: isSchemaExperimental(variant), + dataExperimental: isSchemaExperimental(dataSchema), }; }); } diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 280664448..4300c70e4 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -780,7 +780,7 @@ function generateSessionEventsCode(schema: JSONSchema7): string { for (const variant of variants) { pushRustExperimentalDocs( dataEnumLines, - variant.eventExperimental || variant.dataExperimental, + variant.dataExperimental, " ", ); dataEnumLines.push(` #[serde(rename = "${variant.typeName}")]`); From 7c602be76b02b7486e9456451dc018fc16eed09a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 12 May 2026 13:37:32 -0400 Subject: [PATCH 9/9] Use Python event data experimental metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/codegen/python.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 46e9a9963..30aff56bd 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -1058,7 +1058,8 @@ function emitPyClass( typeName: string, schema: JSONSchema7, ctx: PyCodegenCtx, - description?: string + description?: string, + experimental = isSchemaExperimental(schema) ): void { if (ctx.generatedNames.has(typeName)) { return; @@ -1089,7 +1090,7 @@ function emitPyClass( }); const lines: string[] = []; - if (isSchemaExperimental(schema)) { + if (experimental) { pushPyExperimentalComment(lines, "type"); } if (isSchemaDeprecated(schema)) { @@ -1314,7 +1315,13 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { }; for (const variant of variants) { - emitPyClass(variant.dataClassName, variant.dataSchema, ctx, variant.dataDescription); + emitPyClass( + variant.dataClassName, + variant.dataSchema, + ctx, + variant.dataDescription, + variant.dataExperimental + ); } const envelopeProperties = getPySharedEventEnvelopeProperties(schema, ctx); const envelopePropertiesWithoutDefaults = envelopeProperties.filter((property) => !property.hasDefault);