Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/sdk-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ jobs:
run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check
- if: github.event_name != 'merge_group' && runner.os == 'Linux'
run: cargo clippy --all-targets --no-default-features --features test-support,local-runtime,derive -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type
# Path-dependency consumers must not rerun build.rs on unchanged rebuilds.
- if: github.event_name != 'merge_group' && runner.os == 'Linux'
run: bash scripts/check-fresh-rebuild.sh --no-default-features --features test-support,derive
- if: github.event_name != 'merge_group' && runner.os == 'Linux'
env:
RUSTDOCFLAGS: "-D warnings"
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc.

### Recording and replaying SDK tests

Owned-stdio shutdown regressions share
`test/harness/stdio-shutdown-runtime.cjs` across all six SDKs. Launch it with
Node and arguments `<cleanup-marker> <mode> <pid-file>`. The fixture acknowledges
`runtime.shutdown`, but writes its cleanup marker only after stdin EOF, matching
the native wrapper's host-finalization boundary. Language-native tests exercise
graceful stop/disposal, force-stop where exposed, a child that ignores EOF, and
failed-startup cleanup. Keep those lifecycle expectations aligned when changing
an SDK transport; test watchdogs must allow all cleanup phases their separate
budgets, rather than treating the graceful-exit timeout as a total shutdown cap.

The shared harness records real inference responses under `test/snapshots`.
Record new captures with `GITHUB_TOKEN` set and `GITHUB_ACTIONS` unset;
never author model responses by hand. Rerun with `GITHUB_ACTIONS=true` and real
Expand Down
9 changes: 9 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ Start the CLI server and establish connection.
##### `StopAsync(): Task`

Stop the server and close all sessions. Throws if errors are encountered during cleanup.
For an owned stdio runtime, graceful shutdown closes stdin and waits up to 10 seconds
for host cleanup, including telemetry export. This cleanup is best-effort: if the wait
times out, the process is terminated and that timeout alone is not reported as a cleanup
error. A successful return does not guarantee that all telemetry was exported.

##### `ForceStopAsync(): Task`

Expand Down Expand Up @@ -195,6 +199,11 @@ Create a new conversation session.
- `OnUserInputRequest` - Handler for legacy question-and-answer requests from the agent. Enables the legacy `ask_user` tool. See [User Input Requests](#user-input-requests) section.
- `AskUserVariant` - Selects the model-facing `ask_user` tool shape. Defaults to `AskUserVariant.Legacy`; use `AskUserVariant.Elicitation` with `OnElicitationRequest`.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
- `CanvasHandler` - Handles canvas open, close, and action callbacks. The SDK awaits
asynchronous callbacks before replying, including callbacks without a result, unless
the runtime cancels the request first. A cancellation response can be sent while the
callback is still running. Their cancellation token is canceled by a per-request
`$/cancelRequest`, when the runtime connection closes, or when the client is disposed.

##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task<CopilotSession>`

Expand Down
31 changes: 23 additions & 8 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ or IOException

if (ctx.CliProcess is { } childProcess)
{
await CleanupCliProcessAsync(childProcess, ctx.StderrPump, errors, _logger);
await CleanupCliProcessAsync(childProcess, ctx.StderrPump, errors, _logger, gracefulRuntimeShutdown);
}

if (ctx.FfiHost is { } ffiHost)
Expand All @@ -703,20 +703,35 @@ or IOException
}
}

private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List<Exception>? errors, ILogger? logger)
private static async Task CleanupCliProcessAsync(Process childProcess, ProcessStderrPump? stderrPump, List<Exception>? errors, ILogger? logger, bool gracefulRuntimeShutdown = false)
{
var processExited = false;

try
{
if (gracefulRuntimeShutdown && childProcess.StartInfo.RedirectStandardInput && !childProcess.HasExited)
{
try
{
// The native wrapper finalizes host telemetry after stdin EOF,
// not when it acknowledges runtime.shutdown.
childProcess.StandardInput.Close();
await childProcess.WaitForExitAsync().WaitAsync(s_runtimeShutdownTimeout);
}
catch (Exception ex) when (ex is TimeoutException or IOException or ObjectDisposedException)
{
logger?.LogDebug(ex, "Graceful stdio runtime exit did not complete; terminating the process");
}
catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception or NotSupportedException)
{
AddCleanupError(errors, ex, logger);
}
}

if (!childProcess.HasExited)
{
// The runtime completes all cleanup before responding to
// runtime.shutdown and then leaves termination to us; it
// deliberately keeps its JSON-RPC server alive to send the
// response and never self-exits. Waiting for a self-exit that
// will never come just wastes time, so terminate the child
// immediately and only wait to reap it.
// Force-stop, failed startup, and runtimes that ignore EOF still
// require explicit termination.
childProcess.Kill(entireProcessTree: true);
// Kill is asynchronous; wait for the root CLI process to exit so cleanup callers
// do not observe StopAsync/DisposeAsync completion while it is still tearing down.
Expand Down
49 changes: 47 additions & 2 deletions dotnet/src/Generated/Rpc.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading