Skip to content

C# API review fixes#1343

Merged
SteveSandersonMS merged 40 commits into
mainfrom
stevesandersonms/stevesa-api-review-fixes
May 21, 2026
Merged

C# API review fixes#1343
SteveSandersonMS merged 40 commits into
mainfrom
stevesandersonms/stevesa-api-review-fixes

Conversation

@SteveSandersonMS
Copy link
Copy Markdown
Contributor

@SteveSandersonMS SteveSandersonMS commented May 20, 2026

Summary

API review fixes for the .NET SDK only. Sources combined into this PR:

  • ApiView comments classified into "SDK-side fixes" (the runtime-side fixes landed via copilot-agent-runtime#8283).
  • The api_review_csharp.md review doc.
  • Follow-on cleanups discovered while implementing the above.

Other SDKs are not touched here; once we are happy with the .NET shape we will port what makes sense.

What changed (by phase)

Phase Theme
3 Sealed every hand-written public type that does not need a derived class.
4a Simple property/method renames (e.g. spelling, casing, plurals).
4b Expanded abbreviations in SessionFsProvider.
4c Shape changes: Streaming flag, tools list shape, ToolBinaryResultType cleanup, misc.
4d/4e Removed the redundant CopilotClient.State enum; retyped LogLevel to a strongly-typed CopilotLogLevel value type.
4f Generic client.OnLifecycle<T> / session.On<T>; lifecycle events are now a typed polymorphic hierarchy (SessionCreatedEvent, etc.) instead of stringly-typed discriminators.
4g DateTimeOffset timestamps; PermissionRequestResult.Feedback cleanup.
5 Extracted SessionConfigBase between SessionConfig / ResumeSessionConfig; sealed all config classes.
6 Removed every hand-written named delegate type in favour of plain Func<...>.
7 SendAsync(string) / SendAndWaitAsync(string) convenience overloads.
8 README updates to match.
9 New RuntimeConnection discriminated config: replaces the flat CliPath/CliArgs/Port/UseStdio/CliUrl/TcpConnectionToken properties on CopilotClientOptions with a single Connection of type RuntimeConnection, constructed via factories: RuntimeConnection.Stdio(...), RuntimeConnection.Tcp(...), RuntimeConnection.Uri(...). Also renames ActualPort -> RuntimePort and CopilotHome -> BaseDirectory.

The test harness also got a small UX improvement: Ctx.CreateClient(useStdio: ..., options: ... { Connection = ... }) now throws eagerly if both are supplied. This caught a real bug introduced by Phase 9 bulk rewrite.

Notable thing we explicitly did NOT do: Phase 1

Phase 1 was originally going to retype every "opaque JSON" RPC field from object to System.Text.Json.JsonElement, on the theory that JsonElement is more honest about wire shape than object (which cannot be safely cast). The change was reverted on this branch (see commits 129c281e and 074ace71) for two reasons:

  1. The underlying problem is in the runtime schema, not the SDK. Several RPC parameters/results are polymorphic anyOf unions that have no idiomatic mapping in any of our SDK languages. Tracked in copilot-agent-runtime#8375: the schema generator should reject shapes that have no idiomatic mapping in C#/Go/Python/Rust, and require an explicit opt-in for cases that genuinely are meant to be opaque JSON. Once that lands, codegen will produce real typed hierarchies and the JsonElement workaround stops being needed.

  2. Phase 1 asymmetry created a worse consumer experience for inputs. Changing input parameters from object to JsonElement forced consumers to serialize themselves via their own JsonSerializerOptions, which silently emitted "headers": null and similar (because WhenWritingNull is not a default) and caused the runtime to reject otherwise-valid configs. On the previous object path, the SDK own JsonSerializerContext (which sets WhenWritingNull) was used, so this never bit consumers. We could have applied per-property [JsonIgnore(WhenWritingNull)] workarounds, but they are band-aids; the right fix is the schema change above. Reverting now keeps the API symmetric and aligned with main.

We will revisit JsonElement once #8375 lands and codegen can emit real types.

Risk / compatibility

This is a breaking-change PR by design. All E2E and unit tests have been updated in lock-step. The dotnet/samples and docs/ C# snippets have also been migrated.

Two issues filed against the runtime as follow-ups to this work:

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread dotnet/src/Client.cs Fixed
Comment thread dotnet/src/Client.cs

// Protocol v2 backward-compatibility adapters

public async ValueTask<ToolCallResponseV2> OnToolCallV2(string sessionId,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need to maintain the V2 support?

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1343 · ● 946.3K

Comment thread dotnet/src/Client.cs
/// </code>
/// </example>
public IDisposable On(string eventType, Action<SessionLifecycleEvent> handler)
public IDisposable OnLifecycle<T>(Action<T> handler) where T : SessionLifecycleEvent
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: The lifecycle event subscription API has changed from two overloads (On(handler) and On(eventType, handler)) to a single generic method OnLifecycle<T>(handler). The other SDKs use a different pattern:

  • Node.js: client.on(handler) and client.on(eventType, handler)
  • Python: client.on(handler) and client.on(event_type, handler)
  • Go: client.On(handler) and client.OnEventType(eventType, handler)

The generic type approach is idiomatic .NET and has real advantages (compile-time type safety), but the method name OnLifecycle also diverges from On / on. If this design is retained, it's worth documenting that this is a .NET-specific pattern for filtering by event type, and considering whether the approach (if not the exact API shape) should influence the other SDKs.

Comment thread dotnet/src/Types.cs Outdated
/// When provided, the server will route <c>exitPlanMode.request</c> callbacks to this handler.
/// </summary>
public ExitPlanModeHandler? OnExitPlanMode { get; set; }
public ExitPlanModeHandler? OnExitPlanModeRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: The property has been renamed from OnExitPlanMode to OnExitPlanModeRequest, but the equivalent property in the other SDKs still uses the original name (without the Request suffix):

  • Node.js: SessionConfig.onExitPlanMode
  • Python: create_session(..., on_exit_plan_mode=...)
  • Go: SessionConfig.OnExitPlanMode

If this rename is intentional as an improvement to the API, the same change should be considered for the other SDKs. If the goal is cross-SDK consistency, the other SDKs should eventually align with this new naming (or this property should revert to OnExitPlanMode).

Comment thread dotnet/src/Types.cs
/// This is used only when <see cref="CopilotClientOptions.SessionFs"/> is configured.
/// </summary>
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsHandler { get; set; }
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsProvider { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This has been renamed from CreateSessionFsHandler to CreateSessionFsProvider, but all other SDKs use the ...Handler naming:

  • Node.js: SessionConfig.createSessionFsHandler
  • Python: create_session(..., create_session_fs_handler=...)
  • Go: SessionConfig.CreateSessionFsHandler

Interestingly, SessionFsProvider is already the name of the returned type in Node.js and Python too, so CreateSessionFsProvider could be seen as more descriptive. If this name is preferred, it may be worth aligning the other SDKs.

Comment thread dotnet/src/Types.cs
/// Default: false (resume event is emitted).
/// </summary>
public bool DisableResume { get; set; }
public bool SuppressResumeEvent { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This has been renamed from DisableResume to SuppressResumeEvent, but:

  • Node.js: ResumeSessionConfig.disableResume
  • Go: ResumeSessionConfig.DisableResume
  • Python: Planned as disable_resume (TODO comment in source)

The new name SuppressResumeEvent is arguably more descriptive, but it diverges from the other SDK naming. If this naming is preferred, the other SDKs should be updated to match.

Comment thread dotnet/src/Types.cs Outdated
/// When provided, the server will route <c>autoModeSwitch.request</c> callbacks to this handler.
/// </summary>
public AutoModeSwitchHandler? OnAutoModeSwitch { get; set; }
public AutoModeSwitchHandler? OnAutoModeSwitchRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: Same naming divergence as OnExitPlanModeRequest — the other SDKs use onAutoModeSwitch / on_auto_mode_switch / OnAutoModeSwitch (without the Request suffix). These two renames should be treated together if they're carried through to the other SDKs.

Comment thread dotnet/src/Session.cs Outdated
Comment thread dotnet/src/Types.cs
[JsonConverter(typeof(JsonStringEnumConverter<ConnectionState>))]
public enum ConnectionState
[DebuggerDisplay("{Value,nq}")]
public readonly struct CopilotLogLevel : IEquatable<CopilotLogLevel>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is fine if you think it's useful to maintain a separate type, but we're already using ILogger{Factory}... should we just use LogLevel here? Internally we can translate it to whatever the runtime needs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I started with LogLevel but started thinking nothing stops the runtime from coming up with new loglevel values at any time that could have any semantics, without being constrained to things that match a .NET LogLevel. My guess is this won't inconvenience .NET devs too much since they aren't programmatically mapping some other loglevel into this in most cases, so they will still just pick whatever level they think is right. We could add an implicit conversion from LogLevel later if we want.

@SteveSandersonMS SteveSandersonMS force-pushed the stevesandersonms/stevesa-api-review-fixes branch from 9f16314 to a64803a Compare May 20, 2026 14:18
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1343 · ● 1.3M

Comment thread dotnet/src/Session.cs
/// </code>
/// </example>
public async Task<IReadOnlyList<SessionEvent>> GetMessagesAsync(CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<SessionEvent>> GetEventsAsync(CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency: GetEventsAsync vs getMessages

All other SDK implementations use the "Messages" naming for this method:

  • Node.js: session.getMessages() (nodejs/src/session.ts)
  • Python: session.get_messages() (python/copilot/session.py)
  • Go: session.GetMessages(ctx) (go/session.go)
  • Rust: GetMessagesResponse struct (rust/src/types.rs)

The .NET rename to GetEventsAsync diverges from the cross-SDK convention. Consider whether the other SDKs should also be updated to use "Events" naming, or whether this should remain GetMessagesAsync for consistency.

Comment thread dotnet/src/Types.cs Outdated
/// When provided, the server will route <c>exitPlanMode.request</c> callbacks to this handler.
/// </summary>
public ExitPlanModeHandler? OnExitPlanMode { get; set; }
public ExitPlanModeHandler? OnExitPlanModeRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency: OnExitPlanModeRequest vs onExitPlanMode

Other SDKs use onExitPlanMode (without the Request suffix):

  • Node.js: onExitPlanMode?: ExitPlanModeHandler (nodejs/src/types.ts)
  • Python: on_exit_plan_mode: ExitPlanModeHandler (python/copilot/session.py)
  • Go: OnExitPlanMode ExitPlanModeHandler (go/types.go)
  • Rust: on_exit_plan_mode method (rust/src/handler.rs)

The added Request suffix here (and for OnAutoModeSwitchRequest below) makes the semantics more explicit, but creates a naming divergence from all other language SDKs. If the "Request" suffix is the preferred direction, the other SDKs should be updated to match.

Comment thread dotnet/src/Types.cs
/// Default: false (resume event is emitted).
/// </summary>
public bool DisableResume { get; set; }
public bool SuppressResumeEvent { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency: SuppressResumeEvent vs disableResume

Other SDKs all use disableResume / disable_resume:

  • Node.js: disableResume?: boolean (nodejs/src/types.ts)
  • Python: disable_resume: bool (python/copilot/session.py)
  • Go: DisableResume bool (go/types.go)
  • Rust: disable_resume: Option<bool> (rust/src/types.rs)

SuppressResumeEvent is semantically more descriptive, but it's a significant divergence from the cross-SDK disableResume convention. This is worth aligning across all SDKs if the new name is preferred.

Comment thread dotnet/src/Types.cs
/// </summary>
[JsonPropertyName("maxPromptTokens")]
public int? MaxInputTokens { get; set; }
public int? MaxPromptTokens { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency: MaxPromptTokens vs maxInputTokens

Other SDKs expose this as maxInputTokens / MaxInputTokens in their public APIs (mapping to maxPromptTokens on the wire):

  • Node.js: maxInputTokens?: number in ProviderConfig (nodejs/src/types.ts:1676), mapped to maxPromptTokens wire format in client.ts
  • Go: MaxInputTokens int in ProviderConfig (go/types.go:991), tagged with json:"maxPromptTokens,omitempty"

The .NET SDK now exposes MaxPromptTokens directly, which aligns with the wire format but diverges from the other SDKs' public APIs. If MaxPromptTokens is the better name, consider updating Node.js and Go as well.

@github-actions

This comment has been minimized.

Comment thread dotnet/src/Client.cs Fixed
Comment thread dotnet/src/Client.cs Fixed
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1343 · ● 1.2M

Comment thread dotnet/src/Session.cs
/// </code>
/// </example>
public async Task<IReadOnlyList<SessionEvent>> GetMessagesAsync(CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<SessionEvent>> GetEventsAsync(CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This rename from GetMessagesAsync to GetEventsAsync creates a naming inconsistency with the other SDK implementations, which all still use the getMessages / get_messages / GetMessages name:

  • Node.js: session.getMessages() (nodejs/src/session.ts)
  • Python: session.get_messages() (python/copilot/session.py)
  • Go: session.GetMessages() (go/session.go)

If GetEventsAsync is the better name (since the method returns all session events, not just messages), it would be worth tracking a follow-up to rename the equivalent methods in the other SDKs for consistency. Alternatively, aligning back to GetMessagesAsync would keep cross-SDK naming in sync today.

Comment thread dotnet/src/Types.cs
/// Working directory for the runtime process.
/// </summary>
public string? Cwd { get; set; }
public string? WorkingDirectory { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: Three CopilotClientOptions properties have been renamed in this PR relative to the other SDKs:

.NET (new) Node.js Python Go
WorkingDirectory cwd cwd Cwd
BaseDirectory copilotHome copilot_home CopilotHome
EnableRemoteSessions (line 331) remote remote Remote

The new .NET names are arguably more descriptive/idiomatic for C#, but they diverge from the cross-SDK conceptual alignment. If there's intent to keep the API surface consistent in meaning (even if casing differs per language convention), it may be worth tracking follow-ups to update the other SDKs to similarly rename cwd → workingDirectory, copilotHome → baseDirectory, remote → enableRemoteSessions.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

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

Comment thread dotnet/src/Session.cs
/// </code>
/// </example>
public async Task<IReadOnlyList<SessionEvent>> GetMessagesAsync(CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<SessionEvent>> GetEventsAsync(CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This renames GetMessagesAsyncGetEventsAsync, but the other three SDKs still expose this method as getMessages / get_messages / GetMessages:

  • Node.js (nodejs/src/session.ts): session.getMessages()
  • Python (python/copilot/session.py): session.get_messages()
  • Go (go/session.go): session.GetMessages(ctx)

The rename is arguably more accurate (the method returns all session events, not just messages), but it creates a cross-SDK naming divergence. Is there a plan to align the other SDKs to getEvents / get_events / GetEvents in a follow-up PR?

Comment thread dotnet/src/Types.cs
/// This is used only when <see cref="CopilotClientOptions.SessionFs"/> is configured.
/// </summary>
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsHandler { get; set; }
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsProvider { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This renames CreateSessionFsHandlerCreateSessionFsProvider, but other SDKs still use the "Handler" suffix for this config property:

  • Node.js (nodejs/src/client.ts): createSessionFsHandler
  • Python (python/copilot/session.py): create_session_fs_handler
  • Go (go/types.go): CreateSessionFsHandler

Worth tracking whether the other SDKs should also rename to "Provider" for consistency.

Comment thread dotnet/src/Types.cs
/// </summary>
public ExitPlanModeHandler? OnExitPlanMode { get; set; }
/// <summary>Handler for exit-plan-mode requests from the server.</summary>
public Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? OnExitPlanModeRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: This adds a Request suffix (OnExitPlanModeOnExitPlanModeRequest, OnAutoModeSwitchOnAutoModeSwitchRequest), but other SDKs don't have this suffix on their equivalent config properties:

  • Node.js (nodejs/src/client.ts): onExitPlanMode, onAutoModeSwitch
  • Python (python/copilot/session.py): on_exit_plan_mode, on_auto_mode_switch
  • Go (go/types.go): OnExitPlanMode, OnAutoModeSwitch

Not a blocker for a .NET API review, but worth noting for eventual cross-SDK alignment.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1343 · ● 1.4M

Comment thread dotnet/src/Types.cs
/// This is used only when <see cref="CopilotClientOptions.SessionFs"/> is configured.
/// </summary>
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsHandler { get; set; }
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsProvider { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename CreateSessionFsHandlerCreateSessionFsProvider diverges from the other SDKs:

  • Node.js (client.ts): createSessionFsHandler
  • Python (session.py): create_session_fs_handler
  • Go (types.go): CreateSessionFsHandler

CreateSessionFsProvider is semantically more accurate (a factory that creates a provider, not a handler), so this is a good rename worth carrying over to the other SDKs.

Comment thread dotnet/src/Types.cs
/// </summary>
public ExitPlanModeHandler? OnExitPlanMode { get; set; }
/// <summary>Handler for exit-plan-mode requests from the server.</summary>
public Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? OnExitPlanModeRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename OnExitPlanModeOnExitPlanModeRequest diverges from the other SDKs:

  • Node.js (client.ts): onExitPlanMode
  • Python (session.py): on_exit_plan_mode
  • Go (types.go): OnExitPlanMode

The Request suffix is a nice clarification (it handles a request from the server), so it would be good to track follow-up updates for the other SDKs to match.

Comment thread dotnet/src/Client.cs
/// Gets the actual TCP port the runtime is listening on, if using TCP transport.
/// </summary>
public int? ActualPort => _actualPort;
public int? RuntimePort => _actualPort;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename ActualPortRuntimePort diverges from the other SDKs:

  • Python (client.py): actual_port (public property)
  • Go (client.go): ActualPort() (public method)
  • Node.js: exposes port only internally, but error messages say actualPort

RuntimePort aligns better with the new RuntimeConnection terminology in this PR, so it makes sense here. Consider whether Go and Python should also rename to runtime_port / RuntimePort for consistency.

Comment thread dotnet/src/Types.cs
/// </summary>
public AutoModeSwitchHandler? OnAutoModeSwitch { get; set; }
/// <summary>Handler for auto-mode-switch requests from the server.</summary>
public Func<AutoModeSwitchRequest, AutoModeSwitchInvocation, Task<AutoModeSwitchResponse>>? OnAutoModeSwitchRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename OnAutoModeSwitchOnAutoModeSwitchRequest diverges from the other SDKs:

  • Node.js (client.ts): onAutoModeSwitch
  • Python (session.py): on_auto_mode_switch
  • Go (types.go): OnAutoModeSwitch

Same as above — the Request suffix is an improvement worth propagating to the other SDKs.

Comment thread dotnet/src/Types.cs
/// when connecting to an external runtime via <see cref="RuntimeConnection.Uri(string, string?)"/>.
/// </summary>
public bool Remote { get; set; }
public bool EnableRemoteSessions { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename RemoteEnableRemoteSessions diverges from the other SDKs:

  • Node.js (client.ts): remote
  • Python (client.py): remote
  • Go (types.go): Remote

EnableRemoteSessions is more self-descriptive. Consider a follow-up to rename in Node.js, Python and Go as well.

Comment thread dotnet/src/Types.cs
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("AutoRestart has no effect and will be removed in a future release.")]
public bool AutoRestart { get; set; }
public string? BaseDirectory { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK inconsistency introduced 🔍

The rename CopilotHomeBaseDirectory diverges from the other SDKs:

  • Node.js (client.ts): copilotHome
  • Python (client.py): copilot_home
  • Go (types.go): CopilotHome

BaseDirectory is a more neutral term (not coupling to the internal COPILOT_HOME environment variable name), so it's worth tracking as a potential follow-up rename in the other SDKs.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

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

Comment thread dotnet/src/Types.cs
/// </summary>
public ExitPlanModeHandler? OnExitPlanMode { get; set; }
/// <summary>Handler for exit-plan-mode requests from the server.</summary>
public Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? OnExitPlanModeRequest { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: The .NET rename from OnExitPlanModeOnExitPlanModeRequest (and OnAutoModeSwitchOnAutoModeSwitchRequest on the next line) diverges from all other SDKs:

  • Node.js: onExitPlanMode / onAutoModeSwitch
  • Python: on_exit_plan_mode / on_auto_mode_switch
  • Go: OnExitPlanMode / OnAutoModeSwitch

If the *Request suffix is the correct semantic name (these are requests that the server sends to the client), it would be worth applying the same rename in the other SDKs too. If it's a .NET-only style choice, that's fine but worth documenting the intentional divergence.

Comment thread dotnet/src/Types.cs
/// This is used only when <see cref="CopilotClientOptions.SessionFs"/> is configured.
/// </summary>
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsHandler { get; set; }
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsProvider { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: CreateSessionFsProvider diverges from the name used in all other SDKs:

  • Node.js: createSessionFsHandler
  • Python: create_session_fs_handler
  • Go: CreateSessionFsHandler

If this rename from HandlerProvider is intentional (the field holds a factory for a SessionFsProvider, so "Provider" is semantically clearer), the same rename could be applied to the other SDKs for consistency.

Comment thread dotnet/src/Client.cs
/// Gets the actual TCP port the runtime is listening on, if using TCP transport.
/// </summary>
public int? ActualPort => _actualPort;
public int? RuntimePort => _actualPort;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: RuntimePort diverges from the name used in other SDKs:

  • Node.js: actualPort (private field — not exposed as a public property)
  • Python: actual_port (public property)
  • Go: ActualPort() (public method)

If RuntimePort is the preferred name going forward, the Python and Go SDKs could be updated. Node.js could also expose this publicly.

Comment thread dotnet/src/Session.cs
/// </code>
/// </example>
public IDisposable On(SessionEventHandler handler)
public IDisposable On<T>(Action<T> handler) where T : SessionEvent
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK opportunity: The new On<T>() generic method (where T : SessionEvent) allows callers to filter event subscriptions to a specific event type at compile time — a nice improvement. This capability doesn't currently exist in other SDKs:

  • Node.js: session.on(handler) — all events, no type filtering
  • Python: session.on(handler) — all events, no type filtering
  • Go: session.On(handler) — all events, no type filtering

Worth considering adding equivalent typed filtering to the other SDKs in a follow-up.

Comment thread dotnet/src/Types.cs
/// when connecting to an external runtime via <see cref="RuntimeConnection.Uri(string, string?)"/>.
/// </summary>
public bool Remote { get; set; }
public bool EnableRemoteSessions { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: EnableRemoteSessions diverges from the name used in all other SDKs:

  • Node.js: remote
  • Python: remote
  • Go: Remote

Similarly, BaseDirectory (line ~260) diverges from CopilotHome / copilotHome / copilot_home in the other SDKs, and WorkingDirectory at the client options level (line ~251) diverges from Cwd / cwd in Go, Node.js, and Python.

If these renames are considered improvements (e.g., EnableRemoteSessions is more self-documenting than Remote), it would be worth aligning the other SDKs in follow-up PRs.

Comment thread dotnet/src/Session.cs
/// <param name="prompt">The user message text.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that resolves with the message ID.</returns>
public Task<string> SendAsync(string prompt, CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK opportunity: The new SendAsync(string prompt) and SendAndWaitAsync(string prompt) string overloads are a convenience improvement. Status across other SDKs:

  • Python: Already accepts prompt: str directly — consistent ✅
  • Go: Send(ctx, MessageOptions) requires a struct — no string shorthand
  • Node.js: send({ prompt: "..." }) requires an object — no string shorthand

Go and Node.js could benefit from adding equivalent string convenience overloads/variants.

@SteveSandersonMS SteveSandersonMS changed the title Dotnet SDK: API review fixes C# API review fixes May 20, 2026
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1343 · ● 1.9M

Comment thread dotnet/src/Types.cs
/// Default: false (resume event is emitted).
/// </summary>
public bool DisableResume { get; set; }
public bool SuppressResumeEvent { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: SuppressResumeEvent diverges from the other SDK implementations, which all use DisableResume / disableResume / disable_resume:

  • Go: DisableResume bool (go/types.go:932)
  • Python: disable_resume: bool (python/copilot/session.py:1062)
  • Node.js: disableResume?: boolean (nodejs/src/types.ts:1593)

Worth aligning on a single canonical name if this is an intentional rename, or tracking a follow-up to update the other SDKs.

Comment thread dotnet/src/Session.cs
/// </code>
/// </example>
public async Task<IReadOnlyList<SessionEvent>> GetMessagesAsync(CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<SessionEvent>> GetEventsAsync(CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: GetEventsAsync diverges from the other SDK implementations, which all use GetMessages / get_messages:

  • Go: func (s *Session) GetMessages(ctx context.Context) (go/session.go:1170)
  • Python: async def get_messages(self) (python/copilot/session.py:2217)
  • Node.js: async getMessages() (nodejs/src/session.ts:994)

If GetEvents is the preferred canonical name going forward, the other three SDKs would need follow-up updates.

Comment thread dotnet/src/Types.cs
/// </summary>
[JsonPropertyName("maxPromptTokens")]
public int? MaxInputTokens { get; set; }
public int? MaxPromptTokens { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: MaxPromptTokens diverges from the other SDKs' public ProviderConfig API, which all expose this as MaxInputTokens / maxInputTokens / max_input_tokens (each transparently remapping to the wire field maxPromptTokens):

  • Go: MaxInputTokens intjson:"maxPromptTokens" (go/types.go:991)
  • Python: max_input_tokens: int (mapped to wire maxPromptTokens in client.py:2533)
  • Node.js: maxInputTokens?: number (mapped to wire maxPromptTokens in client.ts:78)

Renaming only the .NET property creates an inconsistency. If MaxPromptTokens is the preferred name, the other SDKs should follow.

Comment thread dotnet/src/Client.cs
/// Gets the actual TCP port the runtime is listening on, if using TCP transport.
/// </summary>
public int? ActualPort => _actualPort;
public int? RuntimePort => _actualPort;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: RuntimePort diverges from the other SDKs, which use ActualPort / actual_port:

  • Go: func (c *Client) ActualPort() int (go/client.go:1300)
  • Python: def actual_port(self) -> int | None (python/copilot/client.py:1026)

If RuntimePort is the preferred name, consider updating the Go and Python equivalents in a follow-up.

Comment thread dotnet/src/Types.cs
/// This is used only when <see cref="CopilotClientOptions.SessionFs"/> is configured.
/// </summary>
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsHandler { get; set; }
public Func<CopilotSession, SessionFsProvider>? CreateSessionFsProvider { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cross-SDK consistency note: CreateSessionFsProvider diverges from the other SDKs, which use createSessionFsHandler / create_session_fs_handler:

  • Node.js: createSessionFsHandler? in SessionConfig (nodejs/src/types.ts:1545)
  • Python: create_session_fs_handler: CreateSessionFsHandler (python/copilot/session.py:994)

This is a minor naming difference, but worth tracking for eventual alignment.

SteveSandersonMS and others added 6 commits May 21, 2026 09:57
…ForUri

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Missed in 5e41280; this file was excluded from the original add because
of an earlier uncommitted debug hunk that has since been committed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The package name is not changing; only the C# namespace lost the .SDK
suffix. My earlier bulk rename incorrectly stripped .SDK from these
three docs install commands too.

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

After the namespace rename, GeneratedStringEnumJson sits in the parent
namespace of the generated converters (GitHub.Copilot.Rpc), so the
unqualified name resolves correctly. Removes leftover qualifier that
was needed to disambiguate against the old GitHub.Copilot.SDK namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Picks up @github/copilot 1.0.51 schema changes (#1351, #1353) and
MCPStdioServerConfig.args optional (#1347).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS force-pushed the stevesandersonms/stevesa-api-review-fixes branch from c1e5d5c to 3e1a3e1 Compare May 21, 2026 08:59
@github-actions

This comment has been minimized.

The runtime tracks 'last session id' as a persistent value (it survives
session deletion), so this test fails if any other test in the class
has run first. Latent on main because xUnit happened to run it first;
exposed on this branch by the namespace rename which changes the
fully-qualified type name and therefore the discovered test order.
Cleaning up any pre-existing sessions before reading the value lets
the test pass regardless of order.

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

Cross-SDK Consistency Review

This PR is correctly scoped to the .NET SDK only, as stated in the description. The changes are high quality and well-considered. Here's a consistency tracking summary for when the improvements are ported to other SDKs.


✅ Already consistent: Typed session event filtering

The Node.js SDK already has an equivalent to the new session.On<T>() pattern (via session.on<K extends SessionLifecycleEventType>(eventType, handler) with TypeScript-typed handlers), so that aspect will be easy to align. Python uses a string-based session.on("event.type", handler) approach that also provides filtering. Go currently has no type-filtered overload.


🔄 Divergences to track for future porting

1. RuntimeConnection discriminated union (Phase 9) — most impactful

.NET now uses:

new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: "...") }
new CopilotClientOptions { Connection = RuntimeConnection.ForUri("localhost:3000") }

The other SDKs still use flat, mutually-exclusive options:

  • Node.js: { cliPath, cliUrl, useStdio } (validated at runtime with throw)
  • Python: ClientConfig(cli_path=..., url=..., use_stdio=...) (validated at __init__)
  • Go: ClientOptions{CLIPath: ..., CLIUrl: ..., UseStdio: ...} (panics on conflict)

The discriminated union approach is idiomatic in TypeScript (tagged unions / satisfies) and is expressible in Python (dataclasses with __post_init__ validation or a Union type). It's worth considering for all SDKs when porting, especially to eliminate the runtime-error validation currently scattered across constructors.

2. RuntimePort rename (was ActualPort)

  • .NET: client.RuntimePort (renamed in this PR)
  • Go: client.ActualPort()
  • Python: client.actual_port

When porting, runtime_port / RuntimePort would be more consistent with the .NET rename.

3. BaseDirectory rename (was CopilotHome)

  • .NET: CopilotClientOptions.BaseDirectory (renamed in this PR)
  • Go: ClientOptions.CopilotHome
  • Python: ClientConfig.copilot_home

When porting, consider aligning to base_directory / BaseDirectory.

4. SessionConfigBase extraction (Phase 5)

.NET extracted a SessionConfigBase shared between SessionConfig and ResumeSessionConfig. Python and Go have separate config types with duplicated fields. This is a minor point but worth harmonising when porting.


No action needed on this PR — these are all intentional and tracked divergences. Just leaving this note to make the porting work easier. 🚀

Generated by SDK Consistency Review Agent for issue #1343 · ● 425.3K ·

@SteveSandersonMS SteveSandersonMS merged commit c490d7a into main May 21, 2026
44 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesandersonms/stevesa-api-review-fixes branch May 21, 2026 09:47
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
- Remove autoStart and autoRestart from CopilotClientOptions. The client
  now always starts on first createSession/resumeSession; users can still
  call client.start() explicitly for eager startup.
- Make MCPServerConfigBase.tools optional (undefined = all, [] = none).
- Fix streaming JSDoc block comment that wasn't attached due to single-star.

Mirrors C# PR #1343 Phase 4c.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
- Split SessionLifecycleEvent into a discriminated union of
  SessionCreatedEvent / SessionDeletedEvent / SessionUpdatedEvent /
  SessionForegroundEvent / SessionBackgroundEvent.
- Promote the metadata payload into a named SessionLifecycleEventMetadata
  interface; metadata is required on non-delete variants and absent on
  session.deleted.
- Convert metadata.startTime and metadata.modifiedTime from string to Date,
  matching SessionMetadata. Parse on receipt in
  client.handleSessionLifecycleNotification.
- Export the new variant types from index.ts.

Mirrors C# PR #1343 Phase 4f + review §2.3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
- Change BaseHookInput.timestamp from number (Unix ms) to Date.
- Parse incoming numeric timestamps into Date in handleHooksInvoke.
- Update hooks_extended.e2e.test.ts assertion accordingly.

Mirrors C# PR #1343 Phase 4g.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
…equest union

- Add optional feedback?: string field to PermissionRequestResult so consumers
  can return free-form text forwarded to the model with the decision.
- Delete the hand-written narrow PermissionRequest interface in types.ts and
  re-export the generated discriminated union from session-events.ts instead.
  Handlers can now type-safely access per-kind fields (e.g. shell .commands,
  write .fileName / .diff, mcp .toolName / .args).

Mirrors C# PR #1343 Phase 4g + review §2.9.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
Replaces the fragile Pick<SessionConfig, '...30+ keys...'> definition for
ResumeSessionConfig with a shared SessionConfigBase interface. SessionConfig
and ResumeSessionConfig now both extend it:

- SessionConfig adds sessionId? and cloud?.
- ResumeSessionConfig adds suppressResumeEvent? and continuePendingWork?.

SessionConfigBase is exported from index.ts for consumers that want to
build shared helpers over both shapes.

Mirrors C# PR #1343 Phase 5 + review §2.2.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
Replaces the flat connection-related fields on CopilotClientOptions
(cliPath, cliArgs, port, useStdio, cliUrl, tcpConnectionToken,
isChildProcess) with a single discriminated 'connection?: RuntimeConnection'
field. Construct values via factory functions:

  RuntimeConnection.forStdio({ path?, args? })          // default
  RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })
  RuntimeConnection.forUri(url, { connectionToken? })

The mutually-exclusive combinations that used to be runtime errors are
now caught at compile time by the discriminated union. The previous
isChildProcess flag (only ever used by joinSession() in extension.ts) is
dropped from the public API surface; extension.ts now uses an @internal
_internalConnection hook to enter the parent-process stdio mode.

Other renames in this phase:
- CopilotClientOptions.copilotHome -> baseDirectory.
- Internal CopilotClient.actualPort field -> runtimePort.

All TS test files, scenario fixtures, samples, README, and docs updated
to the new shape. Mirrors C# PR #1343 Phase 9.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
Both methods now accept either a MessageOptions object or just a string
prompt. The string form is a shorthand for { prompt }:

  await session.send('Hello');
  await session.sendAndWait('Hello');

Mirrors C# PR #1343 Phase 7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
…SDK consistency

Matches the C# API review rename in #1343 (EnableRemoteSessions on
CopilotClientOptions). The wire-level RPC field stays 'remote' since that
is the runtime's contract; only the SDK surface changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
* Phase A: property/method renames on SessionConfig/ResumeSessionConfig

Mirrors C# PR #1343 Phase 4a renames:

- onExitPlanMode -> onExitPlanModeRequest
- onAutoModeSwitch -> onAutoModeSwitchRequest
- createSessionFsHandler -> createSessionFsProvider
- ResumeSessionConfig.disableResume -> suppressResumeEvent
- ProviderConfig.maxInputTokens -> maxPromptTokens (drops the wire shim)
- CopilotSession.getMessages() -> getEvents()
- InputOptions -> UiInputOptions

Wire RPC name 'session.getMessages' is unchanged (runtime contract).

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

* Phase C: CopilotClientOptions / MCP / streaming shape changes

- Remove autoStart and autoRestart from CopilotClientOptions. The client
  now always starts on first createSession/resumeSession; users can still
  call client.start() explicitly for eager startup.
- Make MCPServerConfigBase.tools optional (undefined = all, [] = none).
- Fix streaming JSDoc block comment that wasn't attached due to single-star.

Mirrors C# PR #1343 Phase 4c.

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

* Phase D: lifecycle event polymorphic union + Date timestamps

- Split SessionLifecycleEvent into a discriminated union of
  SessionCreatedEvent / SessionDeletedEvent / SessionUpdatedEvent /
  SessionForegroundEvent / SessionBackgroundEvent.
- Promote the metadata payload into a named SessionLifecycleEventMetadata
  interface; metadata is required on non-delete variants and absent on
  session.deleted.
- Convert metadata.startTime and metadata.modifiedTime from string to Date,
  matching SessionMetadata. Parse on receipt in
  client.handleSessionLifecycleNotification.
- Export the new variant types from index.ts.

Mirrors C# PR #1343 Phase 4f + review §2.3.

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

* Phase E: hook input timestamps as Date

- Change BaseHookInput.timestamp from number (Unix ms) to Date.
- Parse incoming numeric timestamps into Date in handleHooksInvoke.
- Update hooks_extended.e2e.test.ts assertion accordingly.

Mirrors C# PR #1343 Phase 4g.

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

* Phase F: PermissionRequestResult.feedback + use generated PermissionRequest union

- Add optional feedback?: string field to PermissionRequestResult so consumers
  can return free-form text forwarded to the model with the decision.
- Delete the hand-written narrow PermissionRequest interface in types.ts and
  re-export the generated discriminated union from session-events.ts instead.
  Handlers can now type-safely access per-kind fields (e.g. shell .commands,
  write .fileName / .diff, mcp .toolName / .args).

Mirrors C# PR #1343 Phase 4g + review §2.9.

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

* Phase G: extract SessionConfigBase

Replaces the fragile Pick<SessionConfig, '...30+ keys...'> definition for
ResumeSessionConfig with a shared SessionConfigBase interface. SessionConfig
and ResumeSessionConfig now both extend it:

- SessionConfig adds sessionId? and cloud?.
- ResumeSessionConfig adds suppressResumeEvent? and continuePendingWork?.

SessionConfigBase is exported from index.ts for consumers that want to
build shared helpers over both shapes.

Mirrors C# PR #1343 Phase 5 + review §2.2.

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

* Phase H: defineTool({ name, ... }) single-arg form

Change defineTool from defineTool(name, config) to defineTool({ name, ...config })
so the call shape matches the Tool<T> interface. name remains mandatory and is
enforced by the Tool<T> type. Updates all samples, docs, tests, and the
CHANGELOG snippet.

Review §1.3.

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

* Revert "Phase H: defineTool({ name, ... }) single-arg form"

This reverts commit 49da911.

* Phase I: RuntimeConnection discriminated config

Replaces the flat connection-related fields on CopilotClientOptions
(cliPath, cliArgs, port, useStdio, cliUrl, tcpConnectionToken,
isChildProcess) with a single discriminated 'connection?: RuntimeConnection'
field. Construct values via factory functions:

  RuntimeConnection.forStdio({ path?, args? })          // default
  RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })
  RuntimeConnection.forUri(url, { connectionToken? })

The mutually-exclusive combinations that used to be runtime errors are
now caught at compile time by the discriminated union. The previous
isChildProcess flag (only ever used by joinSession() in extension.ts) is
dropped from the public API surface; extension.ts now uses an @internal
_internalConnection hook to enter the parent-process stdio mode.

Other renames in this phase:
- CopilotClientOptions.copilotHome -> baseDirectory.
- Internal CopilotClient.actualPort field -> runtimePort.

All TS test files, scenario fixtures, samples, README, and docs updated
to the new shape. Mirrors C# PR #1343 Phase 9.

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

* Phase J: send / sendAndWait string overloads

Both methods now accept either a MessageOptions object or just a string
prompt. The string form is a shorthand for { prompt }:

  await session.send('Hello');
  await session.sendAndWait('Hello');

Mirrors C# PR #1343 Phase 7.

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

* Phase K: stripInternal, AsyncDisposable, clean stop(), drop destroy()

- Enable stripInternal in tsconfig.json so @internal members no longer
  appear in the published .d.ts. Verified that CopilotSession constructor,
  the register*/clientSessionApis hooks, _handle* methods,
  NO_RESULT_PERMISSION_V2_ERROR, _internalConnection, and
  ParentProcessRuntimeConnection are all stripped from the public types.
  The MessageConnection import from vscode-jsonrpc no longer leaks either.
- Tag NO_RESULT_PERMISSION_V2_ERROR with @internal explicitly.
- Implement Symbol.asyncDispose on CopilotClient so it works inside
  'await using' blocks, matching CopilotSession.
- Tighten client.stop() so the Node process can exit cleanly without
  process.exit(): socket.destroy() in addition to socket.end(), explicit
  destroy() on the child process stdio streams, and cliProcess.unref().
  Manually verified by running examples/basic-example.ts against a live
  runtime: the process exits within a few seconds of the await using
  block ending.
- Remove the deprecated CopilotSession.destroy() alias.
- Rewrite examples/basic-example.ts to import from '@github/copilot-sdk'
  (not '../src/index.js') and demonstrate the await using pattern with
  the new send/sendAndWait string overloads.

Covers review §2.4, §2.5, §2.10, §3.1, §3.2, §4.1, §4.3 and the C# PR's
Phase 8 docs/sample updates.

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

* Phase L: fix githubToken typos in scenario fixtures

Twenty-one test/scenarios/**/typescript/src/index.ts files used
'githubToken: process.env.GITHUB_TOKEN' (lowercase h) instead of the
correct 'gitHubToken'. They silently passed an unrecognized property
and the runtime ignored the token. Fix in lock-step across all
affected scenarios.

The existing 'typecheck' npm script already runs
'tsc --noEmit -p tsconfig.test.json' in CI, so no further CI wiring
is needed to prevent regressions: this typo would now be a compile
error under the SDK's strict CopilotClientOptions shape.

Other Phase L items (missing onPermissionRequest, invalid permission
kinds, resumeSession scenarios without config) were either already
caught by the runtime PR or do not apply to TS — no remaining work.

Covers review §1.1, §2.1, §2.8, §2.11.

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

* Phase L follow-up: run prettier --write over all modified files

Prettier check failed on Ubuntu CI because several files modified in
earlier phases didn't get re-formatted after the bulk regex rewrites.
Running 'npm run format' (prettier --write) normalizes them.

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

* Fix Phase I/E/L test failures surfaced by CI

- commands/multi-client/ui_elicitation: shorthand
  'copilotClientOptions: { tcpConnectionToken }' wasn't matched by the
  earlier batch rewrite, so client1 was still spawned in stdio mode
  while client2 tried to connect by URI. Switch the harness call to
  TCP + RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }).
- session_fs.e2e.test.ts: add the missing RuntimeConnection import.
- hooks_extended.e2e.test.ts: SessionStart and UserPromptSubmitted
  timestamp assertions still used toBeGreaterThan(0) but
  BaseHookInput.timestamp is now Date. Switch to toBeInstanceOf(Date).
- client.test.ts: delete the two obsolete 'allows *Session without
  onPermissionRequest' unit tests. They asserted on the 'Client not
  connected' error that only occurred when autoStart was false; with
  Phase C removing autoStart, the client now auto-starts on the first
  session call and those tests would need a real spawned runtime.

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

* Add E2E equivalents of removed createSession/resumeSession-without-permission tests

The two client.test.ts unit tests deleted in the previous commit only
asserted that createSession/resumeSession surface 'Client not connected'
when called pre-start. The intent behind them was to confirm that
omitting onPermissionRequest doesn't itself throw. With autoStart gone,
the only meaningful version of that test is an E2E one that actually
spawns a runtime. Port the equivalent C# coverage
(ClientE2ETests.Should_Allow_*Session_Called_Without_PermissionHandler)
to client.e2e.test.ts so we have parity.

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

* Add E2E tests for createSession/resumeSession without onPermissionRequest

Ports dotnet's Should_Allow_CreateSession_Called_Without_PermissionHandler
(Theory: stdio + tcp) and Should_Allow_ResumeSession_Called_Without_PermissionHandler
from dotnet/test/E2E/ClientE2ETests.cs into nodejs/test/e2e/session.e2e.test.ts.

These exercise the contract that {onPermissionRequest} is optional on both
SessionConfig and ResumeSessionConfig: when not provided, the runtime
leaves permission prompts pending for the consumer to resolve via the
low-level RPC. Without these tests, that contract was unprotected against
regression.

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

* Harness: preserve caller-supplied RuntimeConnection while still injecting CLI path

Earlier batch rewrite of createSdkTestContext meant that whenever a test
passed copilotClientOptions.connection, the spread overrode the harness's
own connection variant entirely - losing the COPILOT_CLI_PATH binding.
Now merge by variant kind: if the caller asks for tcp/stdio without a
path, the harness fills it in from COPILOT_CLI_PATH; explicit values
from the caller win.

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

* session_fs.e2e: fix unconverted tcpConnectionToken flat key on inner client

In the 'should reject setProvider when sessions already exist' test, the
first client was still using the flat tcpConnectionToken property which
is no longer a valid CopilotClientOptions field. Switch to
RuntimeConnection.forTcp({ connectionToken }).

Verified locally: full session_fs.e2e.test.ts suite (9 tests) now passes.

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

* Move hook input deserialization next to the cast that types it

normalizeHookInput lived in client.ts and inspected for a 'timestamp'
property by name, which felt magical (brittle against any future
hook-shaped wire payload that happens to contain a numeric 'timestamp').

Move the conversion into CopilotSession._handleHooksInvoke, renamed
deserializeHookInput, right next to the GenericHandler cast that says
'this unknown is now a HookInput'. That's the only call site that
actually knows the payload is a hook input, so it's the correct boundary
for the schema transform.

This is the TS equivalent of what C# does via
UnixMillisecondsDateTimeOffsetConverter (attached per-property on each
HookInput.Timestamp); TS just plumbs the same conversion through the
hooks dispatcher instead of a per-type JSON converter.

Verified 3/3 hooks_extended.e2e tests pass locally.

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

* Phase K refinement: use unref() instead of destroy() in client.stop()

destroy() on the child's stdio pipes and the TCP socket is more aggressive
than needed. If the child crashes with a useful message on stderr that our
existing data listener hasn't drained yet, stderr.destroy() drops it. If
there's an in-flight write to stdin, destroy() raises 'error' on the
stream. Same trade-off for socket.destroy() short-circuiting the graceful
FIN/ACK.

unref() solves the actual problem (event loop staying alive after stop())
without disrupting late output. From the Node docs: 'unref will allow the
program to exit if this is the only active socket in the event system.
The socket does not lose any functionality' — error events still fire,
late data still drains through registered listeners.

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

* client.stop(): await child exit and socket close

Replace the unref()-based fire-and-forget cleanup with deterministic
awaiting:

- Socket: end() + await 'close'. By the time stop() returns the
  FIN/ACK exchange has completed.
- ChildProcess: kill() + await 'exit'. By the time stop() returns the
  child has truly exited, its stdio pipes are closed, and there are
  no lingering handles to keep the event loop alive.

No SIGKILL escalation. If the child ignores SIGTERM, stop() blocks;
callers that need a guaranteed-bounded shutdown should use forceStop()
(which already sends SIGKILL).

Replaces the previous unref() approach: that worked for clean exit but
allowed late stderr output to surface after stop() resolved, which is
exactly the timing window where consumers expect cleanup to be done.

Verified locally: full client.test.ts (75 tests) passes.

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

* Revert incorrect feedback widening of PermissionRequestResult

The previous '& { feedback?: string }' incorrectly added feedback to every
variant of the union. In the runtime schema, feedback is reject-only —
it appears only on PermissionDecisionReject and is already typed by the
generated PermissionDecisionRequest['result'] union. No manual
augmentation needed.

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

* Rename client.on -> client.onLifecycle for cross-SDK consistency

Matches the C# rename in PR #1357 Phase 4f (client.On -> client.OnLifecycle).
client.onLifecycle is clearer than client.on at the call site because the
two on() methods on CopilotClient and CopilotSession listen for completely
different event families (lifecycle vs per-session). The receiver alone
isn't always enough to disambiguate, especially in mixed code that holds
both objects.

session.on stays unchanged because that's where the bare 'on' verb belongs:
session events are the primary stream for that object.

Updates README + client_lifecycle.e2e.test.ts to the new name. Tests still
pass (validated via tsc -p tsconfig.test.json).

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

* Reformat src/types.ts after PermissionRequestResult revert

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

* Address PR review comments from #1357

- Add missing RuntimeConnection imports to 26 test/scenarios TypeScript
  fixtures. The earlier batch rewrite added the .forStdio/.forUri call
  sites but missed the corresponding import statement, so each scenario
  failed to compile until now.
- nodejs/test/e2e/harness/sdkTestContext.ts: strip the 'kind' property
  before spreading a user-supplied RuntimeConnection back through the
  forStdio/forTcp factory opts. The factory opt types don't accept
  'kind' so the spread was producing excess-property type errors.
- nodejs/src/client.ts: rewrite the 'Path to Copilot CLI is required'
  error message to point at the new connection options
  (RuntimeConnection.forStdio({ path }), forTcp({ path }), forUri(...),
  or the COPILOT_CLI_PATH environment variable). The old message
  referenced removed cliPath / cliUrl options.
- nodejs/src/client.ts: change the default logLevel from 'debug' to
  'info'. 'debug' was a TS-only outlier; Python and Rust default to
  'info', and the README has always claimed 'info'. Go and .NET don't
  pass --log-level at all when omitted (CLI defaults to info anyway),
  so 'info' is consistent with every other SDK's effective default.
- nodejs/src/types.ts: fix MCPServerConfigBase.tools doc comment to
  spell the all-tools sentinel as ['*'] (the actual type is string[],
  so a bare '*' string can't be passed).

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

* logLevel: don't impose any default, match C#/Go

Instead of defaulting to 'info' on the SDK side, omit the --log-level
flag entirely when the caller didn't set one and let the runtime use
its own default. Matches dotnet/Client.cs and go/client.go, which both
only pass --log-level when explicitly provided.

CopilotClientOptions.logLevel JSDoc and README updated to describe this
('When omitted, the runtime uses its own default').

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

* Rename CopilotClientOptions.remote -> enableRemoteSessions for cross-SDK consistency

Matches the C# API review rename in #1343 (EnableRemoteSessions on
CopilotClientOptions). The wire-level RPC field stays 'remote' since that
is the runtime's contract; only the SDK surface changes.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS mentioned this pull request May 21, 2026
7 tasks
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
C# codegen now hard-errors instead of falling back to `object` for
unmappable schema nodes. Schemas tagged with `x-opaque-json: true` map
to `JsonElement`/`JsonElement?`. Hand-written types that round-trip
arbitrary JSON (ToolInvocation.Arguments, ToolResultObject.ToolTelemetry,
hook *Input* ToolArgs/ToolResult) are retyped to JsonElement to match
the generated wire types. Boundary code in Session/Client/SessionFsProvider
converts between the hand-written user-facing object?-typed dictionaries
(ElicitationSchema.Properties, ElicitationResult.Content) and the wire
JsonElement form.

This is the SDK side of github/copilot-agent-runtime#8375 — replays the
non-Generated/* changes from the reverted PR #1343.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SteveSandersonMS added a commit that referenced this pull request May 21, 2026
* Go SDK API review: Phase A - SessionConfig/ResumeSessionConfig renames

- OnExitPlanMode -> OnExitPlanModeRequest
- OnAutoModeSwitch -> OnAutoModeSwitchRequest
- ExitPlanModeHandler -> ExitPlanModeRequestHandler
- AutoModeSwitchHandler -> AutoModeSwitchRequestHandler
- Session.GetMessages -> Session.GetEvents
- ResumeSessionConfig.DisableResume -> SuppressResumeEvent
- Streaming bool -> *bool on SessionConfig and ResumeSessionConfig

Wire-level RPC method (session.getMessages) and internal request types
are unchanged; only the public Go surface is renamed.

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

* Go SDK API review: Phase B+C - ProviderConfig + MCP Tools tweaks

B. ProviderConfig.MaxInputTokens -> MaxPromptTokens (matches the wire
   name 'maxPromptTokens' exactly).

C. MCPStdioServerConfig.Tools / MCPHTTPServerConfig.Tools: change JSON
   tag from 'tools' to 'tools,omitempty'. Now nil/omitted slice means
   'all tools' (CLI default), matching TS/Rust/C# semantics.

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

* Go SDK API review: Phase D - RuntimeConnection refactor

Replaces the flat connection fields on ClientOptions with a single
discriminated Connection RuntimeConnection field constructed from one of:

  StdioConnection{Path, Args}
  TcpConnection{Port, ConnectionToken, Path, Args}
  UriConnection{URL, ConnectionToken}

Removes:
  - CLIPath, CLIArgs, CLIUrl, UseStdio, Port, TCPConnectionToken
  - AutoStart, AutoRestart
  - public ConnectionState type, State* constants, and Client.State() method

Renames:
  - CopilotHome -> BaseDirectory
  - Remote -> EnableRemoteSessions
  - Client.ActualPort() -> Client.RuntimePort()

LogLevel no longer defaults to 'info'. When empty, the SDK does not
pass --log-level to the runtime, matching the TS SDK.

All unit tests, e2e tests, samples, and the Go scenario apps are
migrated. README updated.

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

* Go SDK API review: Phase E - lifecycle timestamps as time.Time

SessionMetadata and SessionLifecycleEventMetadata: StartTime and
ModifiedTime change from string to time.Time. The runtime emits these
as ISO 8601 strings, which time.Time's default JSON unmarshal handles
natively.

Matches the equivalent C# (#1343) / TS (#1357) changes.

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

* Go SDK API review: Phase F - hook timestamps as time.Time

All six hook input types (PreToolUseHookInput, PostToolUseHookInput,
UserPromptSubmittedHookInput, SessionStartHookInput, SessionEndHookInput,
ErrorOccurredHookInput): Timestamp changes from int64 to time.Time. Each
type gains a MarshalJSON/UnmarshalJSON pair that serializes Timestamp as
Unix milliseconds on the wire, matching the runtime protocol.

Mirrors the equivalent C# (#1343) UnixMillisecondsDateTimeOffsetConverter
and TS (#1357) Date-typed hook timestamps.

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

* Go SDK API review: Phases G-K - misc renames and cleanups

G. InputOptions -> UiInputOptions on the Session.UI().Input convenience.

H. Add Session.SendPrompt(ctx, prompt) and Session.SendPromptAndWait(ctx, prompt)
   convenience wrappers around the MessageOptions-based methods (mirrors the
   string overloads added in C#/TS).

I. SessionFsProvider interface method renames (Go-specific): Mkdir ->
   MakeDirectory, Readdir -> ReadDirectory, ReaddirWithTypes ->
   ReadDirectoryWithTypes, Rm -> Remove. The adapter still implements the
   wire-protocol method names (rpc.SessionFsHandler) unchanged.

J. SessionConfig/ResumeSessionConfig.CreateSessionFsHandler ->
   CreateSessionFsProvider (matches the SessionFsProvider type it returns).

K. Remove deprecated Session.Destroy(); callers must use Session.Disconnect().

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

* Go SDK API review: Phase L - docs and scenario migrations

Update Go code samples in docs/ to use the new ClientOptions.Connection
shape (StdioConnection / UriConnection). Also migrate the streaming
scenario to copilot.Bool(true) for the new *bool Streaming field.

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

* Fix Streaming: true in Go doc snippets after *bool change

CI docs-validate extracts Go code blocks and compiles them. Update the
remaining 4 sites (docs/getting-started.md x3, docs/features/streaming-events.md)
to use copilot.Bool(true) now that SessionConfig.Streaming is *bool.

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

* Apply gofmt

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

* MCP Tools: change []string to *[]string to preserve nil vs empty

With `json:"tools,omitempty"` on a bare []string, Go collapses both
nil and []string{} to "omitted", losing the documented distinction
between "all tools" (nil) and "no tools" (empty slice). Switch the
field type to *[]string so a non-nil pointer to an empty slice
serializes as `tools: []` on the wire, matching TS `tools?: string[]`
and C# `IList<string>?` with WhenWritingNull.

Callers use the standard Go idiom for pointer-to-slice literals:
  Tools: &[]string{"*"}       // explicit all tools
  Tools: &[]string{}          // no tools
  Tools: &[]string{"a","b"}   // only those tools
  Tools: nil                  // (default) all tools, field omitted

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

* docs: show StdioConnection.Args for Go --log-dir snippet

The Go SDK now supports extra runtime args via StdioConnection.Args /
TcpConnection.Args. Replace the outdated 'Go cannot pass args, run the
CLI manually' snippet with the actual idiomatic call.

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

* Drop TS/C# reference from MCPStdioServerConfig doc comment

Go SDK source shouldn't reference other-language SDKs. Rephrase the
Tools field doc to explain the pointer-to-slice form purely in Go
terms.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced May 21, 2026
@github-actions github-actions Bot mentioned this pull request May 22, 2026
SteveSandersonMS added a commit that referenced this pull request May 22, 2026
Mirrors PR #1343 Phase 9 (.NET) and PR #1357 Phase I (TypeScript). Removes
the flat `SubprocessConfig` / `ExternalServerConfig` split in favour of a
`RuntimeConnection` discriminated hierarchy, with process-management
options moved to a new `CopilotClientOptions` dataclass.

Public API:

* `RuntimeConnection` abstract base with static factories `stdio()`,
  `tcp()`, `uri()`.
* `ChildProcessRuntimeConnection` intermediate base carrying `path` and
  `args` shared by Stdio + Tcp.
* `StdioRuntimeConnection`, `TcpRuntimeConnection`, `UriRuntimeConnection`
  concrete subclasses; pattern-match / `isinstance` on the class to
  branch on the transport.
* `CopilotClientOptions(connection=..., working_directory=..., log_level=...,
  env=..., github_token=..., base_directory=..., use_logged_in_user=...,
  telemetry=..., session_fs=..., session_idle_timeout_seconds=...,
  enable_remote_sessions=...)`.
* `CopilotClient(options=CopilotClientOptions(...) | None, *, auto_start=...,
  on_list_models=...)`.

Renames:

* `copilot_home` → `base_directory`
* `remote` → `enable_remote_sessions`
* `CopilotClient.actual_port` → `CopilotClient.runtime_port`
* `CopilotClient.on(...)` → `CopilotClient.on_lifecycle(...)`
* `tcp_connection_token` moves off `CopilotClientOptions` and onto
  the variant: `TcpRuntimeConnection.connection_token` /
  `UriRuntimeConnection.connection_token`
* `use_stdio` bool is gone — the variant class IS the discriminator.

Migration:

* All hand-written code that previously branched on `isinstance(config,
  ExternalServerConfig)` now branches on the runtime-connection variant
  via `isinstance(connection, UriRuntimeConnection)` /
  `StdioRuntimeConnection` etc., giving proper type narrowing in
  pyright/mypy without relying on Literal-based tagged-union narrowing.
* Samples, scenarios, README, and tests updated in lock-step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

3 participants