
    (Gjz$                        d Z ddlZddlZddlZddlZddlZddlZddlmZm	Z	m
Z
  ej        e          ZdZdZdZdZdZ ej        d	          Zd
ZdefdZdedefdZefdededeeef         fdZdedefdZdededefdZdededededef
dZde
dfdededede	dee z  dz  defdZ!de
fde"e#         de	de"e#         fdZ$dS ) a  Tool result persistence -- preserves large outputs instead of truncating.

Defense against context-window overflow operates at three levels:

1. **Per-tool output cap** (inside each tool): Tools like search_files
   pre-truncate their own output before returning. This is the first line
   of defense and the only one the tool author controls.

2. **Per-result persistence** (maybe_persist_tool_result): After a tool
   returns, if its output exceeds the tool's registered threshold
   (registry.get_max_result_size), the full output is written INTO THE
   SANDBOX temp dir (for example /tmp/hermes-results/{tool_use_id}.txt on
   standard Linux, or $TMPDIR/hermes-results/{tool_use_id}.txt on Termux)
   via env.execute(). The in-context content is replaced with a preview +
   file path reference. The model can read_file to access the full output
   on any backend.

3. **Per-turn aggregate budget** (enforce_turn_budget): After all tool
   results in a single assistant turn are collected, if the total exceeds
   MAX_TURN_BUDGET_CHARS (200K), the largest non-persisted results are
   spilled to disk until the aggregate is under budget. This catches cases
   where many medium-sized results combine to overflow context.
    N)DEFAULT_PREVIEW_SIZE_CHARSBudgetConfigDEFAULT_BUDGETz<persisted-output>z</persisted-output>z/tmp/hermes-resultsHERMES_PERSIST_EOF__budget_enforcement__z[^A-Za-z0-9_.-]+x   returnc                    | |t          | dd          }t          |          r\	  |            }|r|                    d          pd}| dS n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt          S )z=Return the best temp-backed storage dir for this environment.Nget_temp_dir/z/hermes-resultsz"Could not resolve env temp dir: %s)getattrcallablerstrip	ExceptionloggerdebugSTORAGE_DIR)envr   temp_direxcs       ?/home/rurouni/.hermes/hermes-agent/tools/tool_result_storage.py_resolve_storage_dirr   0   s    
sND99L!! 	88'<>>  8's33:sH&77778  H H HA3GGGGGGGGH s   
A 
A<A77A<tool_use_idc                    t          | pd          }t                              d|                              d          }||k    }|sd}d}|st	          |          t
          k    rlt          j        |                    d                    	                                dd         }|dt
                   
                    d          pd}| d| }| dS )	z3Return a single safe filename for a tool result id.tool_result_z._-Tzutf-8N   z.txt)str_UNSAFE_RESULT_FILENAME_CHARSsubstriplen_MAX_RESULT_FILENAME_STEMhashlibsha256encode	hexdigestr   )r   raw_id	safe_stemchangeddigests        r   _safe_result_filenamer,   @   s    -..F-11#v>>DDUKKI6!G !	 ,#i..#<<<g 6 677AACCCRCH8889@@GGX=	 ++6++	    content	max_charsc                     t          |           |k    r| dfS | d|         }|                    d          }||dz  k    r|d|dz            }|dfS )zGTruncate at last newline within max_chars. Returns (preview, has_more).FN
      T)r"   rfind)r.   r/   	truncatedlast_nls       r   generate_previewr7   R   sg    
7||y  ~

#Iood##Galw{l+	d?r-   c                 h    t           | vrt           S dt          j                    j        dd          S )z=Return a heredoc delimiter that doesn't collide with content.HERMES_PERSIST_N   )HEREDOC_MARKERuuiduuid4hex)r.   s    r   _heredoc_markerr?   ]   s3    W$$3TZ\\-bqb1333r-   remote_pathc                     t           j                            |          }dt          j        |           dt          j        |           }|                    |d|           }|                    dd          dk    S )u  Write content into the sandbox via env.execute(). Returns True on success.

    Pushes ``content`` through stdin rather than embedding it in the command
    string. Linux's ``MAX_ARG_STRLEN`` caps any single argv element at 128 KB
    (32 * PAGE_SIZE), so the previous heredoc-in-the-command-string approach
    silently failed with ``OSError: [Errno 7] Argument list too long`` for any
    tool result over ~128 KB — exactly the case persistence exists to handle.
    Routing through stdin removes that ceiling on local + ssh (``_stdin_mode
    == "pipe"``); remote backends with ``_stdin_mode == "heredoc"`` keep their
    existing API-body sized limit, which is orders of magnitude larger than
    the exec-arg ceiling.
    z	mkdir -p z
 && cat >    )timeout
stdin_data
returncoder3   r   )ospathdirnameshlexquoteexecuteget)r.   r@   r   storage_dircmdresults         r   _write_to_sandboxrP   d   sp     '//+..K
Tek+..
T
T%+k:R:R
T
TC[[bW[==F::lA&&!++r-   previewhas_moreoriginal_size	file_pathc                     |dz  }|dk    r
|dz  dd}n|dd}t            d}|d|dd| d	z  }|d
| dz  }|dz  }|dt          |            dz  }|| z  }|r|dz  }|dt           z  }|S )z/Build the <persisted-output> replacement block.i   z.1fz MBz KBr1   z This tool result was too large (,z characters, z).
zFull output saved to: zZUse the read_file tool with offset and limit to access specific sections of this output.

zPreview (first z	 chars):
z
...)PERSISTED_OUTPUT_TAGr"   PERSISTED_OUTPUT_CLOSING_TAG)rQ   rR   rS   rT   size_kbsize_strmsgs          r   _build_persisted_messager\   w   s     d"G$n----&&&&!
%
%
%CZmZZZHZZZZC1I1111CiiC5S\\5555C7NC w.,...CJr-   	tool_nameconfig	thresholdc                    ||n|                     |          }|t          d          k    r| S t          |           |k    r| S t          |          }| dt	          |           }t          | |j                  \  }	}
|	 t          | ||          rJt          	                    d||t          |           |           t          |	|
t          |           |          S n3# t          $ r&}t                              d||           Y d}~nd}~ww xY wt          	                    d|t          |                      |	 dt          |           d	d
S )a  Layer 2: persist oversized result into the sandbox, return preview + path.

    Writes via env.execute() so the file is accessible from any backend
    (local, Docker, SSH, Modal, Daytona). Falls back to inline truncation
    if write fails or no env is available.

    Args:
        content: Raw tool result string.
        tool_name: Name of the tool (used for threshold lookup).
        tool_use_id: Unique ID for this tool call (used as filename).
        env: The active BaseEnvironment instance, or None.
        config: BudgetConfig controlling thresholds and preview size.
        threshold: Explicit override; takes precedence over config resolution.

    Returns:
        Original content if small, or <persisted-output> replacement.
    Ninfr   )r/   z4Persisted large tool result: %s (%s, %d chars -> %s)zSandbox write failed for %s: %szDInline-truncating large tool result: %s (%d chars, no sandbox write)z 

[Truncated: tool response was rV   z3 chars. Full output could not be saved to sandbox.])resolve_thresholdfloatr"   r   r,   r7   preview_sizerP   r   infor\   r   warning)r.   r]   r   r   r^   r_   effective_thresholdrM   r@   rQ   rR   r   s               r   maybe_persist_tool_resultrh      s   2 (1'<))&BZBZ[dBeBeeEll**
7||***&s++K GG#8#E#EGGK(F<OPPPGX
	P +s;; ^J{CLL+   03w<<Q\]]]^  	P 	P 	PNN<k3OOOOOOOO	P KKN3w<<  
  	7 	7),WA	7 	7 	7s   AC 
D)D

Dtool_messagesc           	      N   g }d}t          |           D ]O\  }}|                    dd          }t          |          }||z  }t          |vr|                    ||f           P||j        k    r| S |                    d d           |D ]\  }	}||j        k    r n| |	         }|d         }|                    dd|	           }
t          |t          |
||d	          }||k    r>||z  }|t          |          z  }|| |	         d<   t          
                    d
|
|           | S )a"  Layer 3: enforce aggregate budget across all tool results in a turn.

    If total chars exceed budget, persist the largest non-persisted results
    first (via sandbox write) until under budget. Already-persisted results
    are skipped.

    Mutates the list in-place and returns it.
    r   r.    c                     | d         S )Nr3    )xs    r   <lambda>z%enforce_turn_budget.<locals>.<lambda>   s
    !A$ r-   T)keyreversetool_call_idbudget_)r.   r]   r   r   r^   r_   z7Budget enforcement: persisted tool result %s (%d chars))	enumeraterL   r"   rW   appendturn_budgetsortrh   _BUDGET_TOOL_NAMEr   re   )ri   r   r^   
candidates
total_sizeir[   r.   sizeidxr   replacements               r   enforce_turn_budgetr      sx    JJM** ) )3'')R((7||d
w..q$i(((V'''OOO555  	T+++EC i.ggnooo>>/'#
 
 
 '!!$J#k***J,7M#y)KKIT  
 r-   )%__doc__r$   loggingrF   rerI   r<   tools.budget_configr   r   r   	getLogger__name__r   rW   rX   r   r;   rx   compiler   r#   r   r   r,   inttupleboolr7   r?   rP   r\   rc   rh   listdictr   rm   r-   r   <module>r      s   0   				 				            
	8	$	$+ 4 #%,  *
+> ? ?       s s    $ 5O  c c SXY\^bYbSc    4S 4S 4 4 4 4,s , ,d , , , ,&  	
 	   : 	)$(8 888 8
 8 U{T!8 	8 8 8 8z 	)3 3:3 3 
$Z	3 3 3 3 3 3r-   