Repository navigation
Python: [Bug]: Persistent PowerShell LocalShellTool reports exit_code 0 when a native command fails with the same code twice #9657
Description
Observed Behavior
With mode="persistent" and PowerShell (the default shell on Windows), LocalShellTool reports exit_code=0 for a native command that failed, whenever it exits with the same code as the previous native command in the session. An agent that runs a failing test command, edits the code and reruns it with the same failure (exit 1 both times) is told that the second run succeeded. The same happens with repeated git errors (128) or any repeated exit N.
Since #8206, ShellSession._build_script (python/packages/tools/agent_framework_tools/shell/_session.py) works out the exit code like this:
$__af_last = $LASTEXITCODE;
...
Invoke-Expression $__af_cmd | Out-Default;
$__af_ok = $?;
if ($LASTEXITCODE -ne $__af_last) { $__af_rc = $LASTEXITCODE }
elseif (-not $__af_ok) { $__af_rc = 1 }When the native command exits with the code that is already in $LASTEXITCODE, the value doesn't change, so the first branch doesn't run. The elseif doesn't run either, because $? reflects Invoke-Expression, which succeeded, rather than the native command inside it. That is the same on PowerShell 7.6.6 and Windows PowerShell 5.1:
cmd /c exit 3; $? -> False
Invoke-Expression 'cmd /c exit 3'; $? -> True
Invoke-Expression 'cmd /c exit 3' | Out-Default; $? -> True
So $__af_rc stays 0. The #8206 discussion mentioned this same-code case and expected the elseif to still report 1, but in practice it reports success.
Expected Behavior
Each failing run reports its own exit code: 1, 1, 1 and 128, 128 in the reproduction below. Commands that don't run a native process keep reporting 0, or 1 on a cmdlet error, and $LASTEXITCODE stays readable by the user's commands.
Steps to Reproduce
- Create a
LocalShellTool(mode="persistent")on Windows, where the default shell is PowerShell. - Run a native command that exits with a non-zero code, for example
python -c "import sys; sys.exit(1)". It reportsexit_code=1. - Run the same command again. It reports
exit_code=0, although it failed with 1 again.
Minimal Reproduction
import asyncio
import sys
from agent_framework.tools import LocalShellTool
FAILING_PYTHON = f"& '{sys.executable}' -c 'import sys; sys.exit(1)'"
COMMANDS = [FAILING_PYTHON, FAILING_PYTHON, FAILING_PYTHON, "git rev-parse --git-dir", "git rev-parse --git-dir"]
async def main() -> None:
# Default shell on Windows; run from a directory that is not a git repository.
async with LocalShellTool(mode="persistent") as tool:
for command in COMMANDS:
result = await tool.run(command)
label = "python -c 'import sys; sys.exit(1)'" if command == FAILING_PYTHON else command
print(f"exit_code={result.exit_code:<4} <- {label}")
asyncio.run(main())Output on main (Windows 11, PowerShell 7.6.6):
exit_code=1 <- python -c 'import sys; sys.exit(1)'
exit_code=0 <- python -c 'import sys; sys.exit(1)'
exit_code=0 <- python -c 'import sys; sys.exit(1)'
exit_code=128 <- git rev-parse --git-dir
exit_code=0 <- git rev-parse --git-dir
I get the same output with Windows PowerShell 5.1 (AGENT_FRAMEWORK_SHELL="powershell -NoLogo -NoProfile -NonInteractive -Command -").
Error Messages and Stack Traces
No response
Package Versions
agent-framework-tools: 1.0.0b261022, agent-framework-core: 1.23.0 (source checkout of main at 4982e4c)
Python Version
Python 3.13
Operating System
Windows
Regression
Yes
Additional Context
- Regression from Python: reset $LASTEXITCODE per command in persistent PowerShell sessions #8206 (the fix for .NET: Python: [Bug]: persistent PowerShell shell session reports a stale exit code for cmdlet-only commands #8205). Before it, the check was
if ($LASTEXITCODE -ne $null) { $__af_rc = $LASTEXITCODE }, which reported a repeated failure correctly but also reported a stale code for cmdlet-only commands. Python: reset $LASTEXITCODE per command in persistent PowerShell sessions #8206 fixed the stale code and introduced this case.test_persistent_powershell_does_not_inherit_previous_exit_coderuns one failing native command, so it doesn't cover running it twice. - Suggested fix that still never writes
$LASTEXITCODE(the constraint from the Python: reset $LASTEXITCODE per command in persistent PowerShell sessions #8206 review): check whether the variable now holds a different object rather than a different value. PowerShell stores a newly boxed int every time a native process exits, even when the code is the same, so[object]::ReferenceEquals($LASTEXITCODE, $__af_last)isFalseonly if this command ran a native process or assigned the variable itself. I checked this on PowerShell 7.6.6 and Windows PowerShell 5.1 for exit codes 0, 1, 2, 3, 127, 255 and -1. Since this is a one-line change, I'm opening a PR with it and a regression test right after this issue. - AI assistance: I used AI coding assistants (Claude Code and Antigravity) to help find this bug and to draft this report. I reviewed this report myself and checked the reproduction and its output above.
Acknowledgements
- I searched existing issues and did not find a duplicate.
- I personally verified this behavior and the reproduction details are authentic.
- I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity