Skip to content

trading_agent.tools.bash_session

One long-lived bash process inside the sandbox, like a terminal that stays open.

The folder and variables a command sets carry over to the next command. Each command runs with its input from /dev/null, so a command that waits for typing cannot swallow what comes after it. After each command a marker line with the exit status is printed, which is how the end of its output is found. A command that runs too long has its whole session restarted.

Typical usage example:

session = BashSession(sandbox, timeout_seconds=120, maximum_output_characters=20000) outcome = await session.run('ls -la') await session.close()

CommandOutcome dataclass

CommandOutcome(output, exit_status, truncated)

What one command produced.

Attributes:

Name Type Description
output str

Its combined standard output and error, cut to the maximum length.

exit_status int | None

Its exit status, or None when it was stopped for running too long.

truncated bool

Whether output was left out because it was too long.

BashSession

BashSession(sandbox, timeout_seconds, maximum_output_characters)

A persistent bash process in the sandbox.

Creates the session without starting bash.

Parameters:

Name Type Description Default
sandbox BashSandbox

The sandbox to run in.

required
timeout_seconds float

How long one command may run.

required
maximum_output_characters int

How much output to keep from one command.

required
Source code in src/trading_agent/tools/bash_session.py
def __init__(
    self,
    sandbox: bash_sandbox_module.BashSandbox,
    timeout_seconds: float,
    maximum_output_characters: int,
):
    """Creates the session without starting bash.

    Args:
        sandbox (bash_sandbox_module.BashSandbox): The sandbox to run in.
        timeout_seconds (float): How long one command may run.
        maximum_output_characters (int): How much output to keep from one command.
    """
    self._sandbox = sandbox
    self.timeout_seconds = timeout_seconds
    self._maximum_output_characters = maximum_output_characters
    self._process = None
    self._lock = asyncio.Lock()

run async

run(command)

Runs one command and waits for it to finish.

Commands from parallel tool calls run one after another, never at the same time.

Parameters:

Name Type Description Default
command str

The command, as typed at a bash prompt.

required

Returns:

Type Description
CommandOutcome

Its output and exit status.

Source code in src/trading_agent/tools/bash_session.py
async def run(self, command: str) -> CommandOutcome:
    """Runs one command and waits for it to finish.

    Commands from parallel tool calls run one after another, never at the same time.

    Args:
        command (str): The command, as typed at a bash prompt.

    Returns:
        CommandOutcome: Its output and exit status.
    """
    async with self._lock:
        process = await self._started_process()
        marker = f'{_MARKER_PREFIX}{uuid.uuid4().hex}'
        script = '\n'.join(
            [
                '{',
                command,
                '} < /dev/null',
                f'printf \'\\n{marker} %s\\n\' "$?"',
                '',
            ]
        )
        try:
            process.stdin.write(script.encode('utf-8'))
            await process.stdin.drain()
        except ConnectionError:
            return await self._failed_start(process)
        try:
            return await asyncio.wait_for(
                self._read_until(process, marker),
                timeout=self.timeout_seconds,
            )
        except TimeoutError:
            await self._stop()
            return CommandOutcome(
                output=f'The command did not finish within {self.timeout_seconds:g} seconds. The terminal was restarted, so the folder and variables are reset; use bash_background for long commands.',
                exit_status=None,
                truncated=False,
            )

run_in_background async

run_in_background(command)

Starts a long command that keeps running after this call returns.

Its output goes to a log file in the scratch folder. Background commands stop when the session restarts.

Parameters:

Name Type Description Default
command str

The command.

required

Returns:

Type Description
str

The job's identifier, for check_background.

Source code in src/trading_agent/tools/bash_session.py
async def run_in_background(self, command: str) -> str:
    """Starts a long command that keeps running after this call returns.

    Its output goes to a log file in the scratch folder. Background commands stop when the session restarts.

    Args:
        command (str): The command.

    Returns:
        str: The job's identifier, for check_background.
    """
    job_id = uuid.uuid4().hex[:8]
    folder = shlex.quote(
        str(self._sandbox.scratch_directory / _BACKGROUND_FOLDER)
    )
    await self.run(
        f'mkdir -p {folder} && (nohup bash -c {shlex.quote(command)} > {folder}/{job_id}.log 2>&1 & echo $! > {folder}/{job_id}.pid)'
    )
    return job_id

check_background async

check_background(job_id)

Reports whether a background job is still running, and the end of its log.

Parameters:

Name Type Description Default
job_id str

The identifier from run_in_background.

required

Returns:

Type Description
CommandOutcome

"running" or "finished", then the last 4,000 characters of its log.

Source code in src/trading_agent/tools/bash_session.py
async def check_background(self, job_id: str) -> CommandOutcome:
    """Reports whether a background job is still running, and the end of its log.

    Args:
        job_id (str): The identifier from run_in_background.

    Returns:
        CommandOutcome: "running" or "finished", then the last 4,000 characters of its log.
    """
    folder = shlex.quote(
        str(self._sandbox.scratch_directory / _BACKGROUND_FOLDER)
    )
    safe_id = shlex.quote(job_id)
    return await self.run(
        '\n'.join(
            [
                f'if [ ! -f {folder}/{safe_id}.pid ]; then echo "no such job: {job_id}"; else',
                f'  if kill -0 "$(cat {folder}/{safe_id}.pid)" 2>/dev/null; then echo running; else echo finished; fi',
                f'  tail -c 4000 {folder}/{safe_id}.log',
                'fi',
            ]
        )
    )

restart async

restart()

Stops bash, so the next command starts a fresh terminal.

Source code in src/trading_agent/tools/bash_session.py
async def restart(self) -> None:
    """Stops bash, so the next command starts a fresh terminal."""
    async with self._lock:
        await self._stop()

close async

close()

Stops bash for good, when the desk stops.

Source code in src/trading_agent/tools/bash_session.py
async def close(self) -> None:
    """Stops bash for good, when the desk stops."""
    await self._stop()