refactor(tools): group H — compact module/function docstrings by hand (keep WHY/invariants)
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
"""Symlink-safe creation helpers for spill/cache files.
|
||||
|
||||
Spill files live in predictable directories under ``~/.hermes``; a plain
|
||||
``open(path, "w")`` there would follow a pre-planted symlink and let a local
|
||||
process redirect the write onto ``~/.bashrc``, ``authorized_keys``, etc. Every
|
||||
helper refuses symlinks by construction: new files use ``O_CREAT | O_EXCL``
|
||||
(fails on ANY existing path, including a dangling link); overwrites ``lstat`` +
|
||||
``unlink`` the existing path first (removes the link, never its target), then
|
||||
create exclusively, so the pair can't be raced. ``private=True`` (default)
|
||||
forces ``0o700`` dirs / ``0o600`` files for spills that may hold pre-redaction
|
||||
secrets; ``private=False`` keeps umask perms for cache dirs bind-mounted into
|
||||
remote backends (``credential_files._CACHE_DIRS``) where a non-root container
|
||||
UID must read them. Disk failures are the caller's concern: helpers raise ``OSError``.
|
||||
"""
|
||||
"""Symlink-safe creation helpers for spill/cache files under ``~/.hermes``, where a
|
||||
plain ``open(path, "w")`` would follow a pre-planted symlink onto ``~/.bashrc`` etc.
|
||||
New files use ``O_CREAT | O_EXCL`` (fails on ANY existing path, even a dangling
|
||||
link); overwrites ``lstat`` + ``unlink`` first (removes the link, never its target)
|
||||
then create exclusively, so the pair can't be raced. ``private=True`` (default) =
|
||||
``0o700`` dirs / ``0o600`` files for spills that may hold pre-redaction secrets;
|
||||
``private=False`` keeps umask perms for cache dirs bind-mounted into remote backends
|
||||
(``credential_files._CACHE_DIRS``). Disk failures raise ``OSError`` to the caller."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Point at something in the Hermes desktop GUI and say one line about it.
|
||||
|
||||
The quiet sibling of ``tour``: same ``data-tour`` handles and discovery call,
|
||||
but an arrow bubble with no scrim/spotlight/paging. Fire-and-forget — a tip is
|
||||
not a question, so blocking the turn on a round-trip would stall the reply.
|
||||
|
||||
Lives in ``desktop_ui`` (GUI sessions only) and withdraws itself when the user
|
||||
turns tips off, so the model is never offered a tool whose call would fail.
|
||||
"""
|
||||
"""Point at something in the Hermes desktop GUI and say one line about it — the quiet
|
||||
sibling of ``tour`` (same ``data-tour`` handles) with no scrim/spotlight/paging.
|
||||
Fire-and-forget: a tip is not a question, so blocking on a round-trip would stall the
|
||||
reply. Lives in ``desktop_ui`` and withdraws itself when the user turns tips off."""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@@ -18,18 +18,14 @@ _VALID_MODAL_MODES = {"auto", "direct", "managed"}
|
||||
|
||||
|
||||
def managed_nous_tools_enabled(*, force_fresh: bool = False) -> bool:
|
||||
"""True when the user is entitled to the Nous Tool Gateway (coarse gate:
|
||||
paid Portal service access OR a live free tool pool). Fails closed on
|
||||
unknown/error entitlement — never blocks startup. Per-category coverage is
|
||||
narrowed by callers via ``tool_gateway_entitled_for``. ``force_fresh=True``
|
||||
is for interactive flows that must see a just-purchased grant."""
|
||||
"""Coarse gate: entitled to the Nous Tool Gateway (paid Portal access OR a live free
|
||||
pool). Fails closed on unknown/error — never blocks startup. Callers narrow per category
|
||||
via ``tool_gateway_entitled_for``; ``force_fresh`` is for flows needing a just-bought grant."""
|
||||
try:
|
||||
from hermes_cli.nous_account import get_nous_portal_account_info
|
||||
|
||||
if force_fresh:
|
||||
account_info = get_nous_portal_account_info(force_fresh=True)
|
||||
else:
|
||||
account_info = get_nous_portal_account_info()
|
||||
account_info = (get_nous_portal_account_info(force_fresh=True) if force_fresh
|
||||
else get_nous_portal_account_info())
|
||||
return bool(account_info.logged_in) and account_info.tool_gateway_entitled
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"""Configurable tool-output truncation limits (``tool_output`` in config.yaml).
|
||||
|
||||
Centralises the caps once hardcoded in ``terminal_tool`` (``max_bytes``) and
|
||||
``file_operations`` (``max_lines`` / ``max_line_length``). Defaults equal the
|
||||
old constants and the reader never raises, so behaviour is unchanged when the
|
||||
section is absent or malformed::
|
||||
|
||||
tool_output:
|
||||
max_bytes: 100000 # terminal output cap (chars)
|
||||
max_lines: 5000 # read_file pagination + truncation cap
|
||||
max_line_length: 2000 # per-line cap before '... [truncated]'
|
||||
"""
|
||||
"""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
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"""Tool result persistence -- preserves large outputs instead of truncating.
|
||||
|
||||
Three layers against context-window overflow: (1) per-tool output caps inside
|
||||
each tool; (2) ``maybe_persist_tool_result`` — output over the tool's threshold
|
||||
is persisted and replaced in-context by a preview + path. The canonical home is
|
||||
ALWAYS host-side ``$HERMES_HOME/cache/spillover/{tool_use_id}.txt`` (works for
|
||||
MCP-only/cron/gateway sessions that never ran a terminal); remote backends see
|
||||
the translated in-sandbox path (``cache/spillover`` is in the auto-mounted cache
|
||||
list), probed for readability, else a copy written into the sandbox temp dir.
|
||||
(3) ``enforce_turn_budget`` — spills the largest results of a turn until the
|
||||
aggregate fits.
|
||||
"""
|
||||
Layers against context overflow: (1) per-tool caps inside each tool; (2)
|
||||
``maybe_persist_tool_result`` — output over the tool's threshold is persisted and
|
||||
replaced by a preview + path. Canonical home is ALWAYS host-side
|
||||
``$HERMES_HOME/cache/spillover/{id}.txt`` (works for sessions that never ran a
|
||||
terminal); remote backends get the translated in-sandbox path (probed for
|
||||
readability) else a copy in the sandbox temp dir. (3) ``enforce_turn_budget``."""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"""Progressive tool disclosure ("tool search"): MCP/plugin tools and a curated set
|
||||
of event-triggered core tools are replaced in the model-visible array by three
|
||||
bridge tools — tool_search / tool_describe / tool_call.
|
||||
|
||||
Invariants: working-set core tools (``toolsets._HERMES_CORE_TOOLS``) and
|
||||
session-gated GUI toolsets never defer unless named in ``defer``; ANY deferrable
|
||||
tool activates the bridge (the listing scales with budget, not activation); the
|
||||
catalog is stateless — rebuilt from the live tool-defs on every assembly (a
|
||||
session-keyed catalog drifts from the registry and silently drops tools); bridge
|
||||
calls route through ``model_tools.handle_function_call`` so guardrails, hooks,
|
||||
approvals and truncation fire identically.
|
||||
"""
|
||||
bridge tools — tool_search / tool_describe / tool_call. Invariants: core tools
|
||||
(``toolsets._HERMES_CORE_TOOLS``) and session-gated GUI toolsets never defer unless
|
||||
named in ``defer``; ANY deferrable tool activates the bridge (the listing scales
|
||||
with budget, not activation); the catalog is stateless — rebuilt from the live
|
||||
tool-defs every assembly (a session-keyed one drifts and silently drops tools);
|
||||
bridge calls route through ``model_tools.handle_function_call`` (same guardrails)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -111,6 +107,7 @@ def _safe_float(value: Any, fallback: float) -> float:
|
||||
|
||||
|
||||
def _config_from_loader(loader_name: str) -> ToolSearchConfig:
|
||||
"""Tool-search config via ``hermes_cli.config.<loader_name>`` (defaults on any failure)."""
|
||||
try:
|
||||
import hermes_cli.config as _cfg_mod
|
||||
cfg = getattr(_cfg_mod, loader_name)() or {}
|
||||
@@ -122,12 +119,11 @@ def _config_from_loader(loader_name: str) -> ToolSearchConfig:
|
||||
|
||||
|
||||
def load_config() -> ToolSearchConfig:
|
||||
"""Load tool-search config from the user config file."""
|
||||
return _config_from_loader("load_config")
|
||||
|
||||
|
||||
def load_config_readonly() -> ToolSearchConfig:
|
||||
"""Load tool-search config without copying the cached full config."""
|
||||
"""Same as ``load_config`` without copying the cached full config."""
|
||||
return _config_from_loader("load_config_readonly")
|
||||
|
||||
|
||||
@@ -221,11 +217,9 @@ def should_activate(
|
||||
config: ToolSearchConfig,
|
||||
deferrable_tokens: int,
|
||||
context_length: Optional[int]) -> bool:
|
||||
"""``"off"`` never activates; ``"on"``/``"auto"`` activate whenever any
|
||||
deferrable tool exists. ``"auto"`` is an alias of ``"on"`` reserved for a
|
||||
future budget-gated mode — do not distinguish them without that design.
|
||||
``context_length`` stays in the signature for caller compatibility; the
|
||||
threshold governs the listing budget, not activation."""
|
||||
"""``"off"`` never activates; ``"on"``/``"auto"`` activate whenever any deferrable
|
||||
tool exists ("auto" is reserved for a future budget-gated mode — do not distinguish
|
||||
them without that design). ``context_length`` is kept for caller compatibility."""
|
||||
return config.enabled != "off" and deferrable_tokens > 0
|
||||
|
||||
|
||||
@@ -363,9 +357,8 @@ def assemble_tool_defs(
|
||||
*,
|
||||
context_length: Optional[int] = None,
|
||||
config: Optional[ToolSearchConfig] = None) -> AssemblyResult:
|
||||
"""Return the tool-defs list the model should actually see: passthrough
|
||||
when inactive, otherwise deferrable tools replaced by the three bridge
|
||||
tools. Idempotent — bridge tools already present are stripped first."""
|
||||
"""Tool-defs the model should see: passthrough when inactive, else deferrable tools
|
||||
replaced by the three bridge tools. Idempotent — existing bridge tools are stripped first."""
|
||||
if config is None:
|
||||
config = load_config()
|
||||
incoming = [td for td, name in zip(tool_defs, _tool_def_names(tool_defs))
|
||||
@@ -404,9 +397,8 @@ def is_bridge_tool(name: str) -> bool:
|
||||
|
||||
|
||||
def _shared_tool_record(entry: CatalogEntry) -> Dict[str, Any]:
|
||||
"""One record for the response's shared ``tools`` map (held once per tool;
|
||||
per-query groups carry names only). ``required`` lets the model attempt a
|
||||
trivial call without a ``tool_describe`` round-trip."""
|
||||
"""One record for the shared ``tools`` map (per-query groups carry names only);
|
||||
``required`` lets the model attempt a trivial call without a ``tool_describe`` round-trip."""
|
||||
schema = entry.schema if isinstance(entry.schema, dict) else {}
|
||||
fn = schema.get("function")
|
||||
params = fn.get("parameters") if isinstance(fn, dict) else None
|
||||
@@ -430,9 +422,8 @@ def _available_source_summary(catalog: List[CatalogEntry]) -> List[Dict[str, Any
|
||||
def _string_list_arg(
|
||||
args: Dict[str, Any], key: str, *, dedupe: bool, max_items: int, retry_hint: str,
|
||||
) -> Tuple[Optional[List[str]], Optional[str]]:
|
||||
"""Read a list-of-strings bridge argument -> ``(items, error_json)``. A bare
|
||||
string (a common model slip) counts as a one-item list. Rejects non-list
|
||||
input, an empty list (after stripping blanks), and more than ``max_items``."""
|
||||
"""Read a list-of-strings bridge argument -> ``(items, error_json)``. A bare string (a
|
||||
common model slip) is a one-item list; rejects non-lists, all-blank lists, > ``max_items``."""
|
||||
raw = args.get(key)
|
||||
if isinstance(raw, str):
|
||||
raw = [raw]
|
||||
@@ -552,10 +543,9 @@ def dispatch_tool_describe(args: Dict[str, Any],
|
||||
|
||||
|
||||
def scoped_deferrable_names(tool_defs: List[Dict[str, Any]]) -> frozenset[str]:
|
||||
"""Deferrable tool names in the *pre-assembly* ``tool_defs`` of the session's
|
||||
toolset scope — the universe ``tool_call`` may reach. Gates both bridge
|
||||
dispatch and the executor unwrap so a restricted session cannot invoke an
|
||||
out-of-scope tool via the bridge."""
|
||||
"""Deferrable names in the *pre-assembly* ``tool_defs`` of the session scope — the
|
||||
universe ``tool_call`` may reach. Gates bridge dispatch AND the executor unwrap so a
|
||||
restricted session cannot invoke an out-of-scope tool via the bridge."""
|
||||
defer_tools = load_config_readonly().effective_defer_tools
|
||||
return frozenset(
|
||||
name for name in _tool_def_names(tool_defs)
|
||||
@@ -563,9 +553,8 @@ def scoped_deferrable_names(tool_defs: List[Dict[str, Any]]) -> frozenset[str]:
|
||||
|
||||
|
||||
def resolve_underlying_call(args: Dict[str, Any]) -> Tuple[Optional[str], Dict[str, Any], Optional[str]]:
|
||||
"""Parse a ``tool_call`` invocation into (underlying_name, args, error_msg);
|
||||
``(None, {}, msg)`` on parse error. Shared by dispatch, display, and the
|
||||
trajectory recorder so all three agree on the underlying tool."""
|
||||
"""Parse a ``tool_call`` invocation -> (underlying_name, args, error_msg); ``(None, {}, msg)``
|
||||
on error. Shared by dispatch, display and the trajectory recorder so all three agree."""
|
||||
name = str(args.get("name") or "").strip()
|
||||
if not name:
|
||||
return None, {}, "tool_call requires a 'name' argument"
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guided tour (highlight + narrate UI elements) in the Hermes desktop GUI.
|
||||
|
||||
Generic, no baked-in tours: the agent discovers targets (``action="targets"``),
|
||||
then highlights by CSS selector one step at a time (``show``) or hands over a
|
||||
step list the user pages with Next/Prev (``start``). Round-trips through the
|
||||
gateway blocking-prompt bridge (``tour.request``/``tour.respond``) so the agent
|
||||
learns whether the selector matched. Lives in ``desktop_ui`` and withdraws itself
|
||||
when the user turns tours off: a tour takes the whole screen, so "off" must mean
|
||||
the model is never told the tool exists rather than offered a call that fails.
|
||||
"""
|
||||
"""Guided tour (highlight + narrate UI elements) in the Hermes desktop GUI. Generic:
|
||||
the agent discovers targets (``action="targets"``), then highlights one step at a
|
||||
time (``show``) or hands over a step list the user pages (``start``). Round-trips
|
||||
through the gateway blocking-prompt bridge (``tour.request``/``tour.respond``) so the
|
||||
agent learns whether the selector matched. Lives in ``desktop_ui`` and withdraws
|
||||
itself when tours are off: a tour takes the whole screen, so "off" must mean the
|
||||
model is never told the tool exists rather than offered a call that fails."""
|
||||
|
||||
import json
|
||||
from typing import Callable, Optional
|
||||
|
||||
Reference in New Issue
Block a user