tool_output_limits.py
1 """Configurable tool-output truncation limits. 2 3 Ported from anomalyco/opencode PR #23770 (``feat(truncate): allow 4 configuring tool output truncation limits``). 5 6 OpenCode hardcoded ``MAX_LINES = 2000`` and ``MAX_BYTES = 50 * 1024`` 7 as tool-output truncation thresholds. Hermes-agent had the same 8 hardcoded constants in two places: 9 10 * ``tools/terminal_tool.py`` — ``MAX_OUTPUT_CHARS = 50000`` (terminal 11 stdout/stderr cap) 12 * ``tools/file_operations.py`` — ``MAX_LINES = 2000`` / 13 ``MAX_LINE_LENGTH = 2000`` (read_file pagination cap + per-line cap) 14 15 This module centralises those values behind a single config section 16 (``tool_output`` in ``config.yaml``) so power users can tune them 17 without patching the source. The existing hardcoded numbers remain as 18 defaults, so behaviour is unchanged when the config key is absent. 19 20 Example ``config.yaml``:: 21 22 tool_output: 23 max_bytes: 100000 # terminal output cap (chars) 24 max_lines: 5000 # read_file pagination + truncation cap 25 max_line_length: 2000 # per-line length cap before '... [truncated]' 26 27 The limits reader is defensive: any error (missing config file, invalid 28 value type, etc.) falls back to the built-in defaults so tools never 29 fail because of a malformed config. 30 """ 31 32 from __future__ import annotations 33 34 from typing import Any, Dict 35 36 # Hardcoded defaults — these match the pre-existing values, so adding 37 # this module is behaviour-preserving for users who don't set 38 # ``tool_output`` in config.yaml. 39 DEFAULT_MAX_BYTES = 50_000 # terminal_tool.MAX_OUTPUT_CHARS 40 DEFAULT_MAX_LINES = 2000 # file_operations.MAX_LINES 41 DEFAULT_MAX_LINE_LENGTH = 2000 # file_operations.MAX_LINE_LENGTH 42 43 44 def _coerce_positive_int(value: Any, default: int) -> int: 45 """Return ``value`` as a positive int, or ``default`` on any issue.""" 46 try: 47 iv = int(value) 48 except (TypeError, ValueError): 49 return default 50 if iv <= 0: 51 return default 52 return iv 53 54 55 def get_tool_output_limits() -> Dict[str, int]: 56 """Return resolved tool-output limits, reading ``tool_output`` from config. 57 58 Keys: ``max_bytes``, ``max_lines``, ``max_line_length``. Missing or 59 invalid entries fall through to the ``DEFAULT_*`` constants. This 60 function NEVER raises. 61 """ 62 try: 63 from hermes_cli.config import load_config 64 cfg = load_config() or {} 65 section = cfg.get("tool_output") if isinstance(cfg, dict) else None 66 if not isinstance(section, dict): 67 section = {} 68 except Exception: 69 section = {} 70 71 return { 72 "max_bytes": _coerce_positive_int(section.get("max_bytes"), DEFAULT_MAX_BYTES), 73 "max_lines": _coerce_positive_int(section.get("max_lines"), DEFAULT_MAX_LINES), 74 "max_line_length": _coerce_positive_int( 75 section.get("max_line_length"), DEFAULT_MAX_LINE_LENGTH 76 ), 77 } 78 79 80 def get_max_bytes() -> int: 81 """Shortcut for terminal-tool callers that only need the byte cap.""" 82 return get_tool_output_limits()["max_bytes"] 83 84 85 def get_max_lines() -> int: 86 """Shortcut for file-ops callers that only need the line cap.""" 87 return get_tool_output_limits()["max_lines"] 88 89 90 def get_max_line_length() -> int: 91 """Shortcut for file-ops callers that only need the per-line cap.""" 92 return get_tool_output_limits()["max_line_length"]