Under `gateway.multiplex_profiles` one gateway process serves every profile
under ~/.hermes/profiles/NAME/; each routed turn runs with a context-local
HERMES_HOME override while `os.environ` still holds the DEFAULT profile's
values. Anything evaluated once at import, or memoised in a single unkeyed
module slot, therefore freezes the LAUNCH profile's value and leaks it into
every other profile's turns. This lands the tools-side half of that class:
- tools/process_registry.py, tools/environments/{modal,singularity}.py:
`_checkpoint_path()` / `_snapshot_store()` resolve `get_hermes_home()` at
call time (same seam as `tools/skills_tool._skills_dir`, so the existing
`monkeypatch.setattr(CHECKPOINT_PATH)` test sites keep working). Completes
the checkpoint_manager / sticker_cache half cherry-picked from #56315.
- plugins/platforms/feishu/feishu_comment_rules.py: `_MtimeCache` is now
path-keyed (accepts a Path or a zero-arg resolver, one (mtime, data) slot
per resolved path) with `invalidate()`; `_rules_file()` / `_pairing_file()`
resolve the routed profile's files. Proposed in #63962.
- tools/tool_output_limits.py, tools/browser_tool.py, tools/browser_camofox.py:
the process-lifetime config caches are dicts keyed by `hermes_home_key()`;
the `_X_resolved` flags and the lifecycle reset keep their shape.
tools/file_tools.py drops its private `file_read_max_chars` memo and reads
the already mtime+path-cached `load_config_readonly()`.
- hermes_time.py: `get_timezone_name()`; when `is_multiplex_active()` the
env `HERMES_TIMEZONE` (bridged from the default profile's config at gateway
startup) is ignored in favour of the routed profile's config.yaml. Both
sandbox TZ sites (code_execution_env/_tool) now use it.
- tools/cronjob_tools.py, tools/tts_tool.py, tools/skill_manager_tool.py:
the static schema text is profile-neutral and `dynamic_schema_overrides=`
rebuilds the `display_hermes_home()` / create-dir hint per
`get_definitions()`, so a routed profile's model sees its own paths.
Refs #95685.
Co-authored-by: Nathan Shan <nathanielcrush51@gmail.com>
(cherry picked from commit 6d3fc6b07b3155c6196b1fd61a829283f1d7855c)
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""Configurable tool-output truncation limits (``tool_output`` in config.yaml):
|
|
``max_bytes`` (terminal output cap), ``max_lines`` (read_file pagination cap),
|
|
``max_line_length`` (per-line cap before '... [truncated]'). Defaults equal the
|
|
constants once hardcoded in terminal_tool / file_operations and the reader never
|
|
raises, so behaviour is unchanged when the section is absent or malformed."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict
|
|
|
|
from hermes_constants import hermes_home_key
|
|
|
|
DEFAULT_MAX_BYTES = 50_000 # terminal_tool.MAX_OUTPUT_CHARS
|
|
DEFAULT_MAX_LINES = 2000 # file_operations.MAX_LINES
|
|
DEFAULT_MAX_LINE_LENGTH = 2000 # file_operations.MAX_LINE_LENGTH
|
|
# Keyed by profile home: the multiplexed gateway serves every profile from one process, so a
|
|
# single slot would hand the launch profile's limits to every other profile.
|
|
_cached_limits: Dict[str, Dict[str, int]] = {}
|
|
|
|
|
|
def _coerce_int(value: Any, default: int, minimum: int) -> int:
|
|
"""Return ``value`` as an int >= ``minimum``, or ``default`` on any issue."""
|
|
try:
|
|
iv = int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
return default if iv < minimum else iv
|
|
|
|
|
|
def _coerce_positive_int(value: Any, default: int) -> int:
|
|
return _coerce_int(value, default, 1) # positive int, or ``default`` on any issue
|
|
|
|
|
|
def get_tool_output_limits() -> Dict[str, int]:
|
|
"""Resolved ``{max_bytes, max_lines, max_line_length}``; never raises. Cached per profile
|
|
home for the process — ``_reset_tool_output_limits_cache()`` forces a fresh read."""
|
|
key = hermes_home_key()
|
|
cached = _cached_limits.get(key)
|
|
if cached is not None:
|
|
return cached
|
|
try:
|
|
from hermes_cli.config import load_config
|
|
cfg = load_config() or {}
|
|
section = cfg.get("tool_output") if isinstance(cfg, dict) else None
|
|
except Exception:
|
|
section = None
|
|
if not isinstance(section, dict):
|
|
section = {}
|
|
_cached_limits[key] = limits = {
|
|
"max_bytes": _coerce_positive_int(section.get("max_bytes"), DEFAULT_MAX_BYTES),
|
|
"max_lines": _coerce_positive_int(section.get("max_lines"), DEFAULT_MAX_LINES),
|
|
"max_line_length": _coerce_positive_int(
|
|
section.get("max_line_length"), DEFAULT_MAX_LINE_LENGTH)}
|
|
return limits
|
|
|
|
|
|
def _reset_tool_output_limits_cache() -> None:
|
|
"""Reset the cached limits — for tests or after config hot-reload."""
|
|
_cached_limits.clear()
|
|
|
|
|
|
def get_max_bytes() -> int: return get_tool_output_limits()["max_bytes"]
|
|
def get_max_lines() -> int: return get_tool_output_limits()["max_lines"]
|
|
def get_max_line_length() -> int: return get_tool_output_limits()["max_line_length"]
|