
    `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$y) 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                     | Bt        | dd      }t        |      r*	  |       }|r|j                  d      xs d}| dS t        S t        S # t        $ r%}t        j                  d|       Y d}~t        S d}~ww xY w)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       L/root/.hermes/venv/lib/python3.12/site-packages/tools/tool_result_storage.py_resolve_storage_dirr   0   s    
sND9L!8'> 's3:sH&Z77;  HA3GG
 Hs   A 	A9A44A9tool_use_idc                 b   t        | xs d      }t        j                  d|      j                  d      }||k7  }|sd}d}|st	        |      t
        kD  rXt        j                  |j                  d            j                         dd }|dt
         j                  d      xs 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KI6!G!	#i.#<<g 67AACCRH889@@GX=	 k6(+	[    content	max_charsc                 z    t        |       |k  r| dfS | d| }|j                  d      }||dz  kD  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   sX    
7|y ~
#Iood#Galw{+	d?r-   c                 f    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?   ]   s1    W$TZZ\--bq1233r-   remote_pathc                     t         j                  j                  |      }dt        j                  |       dt        j                  |       }|j                  |d|       }|j                  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   sf     ''//+.Kekk+./z%++k:R9S
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$nS)-c]#&!""
%C-mA->mH:UYZZC#I;b11CiiC_S\N*55C7NCwR,-..CJr-   	tool_nameconfig	thresholdc                 (   ||n|j                  |      }|t        d      k(  r| S t        |       |k  r| S t        |      }| dt	        |       }t        | |j                        \  }	}
|H	 t        | ||      r9t        j                  d||t        |       |       t        |	|
t        |       |      S 	 t        j                  d|t        |              |	 dt        |       d	d
S # t        $ r!}t        j                  d||       Y d}~Wd}~ww xY w)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      s1   2 (1'<)&BZBZ[dBeeEl*
7|**&s+K M#8#E"FGK(F<O<OPGX
	P +s;J{CL+ 03w<Q\]] < KKN3w<
 ) )),Wa(8 96	7  	PNN<k3OO	Ps   .AC' '	D0DDtool_messagesc           	      
   g }d}t        |       D ]C  \  }}|j                  dd      }t        |      }||z  }t        |vs1|j	                  ||f       E ||j
                  k  r| S |j                  d d       |D ]  \  }	}||j
                  k  r | S | |	   }|d   }|j                  dd|	       }
t        |t        |
||d	      }||k7  sR||z  }|t        |      z  }|| |	   d<   t        j                  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      sJ    JJM* )3'')R(7|d
w.q$i() V'''OOO5 	T+++, + C i.ggnuo>/'#
 '!$J#k**J,7M#y)KKIT'0 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,s , ,d ,&  	
 	: 	)$(888 8
 8 U{T!8 	8z 	)3:3 3 
$Z	3r-   