Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

.NET: Python: [Bug]: Persistent PowerShell LocalShellTool returns empty stdout for commands that output objects

Closed
#9,042 1 comment 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@eavanvalkenburg is already working on this.

Since Oct 4, 2026.

  • #9043 by @Aditya-XR — open

Assessment

This issue has not been assessed yet.

Description

.NET python reproduced skills
Description

LocalShellTool in its default mode="persistent" returns an empty stdout for any PowerShell command whose output gets formatted as a table or list. That includes Get-Location / pwd, anything piped through Select-Object, Get-Process, and [pscustomobject]. The command succeeds with exit_code=0, but the agent gets nothing back. Once an object has been written, plain strings that come after it in the same command are lost too. mode="stateless" returns all of this output.

This happens on Windows, where PowerShell is the default shell, and with pwsh on Linux, so it isn't Windows-specific.

Cause: ShellSession._build_script runs the command as & { try { Invoke-Expression $__af_cmd; ... } finally { [Console]::WriteLine('<sentinel>_' + $__af_rc) } } and leaves formatting to the host. The host's Out-Default only flushes table/list formatting when the outer pipeline ends, which is after the script block has returned. By then finally has already written the sentinel straight to [Console]. So the formatted output lands after the sentinel in the stdout buffer, and _run_locked throws it away, since it keeps only the bytes before the sentinel and then clears the buffer. Plain strings survive only because Out-Default writes them immediately.

The .NET ShellSession already handles this. BuildScript in dotnet/src/Microsoft.Agents.AI.Tools.Shell/ShellSession.cs formats the output inside the try, with the comment: "Without this, pwsh defers Out-Default formatting until the script block returns and the sentinel races ahead of the user's output in the byte stream."

Expected: persistent mode returns the same stdout as stateless mode.

Code Sample
import asyncio
import re
import shutil

from agent_framework.tools import LocalShellTool

POWERSHELL = shutil.which("pwsh") or shutil.which("powershell")
SHELLS = {
    # the argv LocalShellTool uses by default on Windows
    "persistent": [POWERSHELL, "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "-"],
    "stateless": [POWERSHELL],
}
COMMANDS = [
    "Get-Location",
    "Get-Item $PSHOME | Select-Object Name",
    "[pscustomobject]@{ Name = 'an object' }; Write-Output 'a string after it'",
    "Write-Output 'just a string'",
]


async def main() -> None:
    for mode, shell in SHELLS.items():
        async with LocalShellTool(mode=mode, shell=shell) as tool:
            for command in COMMANDS:
                result = await tool.run(command)
                stdout = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", result.stdout).strip()  # drop terminal escape codes
                print(f"{mode:<10} exit_code={result.exit_code} stdout={stdout!r}")
                print(f"{'':<10} ^ {command}")


asyncio.run(main())

Output on main with Linux (Debian 12, PowerShell 7.6.6):

persistent exit_code=0 stdout=''
           ^ Get-Location
persistent exit_code=0 stdout=''
           ^ Get-Item $PSHOME | Select-Object Name
persistent exit_code=0 stdout=''
           ^ [pscustomobject]@{ Name = 'an object' }; Write-Output 'a string after it'
persistent exit_code=0 stdout='just a string'
           ^ Write-Output 'just a string'
stateless  exit_code=0 stdout='Path\n----\n/tmp'
           ^ Get-Location
stateless  exit_code=0 stdout='Name\n----\n7'
           ^ Get-Item $PSHOME | Select-Object Name
stateless  exit_code=0 stdout='Name\n----\nan object\na string after it'
           ^ [pscustomobject]@{ Name = 'an object' }; Write-Output 'a string after it'
stateless  exit_code=0 stdout='just a string'
           ^ Write-Output 'just a string'

I get the same result on Windows 11 with PowerShell 7.6.6 and with Windows PowerShell 5.1, and the same with the default LocalShellTool() there, without passing shell=.

Error Messages / Stack Traces

None. The empty stdout comes back with exit_code=0.

Package Versions

agent-framework-tools: 1.0.0b261002, agent-framework-core: 1.20.0 (source checkout of main at e9857bd75)

Python Version

Python 3.13

Additional Context
  • The persistent PowerShell tests in python/packages/tools/tests/test_local_shell_tool.py only check plain strings and exit codes, so none of them covers output PowerShell formats as a table. The re-anchor test, for example, reads (Get-Location).Path, which returns a string and so doesn't hit this. Plain Get-Location would.
  • Fix: format the output inside the try, before finally writes the sentinel: Invoke-Expression $__af_cmd | Out-Default;. Unlike a per-object Out-String, this keeps PowerShell's normal formatting (one table per command). Exit codes for native failures, cmdlet errors, throw, and $LASTEXITCODE readback stay the same. I'll open a PR with this and a regression test.
Dominant language
Python
Stars
13.9k
Forks
2.4k
Avg merge
1d 16h
Merged PRs (30d)
432

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from microsoft/agent-framework

All issues in microsoft/agent-framework

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.