
    ߩ^jF                       d Z ddlm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
 ddlmZmZmZmZ  ej        e          ZdZdZdZd	Zd
ZdZdZd,dZd-dZd.dZd/dZ G d d          Zd0dZ	 	 d1d2d"Z d3d#Z!d4d'Z"d5d)Z#efd6d+Z$dS )7u&  Live, tail-able transcripts for delegated subagents.

Every ``delegate_task`` dispatch creates one append-only, human-readable log
per child under::

    <hermes_home>/cache/delegation/live/<delegation_id>/task-<n>.log

The files are pre-created with a header at dispatch time (so ``tail -f``
attaches immediately) and then stream one line per child event: assistant
text, thinking, tool calls, tool results, and lifecycle markers. The paths
are returned from ``delegate_task`` so the parent agent (or the user) can
watch a child work instead of waiting blind for the consolidated summary.

Placement under ``cache/delegation`` is deliberate: that directory is
mounted read-only into remote terminal backends (Docker/Modal/SSH) via
``credential_files._CACHE_DIRS``, so the logs are readable from any backend.

Design constraints:

* **Never raise into the agent loop.** Every write is wrapped; the first
  failure disables the writer and degrades to a debug log.
* **Survive child crashes.** Files are opened in append mode per write —
  no long-lived handle to lose, every event is flushed when written.
* **Side-channel only.** Nothing here touches message content, so prompt
  caching is unaffected.
* **No config knobs.** Retention is a module constant (7 days), pruned
  opportunistically on each new dispatch.
    )annotationsN)Path)AnyDictListOptional   iX  i,     i    i  returnr   c                 ,    ddl m}   | dd          dz  S )zDRoot directory for live transcripts (profile-safe, never ~/.hermes).r   get_hermes_dirzcache/delegationdelegation_cachelive)hermes_constantsr   r   s    ;/home/ice/.hermes/hermes-agent/tools/delegation_live_log.pylive_transcript_rootr   =   s-    //////>,.@AAFJJ    strc                 H    dt          j                    j        dd          S )zHSame shape as async_delegation's ids so the dir name matches the handle.deleg_N   )uuiduuid4hex r   r   new_live_delegation_idr   D   s"    *DJLL$RaR(***r   textr   limitintc                    t          | pd          }d                    |                                          }t          |          |k    r#t          |          |z
  }|d|         d| dz   }|S )zACollapse to a single line and truncate with an elided-chars note.  Nu    …(+z chars))r   joinsplitlen)r   r    somitteds       r   	_one_liner*   I   sk    DJBAA
1vv~~a&&5.fufI11111Hr   c                X    | s| S 	 ddl m}  || d          pdS # t          $ r Y dS w xY w)u(  Mask credentials before anything reaches the transcript file.

    These logs live under ``cache/delegation``, which ``delegate_tool`` mounts
    READ-ONLY into remote terminal backends — so every line written here is
    readable from inside the sandbox. The events rendered here carry exactly
    the data that tends to hold secrets: tool args (a bearer header on a
    curl), tool results (a ``.env`` dump, a provider error echoing the key
    back) and streamed assistant text. Every other sink for that data already
    routes through this same redactor — search results via
    ``redact_sensitive_text``, terminal output via ``redact_terminal_output``
    — so a transcript that skipped it is the one place the operator's keys
    land in plaintext.

    ``force=True``: this is a safety boundary, so it must redact even when the
    global toggle is off. Withholds the line rather than emitting raw text if
    the redactor is somehow unavailable — losing a debug line costs less than
    writing a live credential into a sandbox-readable file.
    r   )redact_sensitive_textT)forcer#   z&[line withheld: redaction unavailable])agent.redactr,   	Exception)r   r,   s     r   _redactr0   S   sg    &  8666666$$T666<"< 8 8 87778s    
))c                  |    e Zd ZdZ	 d+d,dZd-dZd.dZd.dZd/d0dZ	 	 d1d2dZ	d.dZ
d3d Zd4d!Z	 	 d5d6d'Zd7d*ZdS )8LiveTranscriptWriterzAppend-only human-readable event log for ONE subagent task.

    All methods are best-effort: the first write failure flips ``_ok`` off
    and subsequent calls become no-ops (debug-logged). Never raises.
    Ndelegation_idr   
task_indexr!   goalcontextOptional[str]rootOptional[Path]c           	        || _         || _        d| _        t          j                    | _        g | _        d| _        	 ||nt                      }||z  }|	                    dd           |d| dz  | _
        dd| d| d	t          t          |t                               d
t          j        d           ddg}| j
                            d                    |          dz   d           |                     ddt          |t                    z   |rdt          |t                     ndz              d S # t&          $ r6}	t(                              d|||	           d| _        d | _
        Y d }	~	d S d }	~	ww xY w)NTr   )parentsexist_okztask-z.logz'=== Hermes subagent live transcript ===zdelegation: z	   task: zgoal: z	started: %Y-%m-%d %H:%M:%Su=   (append-only; streams while the subagent runs — tail -f me)z(========================================
utf-8encodinguserz	kickoff: z | context: r#   z,Live transcript init failed (%s task %s): %sF)r3   r4   _ok	threadingLock_lock_stream_buf_stream_lenr   mkdirpathr0   r*   _KICKOFF_MAXtimestrftime
write_textr%   eventr/   loggerdebug)
selfr3   r4   r5   r6   r8   basedheaderexcs
             r   __init__zLiveTranscriptWriter.__init__w   s   *$^%%
&(	 ,DD2F2H2HD}$AGGD4G000(),DJ,D,D,D(DDI9C}CCzCC B4!>!>??AA@DM*=>>@@O	F I  6!2!2T!9G LLLJJv{Yt\-J-JJOV^K7L)I)IKKK\^ ` a a a a a 	 	 	LLG&
C9 9 9DHDIIIIIII		s   C=D< <
E<+E77E<roler   r   Nonec                   | j         r| j        dS t          j        d           d|ddt	          |           d}	 | j        5  t          | j        dd	          5 }|                    |           ddd           n# 1 swxY w Y   ddd           dS # 1 swxY w Y   dS # t          $ r3}d
| _         t          
                    d| j        |           Y d}~dS d}~ww xY w)u>   Append one ``HH:MM:SS role ⟩ text`` line. Flushed per event.Nz%H:%M:%Sr$   z<9z| r>   ar?   r@   Fz%Live transcript write failed (%s): %s)rC   rJ   rL   rM   r0   rF   openwriter/   rP   rQ   )rR   rX   r   linefhrV   s         r   rO   zLiveTranscriptWriter.event   s   x 	49,F -
++JJdJJJJJJ	R # # $)S7;;; #rHHTNNN# # # # # # # # # # # # # # ## # # # # # # # # # # # # # # # # #
  	R 	R 	RDHLL@$)SQQQQQQQQQ	Rs_   B# BA>2B>B	BB	B	B# BB# BB# #
C -(CC c                d    t          |t                    }|r|                     d|           d S d S )N	assistant)r*   _ASSISTANT_MAXrO   rR   r   ts      r   assistant_textz#LiveTranscriptWriter.assistant_text   s=    dN++ 	'JJ{A&&&&&	' 	'r   c                d    t          |t                    }|r|                     d|           d S d S )Nthink)r*   _THINKING_MAXrO   rc   s      r   thinkingzLiveTranscriptWriter.thinking   s=    dM** 	#JJw"""""	# 	#r   nameargs_previewr   c                    |                                   t          |t                    }|                     dd|pd d| d           d S )Ntoolz-> ?())flush_streamr*   	_ARGS_MAXrO   )rR   rj   rk   argss       r   
tool_startzLiveTranscriptWriter.tool_start   sU    y11

6666t66677777r   Fresultdurationis_errorboolc                    |rdnd}d}	 |dt          |          dd}n# t          t          f$ r Y nw xY w|                     d|pd d| | d	t	          |t
                                d S )
NERRORokr#   r$   z.1fr(   ru   rn   z: )float	TypeError
ValueErrorrO   r*   _RESULT_MAX)rR   rj   ru   rv   rw   statusdurs          r   tool_resultz LiveTranscriptWriter.tool_result   s    $.$	#0%//0000:& 	 	 	D	

8 A Af Ac A A )&+ > >A A 	B 	B 	B 	B 	Bs   ! 55c                    |                                   |                     dt          |t                               dS )z=Lifecycle marker: start / final / error / interrupt / budget.finalN)rq   rO   r*   rb   rR   r   s     r   markerzLiveTranscriptWriter.marker   s8    

7IdN;;<<<<<r   deltac                    |r| j         sdS | j                            |           | xj        t	          |          z  c_        | j        t
          k    r|                                  dS dS )z:Buffer streamed assistant reply text; flushed as one line.N)rC   rG   appendrH   r'   _STREAM_BUFFER_FLUSH_CHARSrq   )rR   r   s     r   add_stream_deltaz%LiveTranscriptWriter.add_stream_delta   sx     	DH 	F&&&CJJ&999 :9r   c                    | j         sd S d                    | j                   }g | _         d| _        |                     |           d S )Nr#   r   )rG   r%   rH   re   r   s     r   rq   z!LiveTranscriptWriter.flush_stream   sP     	Fwwt'((D!!!!!r   
event_type	tool_namepreviewrs   kwargsc           
        t          |pd          }|dk    r+|                     t          |pd          |r|n|           dS |dk    rp|                     t          |pd          |                    d          |                    d          t	          |                    d                               dS |dk    r(|                     t          |p|pd                     dS |d	k    r&|                     t          |pd                     dS |d
k    r&|                     t          |pd                     dS |dk    r+|                     dt          |t                               dS |dk    r| 
                                 |                    dd          }|                    d          }d| g}	||	                    d| d           |                    d          p|}
|
r+|	                    dt          |
t                                |                     d                    |	                     dS dS )a  Map a child tool_progress_callback event onto transcript lines.

        Mirrors the shapes emitted by agent/tool_executor.py,
        agent/conversation_loop.py, and tools/delegate_tool._run_single_child.
        Unknown events are ignored. Never raises (event() swallows I/O).
        r#   ztool.startedztool.completedru   rv   rw   )ru   rv   rw   	_thinkingzreasoning.availablezsubagent.textzsubagent.startstartzsubagent.completer   rn   duration_secondszstatus=Nz	duration=r(   summaryz	summary: r$   )r   rt   r   getrx   ri   r   rO   r*   rK   rq   r   r   r   r%   )rR   r   r   r   rs   r   etr   r   partsr   s              r   observezLiveTranscriptWriter.observe   su    !r""OOC	R00W2N''$OOOOO###IO$$zz(++J//fjj4455	       ; MM#i878b99:::::(((MM#gm,,-----?""!!#gm"4"455555###JJw	'< @ @AAAAA&&&ZZ#..F**/00C'v''(E////000jj++6wG LJ7K)H)HJJKKKKK((((( '&r   entryDict[str, Any]c                   d|                     dd           g}|                     d          }|r|                    d|            |dk    r|                    d           |                     d          r1|                    d	t          |d         t                                |                     d
                    |                     dS )zTerminal marker from the aggregated result entry.

        Adds exit-reason detail the subagent.complete event doesn't carry
        (budget exhaustion via exit_reason=max_iterations, errors, etc.).
        zend status=r   rn   exit_reasonzexit_reason=max_iterationsz(iteration budget exhausted)errorzerror: r$   N)r   r   r*   r   r   r%   )rR   r   r   r   s       r   finalizezLiveTranscriptWriter.finalize  s     :uyy37799:ii.. 	7LL555666***LL788899W 	MLLK9U7^[#I#IKKLLLCHHUOO$$$$$r   NN)
r3   r   r4   r!   r5   r   r6   r7   r8   r9   )rX   r   r   r   r   rY   )r   r   r   rY   )N)rj   r   rk   r   r   rY   )NNF)
rj   r   ru   r   rv   r   rw   rx   r   rY   )r   r   r   rY   )r   rY   NNN)r   r   r   r   r   r   rs   r   r   r   r   rY   )r   r   r   rY   )__name__
__module____qualname____doc__rW   rO   re   ri   rt   r   r   r   rq   r   r   r   r   r   r2   r2   p   s0         HL    BR R R R&' ' ' '
# # # #
8 8 8 8 8
 48;@
B 
B 
B 
B 
B= = = =       " " " " 9=15') ') ') ') ')R% % % % % %r   r2   writerc                2     d fd	} fd}||_         |S )us  Wrap a child's tool_progress_callback so events also land in the log.

    ``inner_cb`` may be None (no parent display) — the wrapper still records.
    Writer failures never propagate; inner callback behavior is unchanged
    (its own exceptions are handled by callers exactly as before).
    Preserves the ``_flush`` attribute contract used by _run_single_child.
    Nc                    	  j         | |||fi | n2# t          $ r%}t                              d|           Y d }~nd }~ww xY w | |||fi | d S d S )Nz"Live transcript observe failed: %s)r   r/   rP   rQ   )r   r   r   rs   r   rV   inner_cbr   s         r   _cbz#wrap_progress_callback.<locals>._cb!  s    	DFN:y'4JJ6JJJJ 	D 	D 	DLL=sCCCCCCCC	DHZGTDDVDDDDD  s    
A?Ac                     	                                   n# t          $ r Y nw xY wt          dd           } t          |           r |              d S d S )N_flush)rq   r/   getattrcallable)inner_flushr   r   s    r   r   z&wrap_progress_callback.<locals>._flush)  sw    	!!!! 	 	 	D	h$77K   	KMMMMM	 	s    
%%r   )r   )r   r   r   r   s   ``  r   wrap_progress_callbackr     sZ    E E E E E E E      CJJr   	task_listList[Dict[str, Any]]r6   r7   r3   Etuple[Optional[str], List[Optional[LiveTranscriptWriter]], List[str]]c                   t          |           }	 t                       n# t          $ r Y nw xY w	 |pt                      }g }g }t	          |           D ]\  }}t          ||t          |                    dd                    |                    d          p|          }	|                    |	j	        |	nd           |	j	        '|                    t          |	j	                             |s	ddg|z  g fS t          || |           |||fS # t          $ r.}
t                              d|
           ddg|z  g fcY d}
~
S d}
~
ww xY w)a  Create one pre-headered writer per task + a manifest.json.

    Returns ``(delegation_id, writers, paths)``. On any top-level failure
    returns ``(None, [None]*n, [])`` so delegation proceeds untouched.
    Also opportunistically prunes stale live dirs (retention).
    r5   r#   r6   )r6   Nz#Live transcript creation failed: %s)r'   prune_stale_live_dirsr/   r   	enumerater2   r   r   r   rJ   _write_manifestrP   rQ   )r   r6   r3   ndeleg_idwriterspathsird   wrV   s              r   create_live_transcriptsr   8  s    	IA   $ <$:$<$<8:i(( 	* 	*DAq$!Svr!2!233i((3G  A NN 211===v!S[[))) 	($!R'')U333%'' $ $ $:C@@@dVaZ#######$s0     
--CD :D 
E#E=EEc                *    t                      | z  dz  S )Nzmanifest.json)r   )r3   s    r   _manifest_pathr   ]  s    !!M1OCCr   r   	List[str]rY   c                `   	 | t          j        d          t          |          fdt          |          D             d}t	          |                               t          j        |dd          d           d S # t          $ r&}t          
                    d	|           Y d }~d S d }~ww xY w)
Nr=   c                    g | ]]\  }}|t          t          |                    d d                    dd                   |t                    k     r|         nddd^S )r5   r#   Nr   running)indexr5   logr   )r0   r   r   r'   ).0r   rd   r   s      r   
<listcomp>z#_write_manifest.<locals>.<listcomp>h  s        Aq  $Cfb(9(9$:$:4C4$@AA'(3u::~~5884'
 
  r   )r3   started
task_counttasks   Findentensure_asciir?   r@   z)Live transcript manifest write failed: %s)rL   rM   r'   r   r   rN   jsondumpsr/   rP   rQ   )r3   r   r   manifestrV   s     `  r   r   r   a  s    G*}%899i..    &i00  	
 
& 	}%%00Jx>>> 	1 	
 	
 	
 	
 	
  G G G@#FFFFFFFFFGs   A8A= =
B-B((B-resultsc                   | sdS 	 t          |           }t          j        |                    d                    }d |D             }|                    dg           D ]x}|                    |                    d                    }|L|                    d|                    d                    |d<   |                    d          r|d         |d<   yt          j        d	          |d
<   |                    t          j        |dd          d           dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)zABest-effort per-task status update once the batch has aggregated.Nr?   r@   c                d    i | ]-}t          |t                    |                    d           |.S )r4   )
isinstancedictr   )r   rs     r   
<dictcomp>z,update_manifest_statuses.<locals>.<dictcomp>  s5    SSSqz!T?R?RSAEE,''SSSr   r   r   r   r   r=   	completedr   Fr   z*Live transcript manifest update failed: %s)r   r   loads	read_textr   rL   rM   rN   r   r/   rP   rQ   )r3   r   mpr   by_indextaskr   rV   s           r   update_manifest_statusesr   ~  st     HM**:bllGl<<==SSGSSSLL"-- 	; 	;DTXXg..//A}!"x(1C1C!D!DX55'' ;*+M*:D' $.A B B
dj!%HHH& 	 	( 	( 	( 	( 	( H H HA3GGGGGGGGGHs   DD 
E'EEmax_age_daysc                   d}	 t                      }|                                sdS t          j                    | dz  z
  }|                                D ]_}	 |                                r8|                                j        |k     rt          j        |d           |dz  }P# t          $ r Y \w xY wn2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|S )z}Remove live/<delegation_id> dirs older than the retention window.

    Returns how many were removed. Fully best-effort.
    r   iQ T)ignore_errors   z"Live transcript pruning failed: %sN)r   is_dirrL   iterdirstatst_mtimeshutilrmtreeOSErrorr/   rP   rQ   )r   removedr8   cutoffchildrV   s         r   r   r     s   
 G@#%%{{}} 	1|e33\\^^ 	 	E<<>> !ejjll&;f&D&DM%t<<<<qLG   	  @ @ @93????????@NsA   "B6 /B6 AB%$B6 %
B2/B6 1B22B6 6
C% C  C%)r   r   )r   r   )r   r   r    r!   r   r   )r   r   r   r   )r   r2   r   )r   r   r6   r7   r3   r7   r   r   )r3   r   r   r   )r3   r   r   r   r   r   r   rY   )r3   r7   r   r   r   rY   )r   r!   r   r!   )%r   
__future__r   r   loggingr   rD   rL   r   pathlibr   typingr   r   r   r   	getLoggerr   rP   LIVE_RETENTION_DAYSrb   rh   rr   r   rK   r   r   r   r*   r0   r2   r   r   r   r   r   r   r   r   r   <module>r      s   : # " " " " "                , , , , , , , , , , , ,		8	$	$  
 	
 " K K K K+ + + +
   8 8 8 8:e% e% e% e% e% e% e% e%P   D "#'"$ "$ "$ "$ "$JD D D DG G G G:H H H H, /B       r   