Skip to content

Commit 74a6f99

Browse files
committed
Ellipsize long outputs from commands
Signed-off-by: Nikola Forró <[email protected]>
1 parent a8d668e commit 74a6f99

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

agents/tests/unit/test_tools.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ async def test_run_shell_command(command, exit_code, stdout, stderr):
8585
assert result.stderr == stderr
8686

8787

88+
@pytest.mark.parametrize(
89+
"full_output",
90+
[False, True],
91+
)
92+
@pytest.mark.asyncio
93+
async def test_run_shell_command_huge_output(full_output):
94+
command = "printf 'Line\n%.0s' {1..1000}"
95+
tool = RunShellCommandTool()
96+
output = await tool.run(input=RunShellCommandToolInput(command=command, full_output=full_output)).middleware(
97+
GlobalTrajectoryMiddleware(pretty=True)
98+
)
99+
result = output.to_json_safe()
100+
assert result.exit_code == 0
101+
assert result.stderr is None
102+
if full_output:
103+
assert len(result.stdout.splitlines()) == 1000
104+
assert "[...]" not in result.stdout.splitlines()
105+
else:
106+
assert len(result.stdout.splitlines()) == 200
107+
assert "[...]" in result.stdout.splitlines()
108+
109+
88110
@pytest.mark.asyncio
89111
async def test_add_changelog_entry(minimal_spec):
90112
content = ["- some change", " second line"]

agents/tools/commands.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import math
23
from typing import Any
34

45
from pydantic import BaseModel, Field
@@ -10,10 +11,19 @@
1011
from utils import run_subprocess
1112

1213
TIMEOUT = 10 * 60 # seconds
14+
ELLIPSIZED_LINES = 200
1315

1416

1517
class RunShellCommandToolInput(BaseModel):
1618
command: str = Field(description="Command to run")
19+
full_output: bool = Field(
20+
default=False,
21+
description=(
22+
"Whether the content of stdout and stderr should be included in full. "
23+
f"Only approximately {ELLIPSIZED_LINES // 2} lines from the beginning "
24+
"and the end are included by default."
25+
),
26+
)
1727

1828

1929
class RunShellCommandToolResult(BaseModel):
@@ -50,9 +60,24 @@ async def _run(
5060
)
5161
except TimeoutError as e:
5262
raise ToolError(f"The specified command timed out after {TIMEOUT} seconds") from e
63+
64+
def ellipsize(output):
65+
if output is None:
66+
return None
67+
if tool_input.full_output:
68+
return output
69+
lines = output.splitlines(keepends=True)
70+
if len(lines) <= ELLIPSIZED_LINES:
71+
return output
72+
return "".join(
73+
lines[: math.floor((ELLIPSIZED_LINES - 1) / 2)]
74+
+ ["[...]\n"]
75+
+ lines[-math.ceil((ELLIPSIZED_LINES - 1) / 2) :]
76+
)
77+
5378
result = {
5479
"exit_code": exit_code,
55-
"stdout": stdout,
56-
"stderr": stderr,
80+
"stdout": ellipsize(stdout),
81+
"stderr": ellipsize(stderr),
5782
}
5883
return RunShellCommandToolOutput(RunShellCommandToolResult.model_validate(result))

0 commit comments

Comments
 (0)