Skip to content
Open
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
11 changes: 9 additions & 2 deletions src/agents/run_internal/tool_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,15 @@ async def _run_call(span: Any | None) -> RunItem:
if max_output_length is not None:
normalized = truncate_shell_outputs(normalized, max_output_length)
output_text = render_shell_outputs(normalized)
if max_output_length is not None:
output_text = output_text[:max_output_length]
if max_output_length == 0:
# ``truncate_shell_outputs`` already emptied every stream for a zero
# budget, but ``render_shell_outputs`` substitutes a "(no output)"
# placeholder for an empty command, so collapse it back to an empty
# string. For a positive budget the payload was already bounded, and
# re-slicing the rendered text (which adds decoration such as the
# ``$ <command>`` prefix) would chop real output that fit within the
# budget, so it is left intact.
output_text = ""
shell_output_payload = [serialize_shell_output(entry) for entry in normalized]
provider_meta = dict(result.provider_data or {})
else:
Expand Down
52 changes: 52 additions & 0 deletions tests/test_shell_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,58 @@ async def test_shell_tool_output_respects_max_output_length() -> None:
assert raw_item["output"][0]["stderr"] == ""


@pytest.mark.asyncio
async def test_shell_tool_max_output_length_does_not_count_command_decoration() -> None:
"""``max_output_length`` bounds the output streams, not the rendered ``$ <command>`` decoration.

Regression: the rendered text was re-clamped to ``max_output_length`` after the ``$ echo hi``
prefix was added, so a command whose stdout already fit within the budget had real output
chopped (here the whole stdout was lost, leaving only the decoration prefix).
"""
shell_tool = ShellTool(
executor=lambda request: ShellResult(
output=[
ShellCommandOutput(
command="echo hi",
stdout="0123456789",
outcome=ShellCallOutcome(type="exit", exit_code=0),
)
],
)
)

tool_call = {
"type": "shell_call",
"id": "shell_call",
"call_id": "call_shell",
"status": "completed",
"action": {
"commands": ["echo hi"],
"timeout_ms": 1000,
"max_output_length": 6,
},
}

tool_run = ToolRunShellCall(tool_call=tool_call, shell_tool=shell_tool)
agent = Agent(name="shell-agent", tools=[shell_tool])
context_wrapper: RunContextWrapper[Any] = RunContextWrapper(context=None)

result = await ShellAction.execute(
agent=agent,
call=tool_run,
hooks=RunHooks[Any](),
context_wrapper=context_wrapper,
config=RunConfig(),
)

assert isinstance(result, ToolCallOutputItem)
# The 6-char budget bounds stdout to "012345"; the "$ echo hi" decoration is framing that
# neither consumes the budget nor truncates the real output.
assert result.output == "$ echo hi\n012345"
raw_item = cast(dict[str, Any], result.raw_item)
assert raw_item["output"][0]["stdout"] == "012345"


@pytest.mark.asyncio
async def test_shell_tool_uses_smaller_max_output_length() -> None:
shell_tool = ShellTool(
Expand Down