/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ /** * Generate session event types for all SDKs from the JSON schema * * This script reads the session-events.schema.json from the @github/copilot package * (which should be npm linked from copilot-agent-runtime/dist-cli) and generates * TypeScript, Python, Go, and C# type definitions for all SDKs. * * Workflow: * 1. The schema is defined in copilot-agent-runtime using Zod schemas * 2. copilot-agent-runtime/script/generate-session-types.ts generates the JSON schema * 3. copilot-agent-runtime/esbuild.ts copies the schema to dist-cli/ * 4. This script reads the schema from the linked @github/copilot package * 5. Generates types for nodejs/src/generated/, python/copilot/generated/, go/generated/, and dotnet/src/Generated/ * * Usage: * npm run generate:session-types */ import { execFile } from "child_process"; import fs from "fs/promises"; import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { compile } from "json-schema-to-typescript"; import path from "path"; import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype } from "quicktype-core"; import { fileURLToPath } from "url"; import { promisify } from "util"; import { generateCSharpSessionTypes } from "./generate-csharp-session-types.js"; const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); async function getSchemaPath(): Promise { // Read from the @github/copilot package const schemaPath = path.join( __dirname, "../node_modules/@github/copilot/schemas/session-events.schema.json" ); try { await fs.access(schemaPath); console.log(`✅ Found schema at: ${schemaPath}`); return schemaPath; } catch (_error) { throw new Error( `Schema file not found at ${schemaPath}. ` + `Make sure @github/copilot package is installed or linked.` ); } } async function generateTypeScriptTypes(schemaPath: string) { console.log("🔄 Generating TypeScript types from JSON Schema..."); const schema = JSON.parse(await fs.readFile(schemaPath, "utf-8")) as JSONSchema7; const processedSchema = postProcessSchema(schema); const ts = await compile(processedSchema, "SessionEvent", { bannerComment: `/** * AUTO-GENERATED FILE - DO NOT EDIT * * Generated from: @github/copilot/session-events.schema.json * Generated by: scripts/generate-session-types.ts * Generated at: ${new Date().toISOString()} * * To update these types: * 1. Update the schema in copilot-agent-runtime * 2. Run: npm run generate:session-types */`, style: { semi: true, singleQuote: false, trailingComma: "all", }, additionalProperties: false, // Stricter types }); const outputPath = path.join(__dirname, "../src/generated/session-events.ts"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, ts, "utf-8"); console.log(`✅ Generated TypeScript types: ${outputPath}`); } /** * Event types to exclude from generation (internal/legacy types) */ const EXCLUDED_EVENT_TYPES = new Set(["session.import_legacy"]); /** * Post-process JSON Schema to make it compatible with quicktype * Converts boolean const values to enum with single value * Filters out excluded event types */ function postProcessSchema(schema: JSONSchema7): JSONSchema7 { if (typeof schema !== "object" || schema === null) { return schema; } const processed: JSONSchema7 = { ...schema }; // Handle const with boolean values - convert to enum with single value if ("const" in processed && typeof processed.const === "boolean") { const constValue = processed.const; delete processed.const; processed.enum = [constValue]; } // Recursively process all properties if (processed.properties) { const newProperties: Record = {}; for (const [key, value] of Object.entries(processed.properties)) { if (typeof value === "object" && value !== null) { newProperties[key] = postProcessSchema(value as JSONSchema7); } else { newProperties[key] = value; } } processed.properties = newProperties; } // Process items (for arrays) if (processed.items) { if (typeof processed.items === "object" && !Array.isArray(processed.items)) { processed.items = postProcessSchema(processed.items as JSONSchema7); } else if (Array.isArray(processed.items)) { processed.items = processed.items.map((item) => typeof item === "object" ? postProcessSchema(item as JSONSchema7) : item ) as JSONSchema7Definition[]; } } // Process anyOf, allOf, oneOf - also filter out excluded event types for (const combiner of ["anyOf", "allOf", "oneOf"] as const) { if (processed[combiner]) { processed[combiner] = processed[combiner]!.filter((item) => { if (typeof item !== "object") return true; const typeConst = (item as JSONSchema7).properties?.type; if (typeof typeConst === "object" && "const" in typeConst) { return !EXCLUDED_EVENT_TYPES.has(typeConst.const as string); } return true; }).map((item) => typeof item === "object" ? postProcessSchema(item as JSONSchema7) : item ) as JSONSchema7Definition[]; } } // Process definitions if (processed.definitions) { const newDefinitions: Record = {}; for (const [key, value] of Object.entries(processed.definitions)) { if (typeof value === "object" && value !== null) { newDefinitions[key] = postProcessSchema(value as JSONSchema7); } else { newDefinitions[key] = value; } } processed.definitions = newDefinitions; } // Process additionalProperties if it's a schema if (typeof processed.additionalProperties === "object") { processed.additionalProperties = postProcessSchema( processed.additionalProperties as JSONSchema7 ); } return processed; } async function generatePythonTypes(schemaPath: string) { console.log("🔄 Generating Python types from JSON Schema..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); const schema = JSON.parse(schemaContent) as JSONSchema7; // Resolve the $ref at the root level and get the actual schema const resolvedSchema = (schema.definitions?.SessionEvent as JSONSchema7) || schema; // Post-process to fix boolean const values const processedSchema = postProcessSchema(resolvedSchema); const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); await schemaInput.addSource({ name: "SessionEvent", schema: JSON.stringify(processedSchema), }); const inputData = new InputData(); inputData.addInput(schemaInput); const result = await quicktype({ inputData, lang: "python", rendererOptions: { "python-version": "3.7", }, }); let generatedCode = result.lines.join("\n"); // Fix Python dataclass field ordering issue: // Quicktype doesn't support default values in schemas, so it generates "arguments: Any" // (without default) that comes after Optional fields (with defaults), violating Python's // dataclass rules. We post-process to add "= None" to these unconstrained "Any" fields. generatedCode = generatedCode.replace(/: Any$/gm, ": Any = None"); // Add UNKNOWN enum value and _missing_ handler for forward compatibility // This ensures that new event types from the server don't cause errors generatedCode = generatedCode.replace( /^(class SessionEventType\(Enum\):.*?)(^\s*\n@dataclass)/ms, `$1 # UNKNOWN is used for forward compatibility - new event types from the server # will map to this value instead of raising an error UNKNOWN = "unknown" @classmethod def _missing_(cls, value: object) -> "SessionEventType": """Handle unknown event types gracefully for forward compatibility.""" return cls.UNKNOWN $2` ); const banner = `""" AUTO-GENERATED FILE - DO NOT EDIT Generated from: @github/copilot/session-events.schema.json Generated by: scripts/generate-session-types.ts Generated at: ${new Date().toISOString()} To update these types: 1. Update the schema in copilot-agent-runtime 2. Run: npm run generate:session-types """ `; const outputPath = path.join(__dirname, "../../python/copilot/generated/session_events.py"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, banner + generatedCode, "utf-8"); console.log(`✅ Generated Python types: ${outputPath}`); } async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); console.log(`✅ Formatted Go file with go fmt: ${filePath}`); } catch (error: unknown) { if (error instanceof Error && "code" in error) { if (error.code === "ENOENT") { console.warn(`⚠️ go fmt not available - skipping formatting for ${filePath}`); } else { console.warn(`⚠️ go fmt failed for ${filePath}: ${error.message}`); } } } } async function generateGoTypes(schemaPath: string) { console.log("🔄 Generating Go types from JSON Schema..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); const schema = JSON.parse(schemaContent) as JSONSchema7; // Resolve the $ref at the root level and get the actual schema const resolvedSchema = (schema.definitions?.SessionEvent as JSONSchema7) || schema; // Post-process to fix boolean const values const processedSchema = postProcessSchema(resolvedSchema); const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); await schemaInput.addSource({ name: "SessionEvent", schema: JSON.stringify(processedSchema), }); const inputData = new InputData(); inputData.addInput(schemaInput); const result = await quicktype({ inputData, lang: "go", rendererOptions: { package: "copilot", }, }); const generatedCode = result.lines.join("\n"); const banner = `// AUTO-GENERATED FILE - DO NOT EDIT // // Generated from: @github/copilot/session-events.schema.json // Generated by: scripts/generate-session-types.ts // Generated at: ${new Date().toISOString()} // // To update these types: // 1. Update the schema in copilot-agent-runtime // 2. Run: npm run generate:session-types `; const outputPath = path.join(__dirname, "../../go/generated_session_events.go"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, banner + generatedCode, "utf-8"); console.log(`✅ Generated Go types: ${outputPath}`); await formatGoFile(outputPath); } async function formatCSharpFile(filePath: string): Promise { try { // Get the directory containing the .csproj file const projectDir = path.join(__dirname, "../../dotnet/src"); const projectFile = path.join(projectDir, "GitHub.Copilot.SDK.csproj"); // dotnet format needs to be run from the project directory or with --workspace await execFileAsync("dotnet", ["format", projectFile, "--include", filePath]); console.log(`✅ Formatted C# file with dotnet format: ${filePath}`); } catch (error: unknown) { if (error instanceof Error && "code" in error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { console.warn( `⚠️ dotnet format not available - skipping formatting for ${filePath}` ); } else { console.warn( `⚠️ dotnet format failed for ${filePath}: ${(error as Error).message}` ); } } } } async function generateCSharpTypes(schemaPath: string) { console.log("🔄 Generating C# types from JSON Schema..."); const schemaContent = await fs.readFile(schemaPath, "utf-8"); const schema = JSON.parse(schemaContent) as JSONSchema7; const generatedAt = new Date().toISOString(); const generatedCode = generateCSharpSessionTypes(schema, generatedAt); const outputPath = path.join(__dirname, "../../dotnet/src/Generated/SessionEvents.cs"); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, generatedCode, "utf-8"); console.log(`✅ Generated C# types: ${outputPath}`); await formatCSharpFile(outputPath); } async function main() { try { const schemaPath = await getSchemaPath(); await generateTypeScriptTypes(schemaPath); await generatePythonTypes(schemaPath); await generateGoTypes(schemaPath); await generateCSharpTypes(schemaPath); console.log("✅ Type generation complete!"); } catch (error) { console.error("❌ Type generation failed:", error); process.exit(1); } } main();