/ tools / tool_output_limits.py
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"]