
    (GjO                    |   U 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mZ ddl	m
Z
mZmZmZmZ ddlmZ ddlmZ  ej        e          ZeZdaded	<    ej                    Zdad
ed<    ej                    Zi Zded<   dZdZd:dZ d;dZ!d<dZ"d=dZ#dedd>d(Z$d?d,Z%d@d.Z&deddAd1Z'dBd3Z(dCd5Z)dDdEd8Z*d=d9Z+dS )Fu  
Async (background) delegation registry.

Backs ``delegate_task(background=true)``: the parent agent dispatches a
subagent that runs on a module-level daemon executor and returns a handle
immediately, so the user and the model can keep working while the child runs.

When the child finishes, a completion event is pushed onto the SHARED
``process_registry.completion_queue`` with ``type="async_delegation"``. The
CLI (``cli.py`` process_loop) and gateway (``_run_process_watcher`` /
``completion_queue`` drain) already poll that queue while the agent is idle
and forge a fresh user/internal turn from each event. We deliberately reuse
that rail rather than reaching into a running agent loop:

  - completions surface as a NEW turn when the agent is idle, never spliced
    between a tool result and an assistant message. That keeps strict
    message-role alternation legal and the prompt cache intact (hard
    invariant: never mutate past context).
  - we inherit the queue's de-dup, crash-recovery checkpoint, and the
    existing CLI + gateway drain wiring for free — no new drain loops in the
    two largest files in the repo.

The completion payload carries a RICH, self-contained task-source block (the
original goal, the context the parent supplied, toolsets, model, dispatch
time, status, and the full result summary). When the result re-enters the
conversation the parent may be deep in unrelated context and won't remember
why the subagent existed; the block lets it either use the result or
re-dispatch if the world has moved on.

This module owns ONLY the async lifecycle. The actual child build + run is
delegated back to ``delegate_tool._run_single_child`` via an injected
runner, so all the credential leasing, heartbeat, timeout, and result-shaping
logic stays in one place.
    )annotationsN)ThreadPoolExecutor)AnyCallableDictListOptional)DaemonThreadPoolExecutor)propagate_context_to_threadzOptional[ThreadPoolExecutor]	_executorint_executor_max_workerszDict[str, Dict[str, Any]]_records   2   max_workersreturnr   c                    t           5  t          | t          k    rt          | d          a| at          cddd           S # 1 swxY w Y   dS )u  Lazily create (or grow) the shared daemon executor.

    We never shrink — ThreadPoolExecutor can't resize — but if the configured
    cap grows between calls we rebuild a larger pool. Existing in-flight
    futures keep running on the old pool until it's garbage collected.
    Nzasync-delegate)r   thread_name_prefix)_executor_lockr   r   _DaemonThreadPoolExecutor)r   s    </home/rurouni/.hermes/hermes-agent/tools/async_delegation.py_get_executorr   M   s     
  .C C C1'#3  I %0!                 s   ,AAAc                     t           5  t          d t                                          D                       cddd           S # 1 swxY w Y   dS )z.Number of async delegations currently running.c              3  L   K   | ]}|                     d           dk    dV   dS statusrunning   Nget.0rs     r   	<genexpr>zactive_count.<locals>.<genexpr>c   s6      PP155??i3O3O13O3O3O3OPP    N)_records_locksumr   values r&   r   active_countr+   `   s    	 Q QPPhoo//PPPPPQ Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Qs   0AA	A	strc                 H    dt          j                    j        d d          S )Ndeleg_   )uuiduuid4hexr*   r&   r   _new_delegation_idr3   f   s"    *DJLL$RaR(***r&   Nonec                 .   d t                                           D             } t          |           t          k    rdS |                     d            | dt          |           t          z
           D ] \  }}t                               |d           !dS )ziDrop the oldest completed records beyond the retention cap.

    Caller must hold ``_records_lock``.
    c                N    g | ]"\  }}|                     d           dk    ||f#S r   r   r    )r#   ridr$   s      r   
<listcomp>z+_prune_completed_locked.<locals>.<listcomp>o   s?       C55??i'' 
a'''r&   Nc                r    | d                              d          p| d                              d          pdS )Nr   completed_atdispatched_atr   r    )kvs    r   <lambda>z)_prune_completed_locked.<locals>.<lambda>w   s1    "Q%))N";";"^r!uyy?Y?Y"^]^ r&   )key)r   itemslen_MAX_RETAINED_COMPLETEDsortpop)	completedr8   _s      r   _prune_completed_lockedrG   j   s    
 nn&&  I
 9~~000NN^^N___Fc)nn/FFFG    QS$   r&   )interrupt_fnmax_async_childrengoalcontextOptional[str]toolsetsOptional[List[str]]rolemodelsession_keyrunnerCallable[[], Dict[str, Any]]rH   Optional[Callable[[], None]]rI   Dict[str, Any]c        	           t                      t          j                    | ||rt          |          nd|||dd|d}	t          5  t	          d t
                                          D                       }
|
|k    rdd| ddcddd           S |	t
          <   ddd           n# 1 swxY w Y   t          |          }dfd}	 |                    t          |                     nY# t          $ rL}t          5  t
                              d           ddd           n# 1 swxY w Y   dd| dcY d}~S d}~ww xY wt                              d|pd| pddd                    ddS )a  Spawn ``runner`` on the daemon executor and return a handle immediately.

    Parameters
    ----------
    goal, context, toolsets, role, model
        The dispatch-time task spec, captured verbatim for the rich
        completion block.
    session_key
        The gateway session_key (from ``tools.approval.get_current_session_key``)
        captured on the parent thread BEFORE dispatch, because the daemon
        worker thread won't carry the contextvar. Used to route the
        completion back to the originating session.
    runner
        Zero-arg callable that builds + runs the child and returns the same
        result dict ``_run_single_child`` produces. Runs on the worker thread.
    interrupt_fn
        Optional callable to signal the child to stop (used on shutdown /
        explicit cancel).
    max_async_children
        Concurrency cap. When at capacity the dispatch is REJECTED (the caller
        should fall back to sync or tell the user) rather than queued, so a
        runaway model can't pile up unbounded background work.

    Returns
    -------
    dict
        ``{"status": "dispatched", "delegation_id": ...}`` on success, or
        ``{"status": "rejected", "error": ...}`` when at capacity.
    Nr   )delegation_idrJ   rK   rM   rO   rP   rQ   r   r<   r;   rH   c              3  L   K   | ]}|                     d           dk    dV   dS r   r    r"   s     r   r%   z,dispatch_async_delegation.<locals>.<genexpr>   ?       
 
AEE(OOy,H,HA,H,H,H,H
 
r&   rejected#Async delegation capacity reached (z running). Wait for one to finish (its result will re-enter the chat), or run this task synchronously (background=false). Raise delegation.max_concurrent_children in config.yaml to allow more concurrent background subagents.r   errorr   r4   c            	        i } d}	              pi } |                      d          pd}nu# t          $ rh}t                              d           dd t	          |          j         d| dt          t          j                    z
  d          d} d}Y d }~nd }~ww xY wt          | |           d S # t          | |           w xY w)	Nr]   r   rE   zAsync delegation %s crashed: r      )r   summaryr]   	api_callsduration_seconds)	r!   	Exceptionlogger	exceptiontype__name__roundtime	_finalize)resultr   excrW   r<   rR   s      r   _workerz*dispatch_async_delegation.<locals>._worker   s    !#	5VXX^FZZ))8[FF 		 		 		:MJJJ! II.77#77$)$)++*Eq$I$I F FFFFFF		 mVV44444ImVV4444s,   #+ B3 
BABB3 BB3 3Cz%Failed to schedule async delegation: z3Dispatched async delegation %s (session_key=%s): %s<cli> P   
dispatchedr   rW   r   r4   )r3   rj   listr'   r(   r   r)   r   submitr   rd   rD   re   info)rJ   rK   rM   rO   rP   rQ   rR   rH   rI   recordr   executorrn   rm   rW   r<   s         `       @@r   dispatch_async_delegationrz   |   s   R '((MIKKM&&.8DNNND"&$ F  
 ) ) 
 
((
 
 
 
 
 ((($R:L R R R	 	) ) ) ) ) ) ) ) #)) ) ) ) ) ) ) ) ) ) ) ) ) ) )" /00H5 5 5 5 5 5 5 5&

 	3G<<==== 
 
 
 	. 	.LL---	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. !BSBB
 
 	
 	
 	
 	
 	
 	

 KK={-g
CRC/@   #]CCCsZ   ?B-
B--B14B1"C3 3
E	=ED, E,D0	0E3D0	4
E>E	E	rW   rl   r   c                4   t           5  t                              |           }|	 ddd           dS ||d<   t          j                    |d<   d|d<   t	          |          }t                       ddd           n# 1 swxY w Y   t          |||           dS )zDMark a record complete and push the completion event onto the queue.Nr   r;   rH   )r'   r   r!   rj   dictrG   _push_completion_event)rW   rl   r   rx   event_records        r   rk   rk      s    	 	" 	"m,,>	" 	" 	" 	" 	" 	" 	" 	" "x!%~!%~F||!!!	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" <88888s   A<=A<<B B rx   c           	        	 ddl m} nG# t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w|                    d          }|                    d          }|                     d          pt          j                    }|                     d	          pt          j                    }i d
dd|                     d          d|                     dd          d|                     dd          d|                     d          d|                     d          d|                     d          d|                    d          p|                     d          d|d|d|d|                    dd          d|                    dt          ||z
  d                    d|d	|d|                    d          }		 |j        	                    |	           dS # t          $ r:}t                              d|                     d          |           Y d}~dS d}~ww xY w)zPush a type='async_delegation' event onto the shared completion queue.

    Best-effort: a failure here must not crash the worker, but it WOULD mean a
    silently-lost result, so we log loudly.
    r   process_registryzPAsync delegation %s finished but process_registry import failed; result lost: %srW   Nra   r]   r<   r;   rg   async_delegationrQ   rp   rJ   rK   rM   rO   rP   r   rb   rc   r`   exit_reasonzHAsync delegation %s: failed to enqueue completion event; result lost: %s)
tools.process_registryr   rd   re   r]   r!   rj   ri   completion_queueput)
rx   rl   r   r   rm   ra   r]   r<   r;   evts
             r   r}   r}      s   ;;;;;;;   JJ''	
 	
 	

 	 jj##GJJwEJJ//>49;;M::n--<L"O44
 	vzz-44 	

62&& 	6::i(( 	FJJz** 	

6"" 	G$$;

7(;(; 	& 	7 	 	VZZQ// 	FJJl]&BA F F
 
$ 	%& 	'( 	vzz-00)C,
)--c22222 
 
 
JJ''	
 	
 	
 	
 	
 	
 	
 	
 	

s,   	 
A/AA%H 
I/I  Igoals	List[str]c        	           t                      t          j                    t          |           }	|	dk    r| d         n$|	 dd                    d | D                       z   }
|
t	          |           ||rt	          |          nd|||dd|dd	}t
          5  t          d
 t                                          D                       }||k    rdd| ddcddd           S |t          <   ddd           n# 1 swxY w Y   t          |          }dfd}	 |
                    t          |                     nY# t          $ rL}t
          5  t                              d           ddd           n# 1 swxY w Y   dd| dcY d}~S d}~ww xY wt                              d|	|pd           ddS )u+  Dispatch a WHOLE fan-out batch as ONE background unit.

    Unlike ``dispatch_async_delegation`` (which backs a single subagent),
    ``runner`` here runs the entire batch — it builds and joins on every child
    in parallel and returns the combined ``{"results": [...],
    "total_duration_seconds": N}`` dict that the synchronous path would have
    returned. We occupy ONE async slot for the whole batch (the in-batch
    parallelism is bounded separately by ``max_concurrent_children``), so a
    single ``delegate_task`` fan-out never exhausts the async pool by itself.

    When the batch finishes, a SINGLE completion event is pushed onto the
    shared ``process_registry.completion_queue`` carrying the full per-task
    ``results`` list, so the consolidated summaries re-enter the conversation
    as one message once every child is done — the chat is never blocked while
    they run.

    Returns ``{"status": "dispatched", "delegation_id": ...}`` on success or
    ``{"status": "rejected", "error": ...}`` when the async pool is at
    capacity.
    r   r   z parallel subagents: z; c              3  *   K   | ]}|d d         V  d S )N(   r*   )r#   gs     r   r%   z2dispatch_async_delegation_batch.<locals>.<genexpr>\  s+      G^G^ST#2#G^G^G^G^G^G^r&   Nr   T)rW   rJ   r   rK   rM   rO   rP   rQ   r   r<   r;   rH   is_batchc              3  L   K   | ]}|                     d           dk    dV   dS r   r    r"   s     r   r%   z2dispatch_async_delegation_batch.<locals>.<genexpr>n  rY   r&   rZ   r[   z running). Wait for one to finish (its result will re-enter the chat), or raise delegation.max_concurrent_children in config.yaml to allow more concurrent background units.r\   r   r4   c                    i } d}	              pi } |                      d          pg }|rt          d |D                       rd}nd}ns# t          $ rf}t                              d           g t          |          j         d| t          t          j                    z
  d          d} d}Y d }~nd }~ww xY wt          | |           d S # t          | |           w xY w)	Nr]   resultsc              3  D   K   | ]}|                     d           dvV  dS )r   )rE   successNr    r"   s     r   r%   zCdispatch_async_delegation_batch.<locals>._worker.<locals>.<genexpr>  sF       % % x(@@% % % % % %r&   rE   z!Async delegation batch %s crashedr_   r`   )r   r]   total_duration_seconds)
r!   allrd   re   rf   rg   rh   ri   rj   _finalize_batch)combinedr   child_resultsrm   rW   r<   rR   s       r   rn   z0dispatch_async_delegation_batch.<locals>._worker  s5   #%	=vxx~2H$LL339rM % % %&% % % " " % !$ 	 	 	@-PPP II.77#77*/	m0KQ*O*O H
 FFFFFF	 M8V<<<<<OM8V<<<<s1   AA 
C 
B;AB61C 6B;;C C$z+Failed to schedule async delegation batch: zADispatched async delegation batch %s (%d task(s), session_key=%s)ro   rr   rs   rt   )r3   rj   rA   joinru   r'   r(   r   r)   r   rv   r   rd   rD   re   rw   )r   rK   rM   rO   rP   rQ   rR   rH   rI   ncombined_goalrx   r   ry   rn   rm   rW   r<   s         `         @@r   dispatch_async_delegation_batchr   7  s   @ '((MIKKME

A FFa1 ; ; ;diiG^G^X]G^G^G^>^>^ ^  'e&.8DNNND"&$ F 
 ) ) 
 
((
 
 
 
 
 ((($N:L N N N ) ) ) ) ) ) ) ) #)) ) ) ) ) ) ) ) ) ) ) ) ) ) )  /00H= = = = = = = =2	
3G<<==== 
 
 
 	. 	.LL---	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. !H3HH
 
 	
 	
 	
 	
 	
 	

 KKKq+0   #]CCCsZ   ?C>(
C>>DD!"E 
FFE=1F=F	FF	
FFFr   c                   t           5  t                              |           }|	 ddd           dS ||d<   t          j                    |d<   d|d<   t	          |          }t                       ddd           n# 1 swxY w Y   	 ddlm} n4# t          $ r'}t          
                    d| |           Y d}~dS d}~ww xY w|                    d          pt          j                    }|                    d          pt          j                    }i d	d
d| d|                    dd          d|                    dd          d|                    d          d|                    d          d|                    d          d|                    d          d|                    d          d|ddd|                    d          pg d|                    d          d|                    d          d|d|}		 |j                            |	           dS # t          $ r'}t          
                    d| |           Y d}~dS d}~ww xY w)zDMark a batch record complete and push ONE combined completion event.Nr   r;   rH   r   r   zVAsync delegation batch %s finished but process_registry import failed; result lost: %sr<   rg   r   rW   rQ   rp   rJ   r   rK   rM   rO   rP   r   Tr   r]   r   zNAsync delegation batch %s: failed to enqueue completion event; result lost: %s)r'   r   r!   rj   r|   rG   r   r   rd   re   r]   r   r   )
rW   r   r   rx   r~   r   rm   r<   r;   r   s
             r   r   r     sV    
 " "m,,>" " " " " " " " "x!%~!%~F||!!!" " " " " " " " " " " " " " ";;;;;;;   &3	
 	
 	

 	 !$$_55DM##N33Bty{{L" 	|''r:: 	  ,,	
 	!!'** 	<##I.. 	L$$Z00 	  (( 	!!'** 	& 	D 	8<<	**0b 	g&&  	!(,,/G"H"H!" 	#$ 	%C(
)--c22222 
 
 
3	
 	
 	
 	
 	
 	
 	
 	
 	

sF   A<=A<<B B B 
C B;;C H$ $
I.IIList[Dict[str, Any]]c                     t           5  d t                                          D             cddd           S # 1 swxY w Y   dS )zSnapshot of async delegations (running + recently completed).

    Safe to call from any thread. Excludes the non-serialisable interrupt_fn.
    c                J    g | ] }d  |                                 D             !S )c                &    i | ]\  }}|d k    ||S )rH   r*   )r#   kvs      r   
<dictcomp>z5list_async_delegations.<locals>.<listcomp>.<dictcomp>  s(    ???da1+>+>Q+>+>+>r&   )r@   r"   s     r   r9   z*list_async_delegations.<locals>.<listcomp>  s?     
 
 
 @?aggii???
 
 
r&   N)r'   r   r)   r*   r&   r   list_async_delegationsr     s    
 
 
 

 
__&&
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   #8<<shutdownreasonc                   d}t           5  d t                                          D             }ddd           n# 1 swxY w Y   |D ]}}|                    d          }t	          |          rW	  |             |dz  }7# t
          $ r9}t                              d|                    d          |           Y d}~ud}~ww xY w~|rt                              d||            |S )	a*  Signal every running async delegation to stop. Returns how many.

    Used on ``/stop`` and gateway shutdown so a dangling background subagent
    can't keep burning tokens with no one listening. The child still emits a
    completion event (status='interrupted') via the normal finalize path.
    r   c                D    g | ]}|                     d           dk    |S r7   r    r"   s     r   r9   z!interrupt_all.<locals>.<listcomp>  s3     
 
 
AEE(OOy,H,HA,H,H,Hr&   NrH   r   z&interrupt_all: %s interrupt failed: %srW   z'Interrupted %d async delegation(s) (%s))	r'   r   r)   r!   callablerd   re   debugrw   )r   counttargetsr$   fnrm   s         r   interrupt_allr     se    E	 
 

 
((
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
  
 
UU>""B<< 	
   <EE/**C       		  N=ufMMMLs%   $:>>.A>>
C/B<<Cc                    t           5  t          t                              d           dadaddd           n# 1 swxY w Y   t          5  t
                                           ddd           dS # 1 swxY w Y   dS )z6Test-only: clear all state and tear down the executor.NF)waitr   )r   r   r   r   r'   r   clearr*   r&   r   _reset_for_testsr   
  s    
 " " E***	 !	" " " " " " " " " " " " " " "
 
                   s   ';??A44A8;A8)r   r   r   r   )r   r   )r   r,   rt   )rJ   r,   rK   rL   rM   rN   rO   r,   rP   rL   rQ   r,   rR   rS   rH   rT   rI   r   r   rU   )rW   r,   rl   rU   r   r,   r   r4   )rx   rU   rl   rU   r   r,   r   r4   )r   r   rK   rL   rM   rN   rO   r,   rP   rL   rQ   r,   rR   rS   rH   rT   rI   r   r   rU   )rW   r,   r   rU   r   r,   r   r4   )r   r   )r   )r   r,   r   r   ),__doc__
__future__r   logging	threadingrj   r0   concurrent.futuresr   typingr   r   r   r   r	   tools.daemon_poolr
   tools.thread_contextr   	getLoggerrh   re   r   r   __annotations__Lockr   r   r'   r   _DEFAULT_MAX_ASYNC_CHILDRENrB   r   r+   r3   rG   rz   rk   r}   r   r   r   r   r   r*   r&   r   <module>r      so  ! ! !F # " " " " "        1 1 1 1 1 1 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 < < < < < <		8	$	$
 5  +/	 . . . .!!     	   ') ( ( ( (     &Q Q Q Q+ + + +       6 269qD qD qD qD qD qDh9 9 9 9 4
 4
 4
 4
@ 269pD pD pD pD pD pDf5
 5
 5
 5
p	
 	
 	
 	
    8	 	 	 	 	 	r&   