
    `gj^                        d Z ddlmZ ddlZddlZddlZddlmZ ddlm	Z	m
Z
mZ  ej                  e      ZdZdZdZdZdd	Zdd
ZddZddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZdddd	 	 	 	 	 	 	 	 	 ddZg dZy)ui  Spill oversized hook-injected context to disk with a preview placeholder.

Ported from openai/codex PR #21069 (``Spill large hook outputs from context``).

Background
----------
Both shell hooks (``agent/shell_hooks.py``) and Python plugins
(``pre_llm_call`` hook in ``run_agent.py``) can return ``{"context": "..."}``
which gets concatenated into the current turn's user message on EVERY
subsequent API call. If a hook emits a large blob (e.g. a debug dump, a
full file, or a runaway prompt-engineering script), that blob inflates
every turn of the session and blows out the prompt cache prefix the
moment it's appended.

This mirrors what Codex does for its ``PreToolUse``/``Stop``/feedback
hooks: once the injected text exceeds a configured budget, write the
full content to a per-session directory on disk and replace the in-prompt
payload with a head/tail preview plus the saved path. The model can still
inspect the full content via ``read_file`` or ``terminal`` if it needs to.

Config (``config.yaml``)::

    hooks:
      output_spill:
        enabled: true          # default: true; set false to disable spilling
        max_chars: 10000       # default; context above this is spilled
        preview_head: 500      # chars shown at the start of the preview
        preview_tail: 500      # chars shown at the end of the preview
        directory: null        # default: <HERMES_HOME>/hook_outputs

Design invariants
-----------------
* Behaviour-preserving when ``enabled: false`` or when content is under
  the cap — return the input string unchanged.
* Never raises. Any I/O error (disk full, permission denied, missing
  HERMES_HOME, etc.) falls back to a byte-length truncation with an
  in-prompt notice — the hook context still reaches the model, just
  bounded in size.
* Spill files are grouped by session so a ``/new`` session doesn't grow
  them forever in one directory.
    )annotationsN)Path)AnyDictOptionali'  i  Tc                Z    	 t        |       }|dk  r|S |S # t        t        f$ r |cY S w xY w)Nr   int	TypeError
ValueErrorvaluedefaultivs      J/root/.hermes/venv/lib/python3.12/site-packages/tools/hook_output_spill.py_coerce_positive_intr   <   s>    Z 
QwI	 z"     **c                Z    	 t        |       }|dk  r|S |S # t        t        f$ r |cY S w xY w)z@Like ``_coerce_positive_int`` but allows zero (e.g. empty tail).r   r	   r   s      r   _coerce_non_negative_intr   F   s>    Z 
AvI	 z" r   c                 R   i } 	 ddl m}  |       xs i }t        |t              r|j	                  d      nd}t        |t              r#|j	                  d      }t        |t              r|} | j	                  dt              }|t        |      nt        }| j	                  d      }|t        |t              sd}|t        | j	                  d      t              t        | j	                  d	      t              t        | j	                  d
      t              |dS # t
        $ r i } Y w xY w)z7Return resolved hook output-spill config. Never raises.r   )load_confighooksNoutput_spillenabled	directory	max_charspreview_headpreview_tail)r   r   r   r   r   )hermes_cli.configr   
isinstancedictget	ExceptionDEFAULT_ENABLEDboolstrr   DEFAULT_MAX_CHARSr   DEFAULT_PREVIEW_HEADDEFAULT_PREVIEW_TAIL)sectionr   cfgr   subenabled_rawr   r   s           r   get_spill_configr.   Q   s    G	1m!r$.sD$9 teT"))N+C#t$ ++i9K#.#:d;GK(IZ	3%?	 )'++k*BDUV0KK')=
 1KK')=
 
 
  s   A'D D&%D&c                   | r)t        t        j                  j                  |             }n	 ddlm} t         |             dz  }|xs d}|j                  dd      j                  d	d      j                  d
d      }||z  S # t        $ rQ t        j                  j                  d      xs t        j                  j                  d      }t        |      dz  }Y w xY w)z=Return the directory where spill files for this session live.r   )get_hermes_homehook_outputsHERMES_HOMEz	~/.hermesz
no-session/_\z..)
r   ospath
expanduserhermes_constantsr0   r#   environr"   replace)directory_override
session_idbaser0   homesession_segments         r   _resolve_spill_dirrA   s   s    BGG&&'9:;	/8)*^;D !0LO &--c37??cJRRSWY\]O/!!  	/::>>-0SBGG4F4F{4SD:.D	/s   B ACCc               2   t        |       }|dkD  r| d| nd}|dkD  r||kD  r| | d nd}d| d|dd|rd| d	nd
z   g}|r"|j                  d       |j                  |       |r"|j                  d       |j                  |       dj                  |      S )zDAssemble the in-prompt preview with head/tail and saved-path footer.r   N [u    output truncated — ,z chars; full content z	saved to ]u#   unavailable — spill write failed]z--- head ---z--- tail ---
)lenappendjoin)	textheadtail
saved_pathsourcetotal
head_chunk
tail_chunkpartss	            r   _build_previewrT      s     IE $qetbJ!%edltefJ F8)%2GH(2Yzl!$8]	_E ^$Z ^$Z 99U    hook)r=   rO   configc                  | yt        | t              s	 t        |       } ||n	t               }|j	                  dd      s| S t        |j	                  d      xs t              }t        |       |k  r| S t        |j	                  d      xs d      }t        |j	                  d      xs d      }|j	                  d	      }d}		 t        ||      }
|
j                  dd
       t        j                         j                   d}|
|z  }|j                  | j                  d      r| n| dz   d       t        |      }	t#        | |||	|      S # t        $ r Y yw xY w# t        $ r"}t        j!                  d|       d}	Y d}~Ed}~ww xY w)a  Spill ``text`` to disk if it exceeds the configured cap.

    Returns either ``text`` unchanged (when under the cap, disabled, or
    empty) or a preview string with a filesystem path pointing at the
    full content.

    Parameters
    ----------
    text:
        The raw injected-context string from a hook. Non-string inputs
        are coerced with ``str()``.
    session_id:
        Used to group spill files by conversation. Falls back to
        ``"no-session"`` if missing.
    source:
        Human-readable label used in the preview header (``"hook"``,
        ``"plugin hook"``, ``"shell hook"``, etc.). Free-form.
    config:
        Optional override for tests; normally resolved from
        ``config.yaml``.
    NrC   r   Tr   r   r   r   r   )parentsexist_okz.txtrG   zutf-8)encodingzhook output spill failed: %s)rO   )r    r&   r#   r.   r"   r
   r'   rH   rA   mkdiruuiduuid4hex
write_textendswithloggerwarningrT   )rK   r=   rO   rW   r+   r   rL   rM   r<   rN   	spill_dirfilename
spill_pathexcs                 r   spill_if_oversizedrh      s|   8 |dC 	t9D &&,<,>C779d#CGGK(=,=>I
4yIsww~&+!,Dsww~&+!,D- !%J&'9:F	t4jjl&&'t,)
 	dmmD&9dtd{U\]_

 $dJvFF?  		6  5s;
s*   E A9E 	EE	F%FF)r'   r(   r)   r$   r.   rh   )r   r   r   r
   returnr
   )ri   zDict[str, Any])r<   Optional[str]r=   rj   ri   r   )rK   r&   rL   r
   rM   r
   rN   rj   rO   r&   ri   r&   )
rK   r&   r=   rj   rO   r&   rW   zOptional[Dict[str, Any]]ri   r&   )__doc__
__future__r   loggingr6   r]   pathlibr   typingr   r   r   	getLogger__name__rb   r'   r(   r)   r$   r   r   r.   rA   rT   rh   __all__ rU   r   <module>rt      s   (T #  	   & &			8	$    D"*

  	  	: !%'+@G
@G @G 	@G
 %@G 	@GFrU   