mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
feat(config): make tool output truncation limits configurable
Port from anomalyco/opencode#23770: expose a new `tool_output` config section so users can tune the hardcoded truncation caps that apply to terminal output and read_file pagination. Three knobs under `tool_output`: - max_bytes (default 50_000) — terminal stdout/stderr cap - max_lines (default 2000) — read_file pagination cap - max_line_length (default 2000) — per-line cap in line-numbered view All three keep their existing hardcoded values as defaults, so behaviour is unchanged when the section is absent. Power users on big-context models can raise them; small-context local models can lower them. Implementation: - New `tools/tool_output_limits.py` reads the section with defensive fallback (missing/invalid values → defaults, never raises). - `tools/terminal_tool.py` MAX_OUTPUT_CHARS now comes from get_max_bytes(). - `tools/file_operations.py` normalize_read_pagination() and _add_line_numbers() now pull the limits at call time. - `hermes_cli/config.py` DEFAULT_CONFIG gains the `tool_output` section so `hermes setup` writes defaults into fresh configs. - Docs page `user-guide/configuration.md` gains a "Tool Output Truncation Limits" section with large-context and small-context example configs. Tests (18 new in tests/tools/test_tool_output_limits.py): - Default resolution with missing / malformed / non-dict config. - Full and partial user overrides. - Coercion of bad values (None, negative, wrong type, str int). - Shortcut accessors delegate correctly. - DEFAULT_CONFIG exposes the section with the right defaults. - Integration: normalize_read_pagination clamps to the configured max_lines.
This commit is contained in:
parent
c95c6bdb7c
commit
f2b1b3f1a3
6 changed files with 306 additions and 5 deletions
152
tests/tools/test_tool_output_limits.py
Normal file
152
tests/tools/test_tool_output_limits.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"""Tests for tools.tool_output_limits.
|
||||
|
||||
Covers:
|
||||
1. Default values when no config is provided.
|
||||
2. Config override picks up user-supplied max_bytes / max_lines /
|
||||
max_line_length.
|
||||
3. Malformed values (None, negative, wrong type) fall back to defaults
|
||||
rather than raising.
|
||||
4. Integration: the helpers return what the terminal_tool and
|
||||
file_operations call paths will actually consume.
|
||||
|
||||
Port-tracking: anomalyco/opencode PR #23770
|
||||
(feat(truncate): allow configuring tool output truncation limits).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tools import tool_output_limits as tol
|
||||
|
||||
|
||||
class TestDefaults:
|
||||
def test_defaults_match_previous_hardcoded_values(self):
|
||||
assert tol.DEFAULT_MAX_BYTES == 50_000
|
||||
assert tol.DEFAULT_MAX_LINES == 2000
|
||||
assert tol.DEFAULT_MAX_LINE_LENGTH == 2000
|
||||
|
||||
def test_get_limits_returns_defaults_when_config_missing(self):
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits == {
|
||||
"max_bytes": tol.DEFAULT_MAX_BYTES,
|
||||
"max_lines": tol.DEFAULT_MAX_LINES,
|
||||
"max_line_length": tol.DEFAULT_MAX_LINE_LENGTH,
|
||||
}
|
||||
|
||||
def test_get_limits_returns_defaults_when_config_not_a_dict(self):
|
||||
# load_config should always return a dict but be defensive anyway.
|
||||
with patch("hermes_cli.config.load_config", return_value="not a dict"):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES
|
||||
|
||||
def test_get_limits_returns_defaults_when_load_config_raises(self):
|
||||
def _boom():
|
||||
raise RuntimeError("boom")
|
||||
|
||||
with patch("hermes_cli.config.load_config", side_effect=_boom):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_lines"] == tol.DEFAULT_MAX_LINES
|
||||
|
||||
|
||||
class TestOverrides:
|
||||
def test_user_config_overrides_all_three(self):
|
||||
cfg = {
|
||||
"tool_output": {
|
||||
"max_bytes": 100_000,
|
||||
"max_lines": 5000,
|
||||
"max_line_length": 4096,
|
||||
}
|
||||
}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits == {
|
||||
"max_bytes": 100_000,
|
||||
"max_lines": 5000,
|
||||
"max_line_length": 4096,
|
||||
}
|
||||
|
||||
def test_partial_override_preserves_other_defaults(self):
|
||||
cfg = {"tool_output": {"max_bytes": 200_000}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_bytes"] == 200_000
|
||||
assert limits["max_lines"] == tol.DEFAULT_MAX_LINES
|
||||
assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH
|
||||
|
||||
def test_section_not_a_dict_falls_back(self):
|
||||
cfg = {"tool_output": "nonsense"}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES
|
||||
|
||||
|
||||
class TestCoercion:
|
||||
@pytest.mark.parametrize("bad", [None, "not a number", -1, 0, [], {}])
|
||||
def test_invalid_values_fall_back_to_defaults(self, bad):
|
||||
cfg = {"tool_output": {"max_bytes": bad, "max_lines": bad, "max_line_length": bad}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES
|
||||
assert limits["max_lines"] == tol.DEFAULT_MAX_LINES
|
||||
assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH
|
||||
|
||||
def test_string_integer_is_coerced(self):
|
||||
cfg = {"tool_output": {"max_bytes": "75000"}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
limits = tol.get_tool_output_limits()
|
||||
assert limits["max_bytes"] == 75_000
|
||||
|
||||
|
||||
class TestShortcuts:
|
||||
def test_individual_accessors_delegate_to_get_tool_output_limits(self):
|
||||
cfg = {
|
||||
"tool_output": {
|
||||
"max_bytes": 111,
|
||||
"max_lines": 222,
|
||||
"max_line_length": 333,
|
||||
}
|
||||
}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
assert tol.get_max_bytes() == 111
|
||||
assert tol.get_max_lines() == 222
|
||||
assert tol.get_max_line_length() == 333
|
||||
|
||||
|
||||
class TestDefaultConfigHasSection:
|
||||
"""The DEFAULT_CONFIG in hermes_cli.config must expose tool_output so
|
||||
that ``hermes setup`` and default installs stay in sync with the
|
||||
helpers here."""
|
||||
|
||||
def test_default_config_contains_tool_output_section(self):
|
||||
from hermes_cli.config import DEFAULT_CONFIG
|
||||
assert "tool_output" in DEFAULT_CONFIG
|
||||
section = DEFAULT_CONFIG["tool_output"]
|
||||
assert isinstance(section, dict)
|
||||
assert section["max_bytes"] == tol.DEFAULT_MAX_BYTES
|
||||
assert section["max_lines"] == tol.DEFAULT_MAX_LINES
|
||||
assert section["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH
|
||||
|
||||
|
||||
class TestIntegrationReadPagination:
|
||||
"""normalize_read_pagination uses get_max_lines() — verify the plumbing."""
|
||||
|
||||
def test_pagination_limit_clamped_by_config_value(self):
|
||||
from tools.file_operations import normalize_read_pagination
|
||||
cfg = {"tool_output": {"max_lines": 50}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
offset, limit = normalize_read_pagination(offset=1, limit=1000)
|
||||
# limit should have been clamped to 50 (the configured max_lines)
|
||||
assert limit == 50
|
||||
assert offset == 1
|
||||
|
||||
def test_pagination_default_when_config_missing(self):
|
||||
from tools.file_operations import normalize_read_pagination
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
offset, limit = normalize_read_pagination(offset=10, limit=100000)
|
||||
# Clamped to default MAX_LINES (2000).
|
||||
assert limit == tol.DEFAULT_MAX_LINES
|
||||
assert offset == 10
|
||||
Loading…
Add table
Add a link
Reference in a new issue