Spaces:
Running
Running
Sync Bash output handling
Browse files- VERSION +1 -1
- agent_base/react_agent.py +11 -7
- agent_base/tools/tool_runtime.py +52 -3
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
v0.0.
|
|
|
|
| 1 |
+
v0.0.48
|
agent_base/react_agent.py
CHANGED
|
@@ -1324,13 +1324,17 @@ class MultiTurnReactAgent(BaseAgent):
|
|
| 1324 |
)
|
| 1325 |
|
| 1326 |
def execute_tool_item(item: dict[str, Any]) -> tuple[dict[str, Any], Any]:
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
return item, result
|
| 1335 |
|
| 1336 |
for batch_indexes in tool_execution_batches([str(item["tool_name"]) for item in tool_call_items]):
|
|
|
|
| 1324 |
)
|
| 1325 |
|
| 1326 |
def execute_tool_item(item: dict[str, Any]) -> tuple[dict[str, Any], Any]:
|
| 1327 |
+
tool_name = str(item["tool_name"])
|
| 1328 |
+
try:
|
| 1329 |
+
result = self.custom_call_tool(
|
| 1330 |
+
tool_name,
|
| 1331 |
+
item["tool_arguments"],
|
| 1332 |
+
workspace_root=resolved_workspace_root,
|
| 1333 |
+
runtime_deadline=runtime_deadline,
|
| 1334 |
+
model_name=self.model,
|
| 1335 |
+
)
|
| 1336 |
+
except Exception as exc:
|
| 1337 |
+
result = f"[{tool_name}] Tool execution error: {type(exc).__name__}: {exc}"
|
| 1338 |
return item, result
|
| 1339 |
|
| 1340 |
for batch_indexes in tool_execution_batches([str(item["tool_name"]) for item in tool_call_items]):
|
agent_base/tools/tool_runtime.py
CHANGED
|
@@ -31,6 +31,7 @@ DEFAULT_OUTPUT_CHARS = 20000
|
|
| 31 |
DEFAULT_YIELD_MS = 200
|
| 32 |
REPEAT_COLLAPSE_THRESHOLD = 3
|
| 33 |
|
|
|
|
| 34 |
def _default_shell() -> str:
|
| 35 |
return shutil.which("bash") or "/bin/bash"
|
| 36 |
|
|
@@ -100,6 +101,54 @@ def _bounded_output(text: str, *, max_output_chars: int = DEFAULT_OUTPUT_CHARS)
|
|
| 100 |
return compressed[:keep] + suffix
|
| 101 |
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
class Bash(ToolBase):
|
| 104 |
name = "Bash"
|
| 105 |
description = (
|
|
@@ -178,7 +227,7 @@ class Bash(ToolBase):
|
|
| 178 |
command,
|
| 179 |
shell=True,
|
| 180 |
capture_output=True,
|
| 181 |
-
text=
|
| 182 |
timeout=effective_timeout,
|
| 183 |
cwd=str(cwd),
|
| 184 |
env=sanitized_subprocess_env(base_root=base_root),
|
|
@@ -190,8 +239,8 @@ class Bash(ToolBase):
|
|
| 190 |
return f"[Bash] Error executing command: {exc}"
|
| 191 |
|
| 192 |
parts = [f"exit_code: {proc.returncode}"]
|
| 193 |
-
stdout =
|
| 194 |
-
stderr =
|
| 195 |
if stdout:
|
| 196 |
parts.append(f"stdout:\n{stdout}")
|
| 197 |
if stderr:
|
|
|
|
| 31 |
DEFAULT_YIELD_MS = 200
|
| 32 |
REPEAT_COLLAPSE_THRESHOLD = 3
|
| 33 |
|
| 34 |
+
|
| 35 |
def _default_shell() -> str:
|
| 36 |
return shutil.which("bash") or "/bin/bash"
|
| 37 |
|
|
|
|
| 101 |
return compressed[:keep] + suffix
|
| 102 |
|
| 103 |
|
| 104 |
+
def _looks_binary_output(data: bytes) -> bool:
|
| 105 |
+
if not data:
|
| 106 |
+
return False
|
| 107 |
+
sample = data[:4096]
|
| 108 |
+
if b"\x00" in sample:
|
| 109 |
+
return True
|
| 110 |
+
allowed_controls = {9, 10, 12, 13}
|
| 111 |
+
control_count = sum(1 for byte in sample if byte < 32 and byte not in allowed_controls)
|
| 112 |
+
return control_count / max(len(sample), 1) > 0.05
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _truncate_output_bytes(data: bytes, *, max_output_chars: int) -> tuple[bytes, bool, int]:
|
| 116 |
+
max_output_bytes = max(1, int(max_output_chars))
|
| 117 |
+
if len(data) <= max_output_bytes:
|
| 118 |
+
return data, False, 0
|
| 119 |
+
omitted = len(data) - max_output_bytes
|
| 120 |
+
suffix = f"\n[output truncated before decoding: omitted {omitted} bytes]\n".encode("utf-8")
|
| 121 |
+
keep = max(0, max_output_bytes - len(suffix))
|
| 122 |
+
return data[:keep] + suffix, True, omitted
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _safe_process_output(data: Union[str, bytes, None], *, max_output_chars: int = DEFAULT_OUTPUT_CHARS) -> str:
|
| 126 |
+
if data is None or data == "":
|
| 127 |
+
return ""
|
| 128 |
+
if isinstance(data, str):
|
| 129 |
+
return _bounded_output(data, max_output_chars=max_output_chars)
|
| 130 |
+
|
| 131 |
+
if not data:
|
| 132 |
+
return ""
|
| 133 |
+
if _looks_binary_output(data):
|
| 134 |
+
note = (
|
| 135 |
+
f"[binary output omitted: {len(data)} bytes. "
|
| 136 |
+
"Use file or a format-aware reader such as openpyxl/scipy.io.loadmat for binary files.]"
|
| 137 |
+
)
|
| 138 |
+
if len(data) > max_output_chars:
|
| 139 |
+
note += f"\n[output truncated before decoding: omitted {len(data) - max_output_chars} bytes]"
|
| 140 |
+
return note
|
| 141 |
+
|
| 142 |
+
bounded, _, _ = _truncate_output_bytes(data, max_output_chars=max_output_chars)
|
| 143 |
+
try:
|
| 144 |
+
decoded = bounded.decode("utf-8")
|
| 145 |
+
prefix = ""
|
| 146 |
+
except UnicodeDecodeError:
|
| 147 |
+
decoded = bounded.decode("utf-8", errors="replace")
|
| 148 |
+
prefix = "[non-UTF-8 bytes decoded with replacement characters]\n"
|
| 149 |
+
return _bounded_output(prefix + decoded, max_output_chars=max_output_chars)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
class Bash(ToolBase):
|
| 153 |
name = "Bash"
|
| 154 |
description = (
|
|
|
|
| 227 |
command,
|
| 228 |
shell=True,
|
| 229 |
capture_output=True,
|
| 230 |
+
text=False,
|
| 231 |
timeout=effective_timeout,
|
| 232 |
cwd=str(cwd),
|
| 233 |
env=sanitized_subprocess_env(base_root=base_root),
|
|
|
|
| 239 |
return f"[Bash] Error executing command: {exc}"
|
| 240 |
|
| 241 |
parts = [f"exit_code: {proc.returncode}"]
|
| 242 |
+
stdout = _safe_process_output(proc.stdout, max_output_chars=max_output_chars)
|
| 243 |
+
stderr = _safe_process_output(proc.stderr, max_output_chars=max_output_chars)
|
| 244 |
if stdout:
|
| 245 |
parts.append(f"stdout:\n{stdout}")
|
| 246 |
if stderr:
|