
    Pmje                        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dS ))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  Tvaluer   defaultintreturnc                j    	 t          |           }n# t          t          f$ r |cY S w xY w|dk    r|S |S )Nr   r
   	TypeError
ValueErrorr   r	   ivs      =/home/thesage/.hermes/hermes-agent/tools/hook_output_spill.py_coerce_positive_intr   <   sP    ZZz"   	QwwI    ((c                j    	 t          |           }n# t          t          f$ r |cY S w xY w|dk     r|S |S )z@Like ``_coerce_positive_int`` but allows zero (e.g. empty tail).r   r   r   s      r   _coerce_non_negative_intr   F   sP    ZZz"   	AvvIr   Dict[str, Any]c                    i } 	 ddl m}  |            pi }t          |t                    r|                    d          nd}t          |t                    r,|                    d          }t          |t                    r|} n# t
          $ r i } Y nw xY w|                     dt                    }|t          |          nt          }|                     d          }|t          |t                    sd}|t          |                     d          t                    t          |                     d	          t                    t          |                     d
          t                    |dS )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_configr0   Q   s|    G	111111kmm!r$.sD$9$9C   teT"" 	))N++C#t$$     ++i99K#.#:d;GK((IZ	3%?%?	 )'++k*B*BDUVV0KK'')=
 
 1KK'')=
 
 
 
 
s   A?B BBdirectory_overrideOptional[str]
session_idr   c                .   | r-t          t          j                            |                     }n ddlm} t           |                      dz  }|pd}|                    dd                              dd                              dd          }||z  S )	z=Return the directory where spill files for this session live.r   )get_hermes_homehook_outputsz
no-session/_\z..)r   ospath
expanduserhermes_constantsr5   replace)r1   r3   baser5   session_segments        r   _resolve_spill_dirrA   s   s     8BG&&'9::;;444444OO%%&&7 !0LO &--c377??cJJRRSWY\]]O/!!    textr(   headtail
saved_pathsourcec               |   t          |           }|dk    r
| d|         nd}|dk    r||k    r| | d         nd}d| d|dd|rd| d	nd
z   g}|r*|                    d           |                    |           |r*|                    d           |                    |           d                    |          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)	rC   rD   rE   rF   rG   total
head_chunk
tail_chunkpartss	            r   _build_previewrU      s     IIE $qetebJ!%edllteffJ 	IFHH%HHHH(2]$z$$$$8]	_E  !^$$$Z    !^$$$Z   99UrB   hook)r3   rG   configrW   Optional[Dict[str, Any]]c                  | dS t          | t                    s"	 t          |           } n# t          $ r Y dS w xY w||nt                      }|                    dd          s| S t          |                    d          pt                    }t          |           |k    r| S t          |                    d          pd          }t          |                    d          pd          }|                    d	          }d}		 t          ||          }
|
	                    dd
           t          j                    j         d}|
|z  }|                    |                     d          r| n| dz   d           t          |          }	n4# t          $ r'}t                              d|           d}	Y d}~nd}~ww xY wt#          | |||	|          S )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``.
    NrI   r   Tr   r   r   r    r   )parentsexist_okz.txtrM   zutf-8)encodingzhook output spill failed: %s)rG   )r"   r(   r%   r0   r$   r
   r)   rN   rA   mkdiruuiduuid4hex
write_textendswithloggerwarningrU   )rC   r3   rG   rW   r-   r   rD   rE   r1   rF   	spill_dirfilename
spill_pathexcs                 r   spill_if_oversizedri      s   8 |rdC   	t99DD 	 	 	22	 &&&,<,>,>C779d## CGGK((=,=>>I
4yyIsww~&&+!,,Dsww~&&+!,,D-- !%J&'9:FF	t444jll&,,,)
 	dmmD&9&9Jddtd{U\]]]__

   5s;;;





 $dJvFFFFs%   + 
99BF 
F=F88F=)r)   r*   r+   r&   r0   ri   )r   r   r	   r
   r   r
   )r   r   )r1   r2   r3   r2   r   r   )rC   r(   rD   r
   rE   r
   rF   r2   rG   r(   r   r(   )
rC   r(   r3   r2   rG   r(   rW   rX   r   r(   )__doc__
__future__r   loggingr:   r^   pathlibr   typingr   r   r   	getLogger__name__rc   r)   r*   r+   r&   r   r   r0   rA   rU   ri   __all__ rB   r   <module>rs      sJ  ( (T # " " " " "  				        & & & & & & & & & &		8	$	$             D" " " ""   : !%'+@G @G @G @G @G @GF  rB   