diff --git a/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs b/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs index 9e85c645436..4a62bf400b4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs +++ b/dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs @@ -573,6 +573,9 @@ private string BuildScript(string command, string sentinel) return "& {" + " $__af_rc = 0;" + + // $LASTEXITCODE persists across commands and cmdlets do not update it. + // Clear it so any value after invocation belongs to this command. + " $global:LASTEXITCODE = $null;" + " try {" + $" $__af_cmd = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('{encoded}'));" + // Force the user command's success output through the same @@ -588,9 +591,11 @@ private string BuildScript(string command, string sentinel) " [Console]::WriteLine(($_ | Out-String).TrimEnd());" + " }" + " };" + + // Capture the pipeline status before flushing overwrites it. + " $__af_ok = $?;" + " [Console]::Out.Flush();" + " if ($LASTEXITCODE -ne $null) { $__af_rc = $LASTEXITCODE }" + - " elseif (-not $?) { $__af_rc = 1 }" + + " elseif (-not $__af_ok) { $__af_rc = 1 }" + " } catch {" + " [Console]::Error.WriteLine($_.ToString());" + " $__af_rc = 1" + diff --git a/dotnet/tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/LocalShellExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/LocalShellExecutorTests.cs index 247033fa1d6..ae6400c295d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/LocalShellExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Tools.Shell.UnitTests/LocalShellExecutorTests.cs @@ -313,6 +313,37 @@ public async Task Persistent_CarriesEnvironment_AcrossCallsAsync() Assert.Contains("persisted-value", read.Stdout, StringComparison.Ordinal); } + [Fact] + public async Task Persistent_PowerShell_DoesNotInheritPreviousExitCodeAsync() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + // Arrange + await using var shell = new LocalShellExecutor(new() + { + Mode = ShellMode.Persistent, + Shell = "powershell.exe", + Timeout = TimeSpan.FromSeconds(20), + }); + + // Act + var firstFailure = await shell.RunAsync("cmd /c exit 3"); + var succeeding = await shell.RunAsync("Write-Output ok"); + var repeatedFailure = await shell.RunAsync("cmd /c exit 3"); + var readback = await shell.RunAsync("Write-Output $LASTEXITCODE"); + + // Assert + Assert.Equal(3, firstFailure.ExitCode); + Assert.Equal(0, succeeding.ExitCode); + Assert.Contains("ok", succeeding.Stdout, StringComparison.Ordinal); + Assert.Equal(3, repeatedFailure.ExitCode); + Assert.Equal(0, readback.ExitCode); + Assert.Empty(readback.Stdout.Trim()); + } + [Fact] public async Task Persistent_Timeout_ReturnsExitCode124Async() {