Source code for ralph.mcp.tools._exec_execution_error
"""ExecutionError for the exec MCP tool."""
from __future__ import annotations
from ralph.mcp.tools.coordination import ToolError
[docs]
class ExecutionError(ToolError):
"""Raised when the exec subprocess cannot be started or times out.
Optional keyword fields enable structured, agent-actionable error messages
for cache-full and workspace-limit scenarios.
"""
def __init__(
self,
message: str = "",
*,
# Cache-full fields
current_bytes: int | None = None,
cap_bytes: int | None = None,
removed_paths: int | None = None,
removed_bytes: int | None = None,
remaining_bytes: int | None = None,
# Workspace-limit fields
workspace_bytes: int | None = None,
max_workspace_bytes: int | None = None,
# Timeout fields
timed_out: bool = False,
timeout_ms: int | None = None,
suggested_timeout_ms: int | None = None,
# Diagnostics summary line
diagnostics: str | None = None,
) -> None:
super().__init__(message)
self.current_bytes = current_bytes
self.cap_bytes = cap_bytes
self.removed_paths = removed_paths
self.removed_bytes = removed_bytes
self.remaining_bytes = remaining_bytes
self.workspace_bytes = workspace_bytes
self.max_workspace_bytes = max_workspace_bytes
self.timed_out = timed_out
self.timeout_ms = timeout_ms
self.suggested_timeout_ms = suggested_timeout_ms
self.diagnostics = diagnostics
def __str__(self) -> str:
"""Render the appropriate structured template based on populated fields."""
# Timeout template
if self.timed_out:
return self._render_timeout()
# Cache-full template
if self.current_bytes is not None and self.cap_bytes is not None:
return self._render_cache_full()
# Workspace-limit template
if self.workspace_bytes is not None and self.max_workspace_bytes is not None:
return self._render_workspace_limit()
# Fallback: bare message
base = super().__str__()
if self.diagnostics:
return f"{base}\n Diagnostics: {self.diagnostics}"
return base
def _render_timeout(self) -> str:
ms = self.timeout_ms if self.timeout_ms is not None else "?"
lines: list[str] = [f"Command timed out after {ms}ms (process killed)."]
lines.append(
"Re-issuing the IDENTICAL call will time out again. A timeout has two"
" possible causes — decide which before retrying:"
)
if self.suggested_timeout_ms is not None:
lines.append(
f"1. The command is legitimately long-running: pass a larger timeout_ms"
f" (e.g. {self.suggested_timeout_ms}) or run a shorter command."
)
else:
lines.append(
"1. The command is legitimately long-running: pass a larger timeout_ms"
" or run a shorter command."
)
lines.append(
"2. The command is genuinely stuck (infinite loop, deadlock, or blocked"
" waiting on input): raising timeout_ms will only waste more time — fix the"
" command itself. Do not retry unchanged."
)
if self.diagnostics:
lines.append(f" Diagnostics: {self.diagnostics}")
return "\n".join(lines)
def _render_cache_full(self) -> str:
lines: list[str] = ["Error: Exec cache exceeds capacity after automatic reset"]
if self.current_bytes is not None:
lines.append(f" Current usage: {self.current_bytes} bytes")
if self.cap_bytes is not None:
lines.append(f" Hard cap: {self.cap_bytes} bytes")
if self.removed_paths is not None or self.removed_bytes is not None:
removed_p = self.removed_paths or 0
removed_b = self.removed_bytes or 0
remaining = self.remaining_bytes or 0
lines.append(
f" Automatic reset: removed {removed_p} paths ({removed_b} bytes), "
f"{remaining} bytes remaining"
)
lines.append(
" Remaining bytes usually indicate active/live exec slots "
"or filesystem permission issues"
)
if self.diagnostics:
lines.append(f" Diagnostics: {self.diagnostics}")
return "\n".join(lines)
def _render_workspace_limit(self) -> str:
lines: list[str] = ["Error: Workspace exceeds safety size limit"]
if self.workspace_bytes is not None:
lines.append(f" Workspace size: {self.workspace_bytes} bytes")
if self.max_workspace_bytes is not None:
lines.append(f" Limit: {self.max_workspace_bytes} bytes")
lines.append(
" Suggested exclusions: node_modules, .venv, .mypy_cache, __pycache__, .tox, .nox"
)
lines.append(
" Suggestion: Add large generated directories to .gitignore or reduce workspace scope"
)
if self.diagnostics:
lines.append(f" Diagnostics: {self.diagnostics}")
return "\n".join(lines)