Repository navigation
Description
Description
On Windows, LocalShellTool(mode="persistent") reports the exit code of an earlier command for any later command that only runs cmdlets.
ShellSession._build_script derives the return code from $LASTEXITCODE:
" Invoke-Expression $__af_cmd;"
" if ($LASTEXITCODE -ne $null) { $__af_rc = $LASTEXITCODE }"
" elseif (-not $?) { $__af_rc = 1 }"
$LASTEXITCODE is a session-wide automatic variable that PowerShell only updates when a native (external) executable exits. Cmdlets leave it untouched. Since the session is long-lived, once any command in it has run an external process, $LASTEXITCODE stays set, and every subsequent cmdlet-only command takes the if branch and reports that stale value instead of 0.
The blast radius is that the tool tells the agent a command failed when it succeeded, for the whole rest of the session, until another external command happens to reset the variable.
Stateless mode is unaffected — each command is a fresh process and the rc comes from the process exit code.
Code Sample
import asyncio
from agent_framework_tools.shell._resolve import resolve_shell
from agent_framework_tools.shell._session import ShellSession
async def main():
async with ShellSession(resolve_shell(None, interactive=True)) as s:
for cmd in [
"Write-Output 'cmdlet only'",
"cmd /c exit 3",
"Write-Output 'cmdlet only, after a failing native command'",
"Write-Output 'cmdlet only again'",
]:
r = await s.run(cmd, timeout=20)
print(f"exit_code={r.exit_code:<4} cmd={cmd}")
asyncio.run(main())Output I get on Windows 11 with Windows PowerShell 5.1:
exit_code=0 cmd=Write-Output 'cmdlet only'
exit_code=3 cmd=cmd /c exit 3
exit_code=3 cmd=Write-Output 'cmdlet only, after a failing native command'
exit_code=3 cmd=Write-Output 'cmdlet only again'
The last two commands succeeded and printed their output, but are reported as exit code 3.
Expected: 0, 3, 0, 0.
Error Messages / Stack Traces
No exception — the wrong value is returned silently in ShellResult.exit_code.
Package Versions
agent-framework-tools: 1.0.0b260730 (from a source checkout of main at 992d445)
Python Version
Python 3.12.9
Additional Context
This is Windows-only, and the CI matrix (.github/workflows/python-*.yml) is ubuntu-latest everywhere, so the existing PowerShell tests in python/packages/tools/tests/test_local_shell_tool.py — the ones guarded by @pytest.mark.skipif(sys.platform != "win32", ...) — never actually execute anywhere.
The .NET port has the same lines in dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs (around line 592), so it very likely has the same behaviour. I have not built or run the .NET side, so I am reporting that as an observation rather than a verified claim.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
[-]Python: [Bug]: persistent PowerShell shell session reports a stale exit code for cmdlet-only commands[/-][+].NET: Python: [Bug]: persistent PowerShell shell session reports a stale exit code for cmdlet-only commands[/+]github-actions commentedon Sep 9, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/tools/agent_framework_tools/shell/_session.py::ShellSession._build_scriptat lines 342-365 reports a stale exit code when a persistent PowerShell session runs a failing native process followed by a successful cmdlet-only command. Minimal repro: startShellSessionwith non-interactivepwsh, runpwsh -NoProfile -Command 'exit 3', then runWrite-Output 'cmdlet only'; the second result contains the expected output but hasexit_code=3. The equivalent$LASTEXITCODE-first logic is also present indotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs::BuildScriptat lines 592-593.python/packages/tools/tests/test_local_shell_tool.py::test_persistent_powershell_does_not_reuse_native_exit_code1.0.0b2607301.0.0b260730