mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-10 13:31:38 +00:00
feat(hooks): spill oversized hook-injected context to disk (#20468)
Port from openai/codex#21069 ("Spill large hook outputs from context"). Both shell hooks and Python plugins can return {"context": "..."} from pre_llm_call, which gets appended to the current turn's user message on every subsequent API call. A plugin that emits a large blob inflates every turn and blows out the prompt cache prefix. - tools/hook_output_spill.py: shared helper that writes oversized context to $HERMES_HOME/hook_outputs/<session_id>/<uuid>.txt and returns a head/tail preview plus the saved path. Never raises. - agent/turn_context.py: apply the cap at the pre_llm_call aggregation site (moved here from run_agent.py since the original PR), covering both Python plugins and shell hooks. - agent/shell_hooks.py: reserve output_spill as a sub-key under hooks: so the config block doesn't emit unknown-hook-event warnings. - Docs: document the cap + config in build-a-hermes-plugin.md. Config (behaviour-preserving when absent): hooks.output_spill: enabled/max_chars/preview_head/preview_tail/directory Tests: 14 unit tests; shell_hooks (56) and plugins (100) suites green. E2E validated with isolated HERMES_HOME (spill, passthrough, traversal sanitisation, reserved-key skip).
This commit is contained in:
parent
1b69ad0b8b
commit
eab208db70
5 changed files with 488 additions and 2 deletions
|
|
@ -307,6 +307,11 @@ def _parse_hooks_block(hooks_cfg: Any) -> List[ShellHookSpec]:
|
|||
specs: List[ShellHookSpec] = []
|
||||
|
||||
for event_name, entries in hooks_cfg.items():
|
||||
# Reserved sub-keys that aren't event names — skip silently. These
|
||||
# are config sub-sections nested under `hooks:` for related
|
||||
# functionality (e.g. output-spill budgets).
|
||||
if event_name in ("output_spill",):
|
||||
continue
|
||||
if event_name not in VALID_HOOKS:
|
||||
suggestion = difflib.get_close_matches(
|
||||
str(event_name), VALID_HOOKS, n=1, cutoff=0.6,
|
||||
|
|
|
|||
|
|
@ -448,11 +448,37 @@ def build_turn_context(
|
|||
sender_id=getattr(agent, "_user_id", None) or "",
|
||||
)
|
||||
_ctx_parts: list[str] = []
|
||||
# Spill oversized per-hook context to disk so a runaway plugin
|
||||
# can't inflate every subsequent turn's prompt. Ported from
|
||||
# openai/codex PR #21069 ("Spill large hook outputs from context").
|
||||
try:
|
||||
from tools.hook_output_spill import (
|
||||
get_spill_config as _spill_cfg,
|
||||
spill_if_oversized as _spill_if_oversized,
|
||||
)
|
||||
_spill_config_cached = _spill_cfg()
|
||||
except Exception:
|
||||
_spill_if_oversized = None # type: ignore[assignment]
|
||||
_spill_config_cached = None
|
||||
for r in _pre_results:
|
||||
_piece: str = ""
|
||||
if isinstance(r, dict) and r.get("context"):
|
||||
_ctx_parts.append(str(r["context"]))
|
||||
_piece = str(r["context"])
|
||||
elif isinstance(r, str) and r.strip():
|
||||
_ctx_parts.append(r)
|
||||
_piece = r
|
||||
else:
|
||||
continue
|
||||
if _spill_if_oversized is not None:
|
||||
try:
|
||||
_piece = _spill_if_oversized(
|
||||
_piece,
|
||||
session_id=agent.session_id,
|
||||
source="plugin hook",
|
||||
config=_spill_config_cached,
|
||||
)
|
||||
except Exception as _spill_exc:
|
||||
logger.warning("hook context spill failed: %s", _spill_exc)
|
||||
_ctx_parts.append(_piece)
|
||||
if _ctx_parts:
|
||||
plugin_user_context = "\n\n".join(_ctx_parts)
|
||||
except Exception as exc:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue