mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: preserve existing thresholds, remove pre-read byte guard
- DEFAULT_RESULT_SIZE_CHARS: 50K -> 100K (match current _LARGE_RESULT_CHARS) - DEFAULT_PREVIEW_SIZE_CHARS: 2K -> 1.5K (match current _LARGE_RESULT_PREVIEW_CHARS) - Per-tool overrides all set to 100K (terminal, execute_code, search_files) - Remove pre-read byte guard (no behavioral regression vs current main) - Revert limit signature change to int=500 (match current default) - Restore original read_file schema description - Update test assertions to match 100K thresholds
This commit is contained in:
parent
bbcff8dcd0
commit
3696c74bfb
5 changed files with 11 additions and 35 deletions
|
|
@ -395,7 +395,7 @@ class TestEnforceTurnBudget:
|
||||||
assert PERSISTED_OUTPUT_TAG in msgs[1]["content"]
|
assert PERSISTED_OUTPUT_TAG in msgs[1]["content"]
|
||||||
|
|
||||||
def test_medium_result_regression(self):
|
def test_medium_result_regression(self):
|
||||||
"""6 results of 42K chars each (252K total) — each under 50K default
|
"""6 results of 42K chars each (252K total) — each under 100K default
|
||||||
threshold but aggregate exceeds 200K budget. L3 should persist."""
|
threshold but aggregate exceeds 200K budget. L3 should persist."""
|
||||||
env = MagicMock()
|
env = MagicMock()
|
||||||
env.execute.return_value = {"output": "", "returncode": 0}
|
env.execute.return_value = {"output": "", "returncode": 0}
|
||||||
|
|
@ -449,7 +449,7 @@ class TestPerToolThresholds:
|
||||||
try:
|
try:
|
||||||
import tools.terminal_tool # noqa: F401
|
import tools.terminal_tool # noqa: F401
|
||||||
val = registry.get_max_result_size("terminal")
|
val = registry.get_max_result_size("terminal")
|
||||||
assert val == 30_000
|
assert val == 100_000
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pytest.skip("terminal_tool not importable in test env")
|
pytest.skip("terminal_tool not importable in test env")
|
||||||
|
|
||||||
|
|
@ -467,6 +467,6 @@ class TestPerToolThresholds:
|
||||||
try:
|
try:
|
||||||
import tools.file_tools # noqa: F401
|
import tools.file_tools # noqa: F401
|
||||||
val = registry.get_max_result_size("search_files")
|
val = registry.get_max_result_size("search_files")
|
||||||
assert val == 20_000
|
assert val == 100_000
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pytest.skip("file_tools not importable in test env")
|
pytest.skip("file_tools not importable in test env")
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ PINNED_THRESHOLDS: Dict[str, float] = {
|
||||||
|
|
||||||
# Defaults matching the current hardcoded values in tool_result_storage.py.
|
# Defaults matching the current hardcoded values in tool_result_storage.py.
|
||||||
# Kept here as the single source of truth; tool_result_storage.py imports these.
|
# Kept here as the single source of truth; tool_result_storage.py imports these.
|
||||||
DEFAULT_RESULT_SIZE_CHARS: int = 50_000
|
DEFAULT_RESULT_SIZE_CHARS: int = 100_000
|
||||||
DEFAULT_TURN_BUDGET_CHARS: int = 200_000
|
DEFAULT_TURN_BUDGET_CHARS: int = 200_000
|
||||||
DEFAULT_PREVIEW_SIZE_CHARS: int = 2_000
|
DEFAULT_PREVIEW_SIZE_CHARS: int = 1_500
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
|
||||||
|
|
@ -1343,5 +1343,5 @@ registry.register(
|
||||||
enabled_tools=kw.get("enabled_tools")),
|
enabled_tools=kw.get("enabled_tools")),
|
||||||
check_fn=check_sandbox_requirements,
|
check_fn=check_sandbox_requirements,
|
||||||
emoji="🐍",
|
emoji="🐍",
|
||||||
max_result_size_chars=30_000,
|
max_result_size_chars=100_000,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ _EXPECTED_WRITE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS}
|
||||||
# Configurable via config.yaml: file_read_max_chars: 200000
|
# Configurable via config.yaml: file_read_max_chars: 200000
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_DEFAULT_MAX_READ_CHARS = 100_000
|
_DEFAULT_MAX_READ_CHARS = 100_000
|
||||||
_PRE_READ_MAX_BYTES = 256_000 # reject full-file reads on files larger than this
|
|
||||||
_DEFAULT_READ_LIMIT = 500
|
|
||||||
_max_read_chars_cached: int | None = None
|
_max_read_chars_cached: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -279,7 +277,7 @@ def clear_file_ops_cache(task_id: str = None):
|
||||||
_file_ops_cache.clear()
|
_file_ops_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
def read_file_tool(path: str, offset: int = 1, limit: int | None = None, task_id: str = "default") -> str:
|
def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str = "default") -> str:
|
||||||
"""Read a file with pagination and line numbers."""
|
"""Read a file with pagination and line numbers."""
|
||||||
try:
|
try:
|
||||||
# ── Device path guard ─────────────────────────────────────────
|
# ── Device path guard ─────────────────────────────────────────
|
||||||
|
|
@ -327,28 +325,6 @@ def read_file_tool(path: str, offset: int = 1, limit: int | None = None, task_id
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ── Pre-read file size guard ──────────────────────────────────
|
|
||||||
# Guard only when the caller omits limit; an explicit limit means
|
|
||||||
# the caller knows what slice it wants.
|
|
||||||
if limit is None:
|
|
||||||
try:
|
|
||||||
_fsize = os.path.getsize(str(_resolved))
|
|
||||||
except OSError:
|
|
||||||
_fsize = 0
|
|
||||||
if _fsize > _PRE_READ_MAX_BYTES:
|
|
||||||
return json.dumps({
|
|
||||||
"error": (
|
|
||||||
f"File is too large to read in full ({_fsize:,} bytes). "
|
|
||||||
f"Use offset and limit parameters to read specific sections "
|
|
||||||
f"(e.g. offset=1, limit=100 for the first 100 lines)."
|
|
||||||
),
|
|
||||||
"path": path,
|
|
||||||
"file_size": _fsize,
|
|
||||||
}, ensure_ascii=False)
|
|
||||||
|
|
||||||
if limit is None:
|
|
||||||
limit = _DEFAULT_READ_LIMIT
|
|
||||||
|
|
||||||
# ── Dedup check ───────────────────────────────────────────────
|
# ── Dedup check ───────────────────────────────────────────────
|
||||||
# If we already read this exact (path, offset, limit) and the
|
# If we already read this exact (path, offset, limit) and the
|
||||||
# file hasn't been modified since, return a lightweight stub
|
# file hasn't been modified since, return a lightweight stub
|
||||||
|
|
@ -762,7 +738,7 @@ def _check_file_reqs():
|
||||||
|
|
||||||
READ_FILE_SCHEMA = {
|
READ_FILE_SCHEMA = {
|
||||||
"name": "read_file",
|
"name": "read_file",
|
||||||
"description": "Read a text file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. When you already know which part of the file you need, only read that part using offset and limit — this is important for larger files. Files over 256KB will be rejected unless you provide a limit parameter. NOTE: Cannot read images or binary files — use vision_analyze for images.",
|
"description": "Read a text file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Use offset and limit for large files. Reads exceeding ~100K characters are rejected; use offset and limit to read specific sections of large files. NOTE: Cannot read images or binary files — use vision_analyze for images.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -826,7 +802,7 @@ SEARCH_FILES_SCHEMA = {
|
||||||
|
|
||||||
def _handle_read_file(args, **kw):
|
def _handle_read_file(args, **kw):
|
||||||
tid = kw.get("task_id") or "default"
|
tid = kw.get("task_id") or "default"
|
||||||
return read_file_tool(path=args.get("path", ""), offset=args.get("offset", 1), limit=args.get("limit"), task_id=tid)
|
return read_file_tool(path=args.get("path", ""), offset=args.get("offset", 1), limit=args.get("limit", 500), task_id=tid)
|
||||||
|
|
||||||
|
|
||||||
def _handle_write_file(args, **kw):
|
def _handle_write_file(args, **kw):
|
||||||
|
|
@ -856,4 +832,4 @@ def _handle_search_files(args, **kw):
|
||||||
registry.register(name="read_file", toolset="file", schema=READ_FILE_SCHEMA, handler=_handle_read_file, check_fn=_check_file_reqs, emoji="📖", max_result_size_chars=float('inf'))
|
registry.register(name="read_file", toolset="file", schema=READ_FILE_SCHEMA, handler=_handle_read_file, check_fn=_check_file_reqs, emoji="📖", max_result_size_chars=float('inf'))
|
||||||
registry.register(name="write_file", toolset="file", schema=WRITE_FILE_SCHEMA, handler=_handle_write_file, check_fn=_check_file_reqs, emoji="✍️", max_result_size_chars=100_000)
|
registry.register(name="write_file", toolset="file", schema=WRITE_FILE_SCHEMA, handler=_handle_write_file, check_fn=_check_file_reqs, emoji="✍️", max_result_size_chars=100_000)
|
||||||
registry.register(name="patch", toolset="file", schema=PATCH_SCHEMA, handler=_handle_patch, check_fn=_check_file_reqs, emoji="🔧", max_result_size_chars=100_000)
|
registry.register(name="patch", toolset="file", schema=PATCH_SCHEMA, handler=_handle_patch, check_fn=_check_file_reqs, emoji="🔧", max_result_size_chars=100_000)
|
||||||
registry.register(name="search_files", toolset="file", schema=SEARCH_FILES_SCHEMA, handler=_handle_search_files, check_fn=_check_file_reqs, emoji="🔎", max_result_size_chars=20_000)
|
registry.register(name="search_files", toolset="file", schema=SEARCH_FILES_SCHEMA, handler=_handle_search_files, check_fn=_check_file_reqs, emoji="🔎", max_result_size_chars=100_000)
|
||||||
|
|
|
||||||
|
|
@ -1623,5 +1623,5 @@ registry.register(
|
||||||
handler=_handle_terminal,
|
handler=_handle_terminal,
|
||||||
check_fn=check_terminal_requirements,
|
check_fn=check_terminal_requirements,
|
||||||
emoji="💻",
|
emoji="💻",
|
||||||
max_result_size_chars=30_000,
|
max_result_size_chars=100_000,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue