Skip to content

Use string enums for .NET session events#1226

Merged
stephentoub merged 7 commits intomainfrom
stephentoub/abort-enum-fix
May 8, 2026
Merged

Use string enums for .NET session events#1226
stephentoub merged 7 commits intomainfrom
stephentoub/abort-enum-fix

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

Newer Copilot runtime versions can add string enum values to persisted session events before the .NET SDK schema has been regenerated. Regular .NET enums throw during deserialization in that case, which can break GetMessagesAsync when replaying session history.

Summary

  • Update C# session-event codegen to emit string-backed readonly structs for schema enum values, preserving unknown runtime strings instead of throwing.
  • Keep RPC schema enums as regular .NET enums, since RPC method contracts are closed to the schema version.
  • Regenerate dotnet/src/Generated/SessionEvents.cs and hide generated JSON converter types from IntelliSense with EditorBrowsable(Never).
  • Add forward-compatibility coverage for unknown abort reason values.

Testing

  • dotnet test dotnet\test\GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~GitHub.Copilot.SDK.Test.Unit.ForwardCompatibilityTests|FullyQualifiedName~GitHub.Copilot.SDK.Test.Unit.SessionEventSerializationTests" --logger "console;verbosity=minimal"

Generate string-backed value types for session event schema enums so unknown values emitted by newer runtimes deserialize without throwing and preserve their raw string values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 7, 2026 22:29
@stephentoub stephentoub requested a review from a team as a code owner May 7, 2026 22:29
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the .NET session-events code generation to improve forward compatibility when newer Copilot runtimes emit previously-unknown enum string values into persisted session history, preventing deserialization failures in APIs like GetMessagesAsync.

Changes:

  • Update C# session-event codegen to generate string-backed readonly struct “enums” (with custom JsonConverter) for session event schema enums.
  • Regenerate dotnet/src/Generated/SessionEvents.cs to use the new string-enum structs and hide converter types from IntelliSense.
  • Add a unit test covering preservation of an unknown abort.reason value during deserialization.
Show a summary per file
File Description
scripts/codegen/csharp.ts Generates string-backed enum structs (with converters) for session-event enums.
dotnet/src/Generated/SessionEvents.cs Regenerated session event types using the new string-enum structs.
dotnet/test/Unit/ForwardCompatibilityTests.cs Adds coverage ensuring unknown enum values in known events are preserved.

Copilot's findings

  • Files reviewed: 2/3 changed files
  • Comments generated: 2

Comment thread scripts/codegen/csharp.ts Outdated
Comment thread scripts/codegen/csharp.ts Outdated
@stephentoub
Copy link
Copy Markdown
Collaborator Author

@brettcannon, we can do it as a separate PR, but we probably want to apply a forward-compat-focused fix here for Python as well?

Extend the generated string-backed value type enum model to RPC schema enums so unknown wire values from newer runtimes deserialize without throwing and preserve their raw values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

stephentoub and others added 2 commits May 7, 2026 22:34
Update generated C# string enum value types to back Value with a nullable field so default instances expose an empty string instead of null.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot mentioned this pull request May 8, 2026
@github-actions

This comment has been minimized.

stephentoub and others added 3 commits May 7, 2026 22:44
Move generated string enum JSON read/write validation into a handwritten SDK helper and have generated converters delegate to it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the C# generator so known string enum static properties are emitted after the constructor, then regenerate the C# protocol outputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the C# generator so generated string enum structs place only the backing field before the constructor, with Value and known static properties after it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Cross-SDK Consistency Review

This PR correctly fixes a forward-compatibility issue in the .NET SDK where regular C# enums would throw during deserialization of unknown session-event enum values. The approach (string-backed readonly structs) is idiomatic and well-designed.

Forward-compatibility status across SDKs

SDK Enum representation Unknown value behavior
TypeScript/Node.js String literal unions ("user_initiated" | "remote_command" | "user_abort") ✅ Strings at runtime — no deserialization error
Go type AbortReason string with typed constants ✅ Stores the raw string — no deserialization error
Rust Regular enums with #[serde(other)] Unknown variant ✅ Falls back to Unknown — no deserialization error
.NET (before this PR) Regular C# enums with JsonStringEnumConverter ❌ Threw on unknown values
.NET (after this PR) String-backed readonly structs ✅ Preserves unknown strings
Python Python Enum class ⚠️ Same issue as pre-fix .NET

Python has the same forward-compatibility gap

The Python codegen (scripts/codegen/python.ts) generates enums like:

class AbortReason(Enum):
    "Finite reason code describing why the current turn was aborted"
    USER_INITIATED = "user_initiated"
    REMOTE_COMMAND = "remote_command"
    USER_ABORT = "user_abort"

And deserializes them with:

def parse_enum(c: type[EnumT], x: Any) -> EnumT:
    assert isinstance(x, str)
    return c(x)   # ← raises ValueError for unknown values

If the runtime adds a new AbortReason (or any other session-event enum value) before the Python SDK schema is regenerated, parse_enum will raise ValueError and break session history replay — exactly the same scenario described in this PR's description.

Note: SessionEventType already handles this correctly via a hand-written _missing_ classmethod that returns UNKNOWN. But the ~20 other generated session-event enums (AbortReason, ElicitationCompletedAction, ExtensionsLoadedExtensionStatus, PermissionPromptRequestKind, etc.) do not have this protection.

Suggested fix for Python codegen

In scripts/codegen/python.ts, the getOrCreatePyEnum function (line ~719) could add a _missing_ handler to every generated enum:

lines.push(`    `@classmethod``);
lines.push(`    def _missing_(cls, value: object) -> "${enumName}":`);
lines.push(`        obj = object.__new__(cls)`);
lines.push(`        obj._value_ = value`);
lines.push(`        return obj`);

This would make all Python session-event enums forward-compatible, consistent with the fix applied to .NET in this PR. The regenerated python/copilot/generated/session_events.py would need to be committed as well.

This is not a blocker for merging this PR (which is purely a .NET change), but it would be worth a follow-up issue/PR to keep the Python SDK in parity.

Generated by SDK Consistency Review Agent for issue #1226 · ● 1.1M ·

@stephentoub stephentoub merged commit e759700 into main May 8, 2026
29 checks passed
@stephentoub stephentoub deleted the stephentoub/abort-enum-fix branch May 8, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants