"""Configurable tool-output truncation limits (``tool_output`` in config.yaml). Centralises the caps previously hardcoded in ``terminal_tool`` (``max_bytes``) and ``file_operations`` (``max_lines`` / ``max_line_length``). Defaults equal the old constants, so behaviour is unchanged when the section is absent, and the reader never raises — any config error falls back to the defaults. Ported from anomalyco/opencode PR #23770. Example:: tool_output: max_bytes: 100000 # terminal output cap (chars) max_lines: 5000 # read_file pagination + truncation cap max_line_length: 2000 # per-line cap before '... [truncated]' """ from __future__ import annotations from typing import Any, Dict DEFAULT_MAX_BYTES = 50_000 # terminal_tool.MAX_OUTPUT_CHARS DEFAULT_MAX_LINES = 2000 # file_operations.MAX_LINES DEFAULT_MAX_LINE_LENGTH = 2000 # file_operations.MAX_LINE_LENGTH # Process-lifetime cache: avoids re-reading config.yaml on every tool call. _cached_limits: dict | None = None def _coerce_int(value: Any, default: int, minimum: int) -> int: """Return ``value`` as an int >= ``minimum``, or ``default`` on any issue.""" try: iv = int(value) except (TypeError, ValueError): return default return default if iv < minimum else iv def _coerce_positive_int(value: Any, default: int) -> int: """Return ``value`` as a positive int, or ``default`` on any issue.""" return _coerce_int(value, default, 1) def get_tool_output_limits() -> Dict[str, int]: """Return resolved limits ``{max_bytes, max_lines, max_line_length}``; never raises. Cached for the process lifetime — ``_reset_tool_output_limits_cache()`` forces a fresh read after config changes. """ global _cached_limits if _cached_limits is not None: return _cached_limits try: from hermes_cli.config import load_config cfg = load_config() or {} section = cfg.get("tool_output") if isinstance(cfg, dict) else None if not isinstance(section, dict): section = {} except Exception: section = {} _cached_limits = { "max_bytes": _coerce_positive_int(section.get("max_bytes"), DEFAULT_MAX_BYTES), "max_lines": _coerce_positive_int(section.get("max_lines"), DEFAULT_MAX_LINES), "max_line_length": _coerce_positive_int( section.get("max_line_length"), DEFAULT_MAX_LINE_LENGTH ), } return _cached_limits def _reset_tool_output_limits_cache() -> None: """Reset the cached limits — for tests or after config hot-reload.""" global _cached_limits _cached_limits = None def get_max_bytes() -> int: return get_tool_output_limits()["max_bytes"] def get_max_lines() -> int: return get_tool_output_limits()["max_lines"] def get_max_line_length() -> int: return get_tool_output_limits()["max_line_length"]