mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +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. * feat(skills): add design-md skill for Google's DESIGN.md spec Built-in skill under skills/creative/ that teaches the agent to author, lint, diff, and export DESIGN.md files — Google's open-source (Apache-2.0) format for describing a visual identity to coding agents. Covers: - YAML front matter + markdown body anatomy - Full token schema (colors, typography, rounded, spacing, components) - Canonical section order + duplicate-heading rejection - Component property whitelist + variants-as-siblings pattern - CLI workflow via 'npx @google/design.md' (lint/diff/export/spec) - Lint rule reference including WCAG contrast checks - Common YAML pitfalls (quoted hex, negative dimensions, dotted refs) - Starter template at templates/starter.md Package verified live on npm (@google/design.md@0.1.1).
152 lines
5.9 KiB
Python
152 lines
5.9 KiB
Python
"""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
|