
    Jj^                        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/rurouni/.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  }n`# t          $ rS t          j                            d          pt          j                            d          }t          |          dz  }Y nw xY w|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_outputsHERMES_HOMEz	~/.hermesz
no-session/_\z..)
r   ospath
expanduserhermes_constantsr5   r%   environr$   replace)r1   r3   baser5   homesession_segments         r   _resolve_spill_dirrD   s   s    	/BG&&'9::;;	/888888))**^;DD 	/ 	/ 	/:>>-00SBG4F4F{4S4SD::.DDD	/ !0LO &--c377??cJJRRSWY\]]O/!!s    A AB/.B/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)	rE   rF   rG   rH   rI   total
head_chunk
tail_chunkpartss	            r   _build_previewrW      s     IIE $qetebJ!%edllteffJ 	IFHH%HHHH(2]$z$$$$8]	_E  !^$$$Z    !^$$$Z   99U    hook)r3   rI   configrZ   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``.
    NrK   r   Tr   r   r   r    r   )parentsexist_okz.txtrO   zutf-8)encodingzhook output spill failed: %s)rI   )r"   r(   r%   r0   r$   r
   r)   rP   rD   mkdiruuiduuid4hex
write_textendswithloggerwarningrW   )rE   r3   rI   rZ   r-   r   rF   rG   r1   rH   	spill_dirfilename
spill_pathexcs                 r   spill_if_oversizedrl      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   rl   )r   r   r	   r
   r   r
   )r   r   )r1   r2   r3   r2   r   r   )rE   r(   rF   r
   rG   r
   rH   r2   rI   r(   r   r(   )
rE   r(   r3   r2   rI   r(   rZ   r[   r   r(   )__doc__
__future__r   loggingr;   ra   pathlibr   typingr   r   r   	getLogger__name__rf   r)   r*   r+   r&   r   r   r0   rD   rW   rl   __all__ rX   r   <module>rv      sJ  ( (T # " " " " "  				        & & & & & & & & & &		8	$	$             D" " " "*   : !%'+@G @G @G @G @G @GF  rX   