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:
Teknium 2026-07-05 13:51:26 -07:00 committed by GitHub
parent 1b69ad0b8b
commit eab208db70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 488 additions and 2 deletions

View file

@ -630,6 +630,20 @@ return None
Any non-None, non-empty return with a `"context"` key (or a plain non-empty string) is collected and appended to the user message for the current turn.
#### Oversized-context spill
Per-hook context is capped at `10,000` characters by default. Anything above the cap is written to `$HERMES_HOME/hook_outputs/<session_id>/<uuid>.txt` and replaced with a head/tail preview plus the saved path. The model can read the full content via `read_file` or `terminal` if it genuinely needs it. This keeps a runaway plugin from inflating every subsequent turn's prompt and blowing out the prompt cache prefix. Tune in `config.yaml`:
```yaml
hooks:
output_spill:
enabled: true # default: true
max_chars: 10000 # default; set higher to opt out of spilling
preview_head: 500 # chars shown at the top of the preview
preview_tail: 500 # chars shown at the bottom of the preview
# directory: null # default: $HERMES_HOME/hook_outputs
```
#### How injection works
Injected context is appended to the **user message**, not the system prompt. This is a deliberate design choice: