feat(tools): make tool-result persistence threshold configurable - #86171
Open
x1051445024 wants to merge 2 commits into
Open
feat(tools): make tool-result persistence threshold configurable#86171x1051445024 wants to merge 2 commits into
x1051445024 wants to merge 2 commits into
Conversation
Add tools.tool_result_persist_threshold_chars (default None = current behavior) so operators can persist oversized tool results earlier than the 100K-char default, reclaiming medium-sized results from re-sent history on long tool loops. The explicit value is normalized through a single source of truth (tools/budget_config.normalize_persist_threshold, strict whitelist: non-bool int and whole-number strings only) used by config parsing, the budget factory and the tool executor, and overrides only the per-result size: the per-turn budget and preview keep their context-window scaling, so small models keep their small-window turn-budget protection (NousResearch#23767). Booleans, floats, Decimal/Fraction, bytes and non-whole strings are rejected at every layer instead of silently clamping to 1. Per-tool registry caps and the read_file pin still apply through resolve_threshold.
Contributor
feat(tools): make tool-result persistence threshold configurable
|
Contributor
Author
|
Thanks for the review. Addressed all four suggestions in commit
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
tools/tool_result_storage.pypersists oversized tool results (file reads,searches, script output) to the sandbox and replaces them in-context with a
preview + path. The per-result persistence threshold is hardcoded at
100K chars for large-context models (
tools/budget_config.py), scaled downonly for small models.
In long tool loops (agentic group chats, deep investigation turns) this is
wasteful: medium-sized results (30–50K chars of search/script output) stay
fully in history and are re-sent on every subsequent API call. Measured on
real logs (one room, one day, see A/B below): per-call input grew monotonically
from ~31K to ~193K as results accumulated — with no compression firing because
usage never approached the 50% threshold of a 1M-token model.
Proposed change (minimal, default-preserving)
New config key
tools.tool_result_persist_threshold_chars:None(default) — today's behavior, byte-identical (context-scaled budget).int > 0— explicit per-result cap in chars. Smaller values (e.g. 20000)persist medium results early: preview + path in context, full original on
disk for audit/replay. Values are still bounded by each tool's registry cap
and
read_filestays pinned (no persist→read→persist loops).aggregate budget and the preview size still come from the model's context
window scaling, so a small-window model keeps its small turn budget (it must
not silently reset back to the 200K default — regression [Bug]: Hermes sends oversized prompts after switching to lower-context local model; token estimation undercounts and compression can increase prompt size #23767).
tools/budget_config.normalize_persist_threshold, used by all three layers(config parsing, factory, executor) so accept/reject rules cannot drift.
Strict type whitelist — only non-bool
intand whole-number strings areaccepted:
None= unset; booleans rejected (bool is an int subclass —int(True) == 1would persist almost every result, even when setprogrammatically by plugins/tests, not just via YAML);
int(1.5) == 1,int(Decimal("1.5")) == 1,int(Fraction(3, 2)) == 1truncation traps);bytesand arbitrary objects rejected — no coercion;"20.5","1e4") rejected up front;1.Files (8 files total; diff vs baseline
fe1b5d8: +477/-5)hermes_cli/config_defaults.pyNone) + docs for the new key undertools:tools/budget_config.pynormalize_persist_threshold()(single source of truth, strict whitelist) +budget_with_persist_threshold(threshold, context_length=None)— explicit value overrides onlydefault_result_size; turn budget/preview keep context-scaled valuesagent/tool_executor.py_budget_for_agent()normalizes the explicit value through the same function; booleans/non-integer numerics/garbage set programmatically fall back to context scaling instead of crashing or persisting everythingagent/agent_init.pyagent._tool_result_persist_threshold_charsvia the shared normalizer; warning + fallback on rejectiontests/tools/test_budget_config.pytests/tools/test_tool_result_storage.pytests/agent/test_tool_budget_explicit_threshold.pywebsite/docs/user-guide/configuration.mdRelationship to #85479 (draft, tool-result pruner in compression)
Orthogonal by design:
marker + tail 1024, 8192 threshold) — rewrites already-sent history, prompt
cache prefix broken on each commit.
append-only, never rewrites history, cache-prefix safe. A tool result is
persisted once at production time; nothing later modifies it.
agent/agent_init.py+hermes_cli/config_defaults.pyin different regions (compression vstools sections); no merge conflict. If both land, users can combine them
(early persist + late prune).
Expected benefit (offline A/B, real room logs)
This is a logical-input upper-bound estimate, NOT a billing-saving promise.
Actual billed savings depend on the provider's prompt-cache pricing; with a
94% cached-input ratio the billed saving is far smaller than the input
reduction.
inclusive); both bounds are fixed:
frozen_since_ms=1786636800000,frozen_until_ms=1786701813000. Log calls and DB tool results are filteredby the same
since_ms <= ts <= until_msbounds — no hardcoded date strings.Re-runs produce identical numbers even though the data sources keep growing
(verified: two consecutive runs diff only in the informational
extracted_atline). The lower-bound fix changed nothing for this dataset(the room was created on 2026-08-14; 0 log rows and 0 DB rows exist before
the start of window — verified separately), so all numbers below are
unchanged by the round-5 fix.
(4,378,343 chars); cached input 34,820,480 = 94.1% of total input
(token ratio, not a per-call ratio).
run_id→ thread mapping):run_id-grouped unique-window heuristic: resultsgrouped by
gc_messages.run_id; a run whose tool-message timestamp medianfalls inside exactly one turn-segment's call interval is assigned to that
segment.
run_idNULL)..venv/Scripts/python.exe ../hermes-agent-evidence/ab_simulate.py \ --since "2026-08-14 00:00:00" --until "2026-08-14 18:03:33"(
hermes-agent-evidence/ab_simulate.py), not inside the repo; itimports
tools.budget_configfrom the sibling checkout (default../hermes-agent-main, overridable with--repo).Per-tool thresholds are resolved from the production budget objects
(
DEFAULT_BUDGET.resolve_threshold(tool)/budget_with_persist_threshold(T).resolve_threshold(tool)),so
PINNED_THRESHOLDSand registry per-tool caps are applied exactly as atruntime. In the frozen data, all 15 tool names resolve to
base=100,000(no tool registers a smaller
max_result_size_chars; the registry cap branchis therefore a no-op for this dataset), and
read_fileresolves to∞(pinned — never counted as newly persisted).
Method (incremental-cut simulation, conservative):
counter reset to Terminal tool #1; each segment's carried set starts empty (no cross-turn
leakage). Per call, saving = Σ (len − 1500 preview) chars over results newly
persisted at the candidate threshold but not at baseline
(
candidate_threshold < len ≤ baseline_threshold), chars→tokens at chars/4.First carry would be uncached (full price); re-carry on later calls of the
same turn would be cache-read (discounted price).
resolve_threshold(see above) — not hardcoded; the script prints the resolved per-tool
thresholds as evidence.
depend on the model re-reading persisted files.
Tests (revision 8 — accurate scope; NOT a full-suite run)
Three runs on the feature branch
881c79a(Python 3.12 venv, pytest 9.1.1,ruff 0.16.3; workdir = repo root, all commands with
-p no:cacheprovider):9cc77f4c) was byte-identical after the run (ISO == BACKUP: True) — thesuite cannot touch a production config; conftest also rewires any
production-pointing
HERMES_HOMEto a per-session tempdir.ruff checkclean (All checks passed!) on all 7 touched Python files;the eighth touched file is Markdown documentation.
~32,000 tests (incl. web/browser/gateway suites that hang or are
platform-specific on Windows;
tests/hermes_cli/test_doctor_journal_modes.pyfails collection on Windows via
os.geteuid). Full-suite CI is onlymeaningful on the project's Linux runners. Precise terminal commands for
runs 1 and 2, the session-log-recovered core script for run 3, and the
result lines from the session log are archived in the local evidence
directory (
token-save-audit/test-commands-round8.md, not included in thePR; note these are session-log records, not raw stdout files);
programmatic-coercion verification archived as
hermes-agent-evidence/bool_verification.txt.Compatibility & rollback
None→ zero behavior change on upgrade.None).read_filepin keep the feature from bypassing per-toolsafety limits.
(explicit value never touches
turn_budget).factory, executor) through one normalizer, so a malformed value cannot
silently persist everything.
Open questions / follow-ups
fixed ratio (e.g. ×10)? Currently the turn budget stays context-scaled,
independent of the explicit value.
completion quality (evidence preserved on disk).
(same regions, no conflict).