# Copyright (c) 2023 Anthropic # Copyright (c) 2025 ByteDance Ltd. and/or its affiliates. # SPDX-License-Identifier: MIT # # This file has been modified by ByteDance Ltd. and/or its affiliates. on 13 June 2025 # # Original file was released under MIT License, with the full license text # available at https://github.com/anthropics/anthropic-quickstarts/blob/main/LICENSE # # This modified file is released under the same license. """Utility to run shell commands asynchronously with a timeout.""" import asyncio import contextlib TRUNCATED_MESSAGE: str = "To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for." MAX_RESPONSE_LEN: int = 16000 def maybe_truncate(content: str, truncate_after: int | None = MAX_RESPONSE_LEN): """Truncate content and append a notice if content exceeds the specified length.""" return ( content if not truncate_after or len(content) <= truncate_after else content[:truncate_after] + TRUNCATED_MESSAGE ) async def run( cmd: str, timeout: float | None = 120.0, # seconds truncate_after: int | None = MAX_RESPONSE_LEN, ): """Run a shell command asynchronously with a timeout.""" process = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) try: stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout) return ( process.returncode or 0, maybe_truncate(stdout.decode(), truncate_after=truncate_after), maybe_truncate(stderr.decode(), truncate_after=truncate_after), ) except asyncio.TimeoutError as exc: with contextlib.suppress(ProcessLookupError): process.kill() raise TimeoutError(f"Command '{cmd}' timed out after {timeout} seconds") from exc