
    .cje8                    `   U d Z ddlmZ ddlZddlZddlZddlmZmZ ddl	m
Z
mZmZmZ  ej        e          Ze G d d                      Z ej                    Zi Zded	<   i Zd
ed<   d4dZd5dZd6dZddd7dZd8d!Zd9d"Zd:d#Zd;d$Zd<d&Zd=d)Z d>d*Z!i Z"d+ed,<   d?d0Z#d@d1Z$dAd3Z%dS )Bu  Gateway-side clarify primitive (blocking event-based queue).

The ``clarify`` tool needs to ask the user a question and block the agent
thread until they respond.  In CLI mode this is trivial — ``input()`` is
synchronous.  In gateway mode the agent runs on a worker thread while the
event loop handles the user's reply, so we need a thread-safe primitive
that:

  * stores a pending clarify request (with a generated ``clarify_id``),
  * blocks the agent thread on an ``Event``,
  * resolves the wait when the gateway's button-callback or text-intercept
    fires ``resolve_gateway_clarify(clarify_id, response)``,
  * supports timeouts so a user who never responds does NOT hang the agent
    thread forever (which would also pin the gateway's running-agent guard).

State is module-level (same shape as ``tools.approval``) so platform
adapters can call ``resolve_gateway_clarify`` without holding a back-
reference to the ``GatewayRunner`` instance.

Two delivery paths from the adapter:

  1. **Button UI** — adapters override ``send_clarify`` to render inline
     buttons (e.g. Telegram ``InlineKeyboardMarkup``).  The button
     callback resolves with the chosen string.  A final "Other (type
     answer)" button enters text-capture mode for free-form responses.

  2. **Text fallback** — adapters without rich UI render a numbered list.
     The user replies with a number ("2") or with free text; the gateway's
     ``_handle_message`` intercepts the reply and resolves directly.
    )annotationsN)	dataclassfield)CallableDictListOptionalc                      e Zd ZU dZded<   ded<   ded<   ded<    eej                  Zd	ed
<   dZ	ded<   dZ
ded<   ddZdS )_ClarifyEntryz5One pending clarify request inside a gateway session.str
clarify_idsession_keyquestionOptional[List[str]]choices)default_factoryzthreading.EventeventNOptional[str]responseFboolawaiting_textreturnDict[str, object]c                d    | j         | j        | j        | j        rt	          | j                  nd dS )N)r   r   r   r   )r   r   r   r   list)selfs    7/home/ice/.hermes/hermes-agent/tools/clarify_gateway.py	signaturez_ClarifyEntry.signature:   s:    /+-1\CtDL)))t	
 
 	
    )r   r   )__name__
__module____qualname____doc____annotations__r   	threadingEventr   r   r   r    r   r   r   r   /   s         ??OOOMMM    "U9?CCCECCCC"H""""M
 
 
 
 
 
r   r   zDict[str, _ClarifyEntry]_entrieszDict[str, List[str]]_session_indexr   r   r   r   r   r   r   c           	        t          | |||rt          |          ndt          |                     }t          5  |t          | <   t
                              |g                               |            ddd           n# 1 swxY w Y   |S )zRegister a pending clarify request and return the entry.

    The caller (gateway clarify_callback) will then send the prompt to the
    user and block on ``wait_for_response(clarify_id, timeout)``.
    N)r   r   r   r   r   )r   r   r   _lockr(   r)   
setdefaultappend)r   r   r   r   entrys        r   registerr/   N   s     !(2Wdw--'  E 
 F F$!!+r2299*EEEF F F F F F F F F F F F F F F Ls   9B  BBtimeoutfloatr   c                p   t           5  t                              |           }ddd           n# 1 swxY w Y   |dS 	 ddlm} n# t
          $ r d}Y nw xY w|du pt          |          dk    }|rdn"t          j                    t          |          z   }t          j                    t          j                    d}	 |d}n-|t          j                    z
  }|dk    rn;t          d|          }|j
                            |          rn| ||d	           ]t           5  t                              | d           t                              |j                  }	|	r;| |	v r7|	                    |            |	s t                              |j        d           ddd           n# 1 swxY w Y   |j        S )
uH  Block on the entry's event until resolved or timeout fires.

    Polls in 1-second slices so the agent's inactivity heartbeat keeps
    firing — without this, ``Event.wait(timeout=600)`` blocks the thread
    for 10 minutes with zero activity touches and the gateway's inactivity
    watchdog kills the agent while the user is still typing.

    ``timeout <= 0`` means an unlimited wait (never auto-skip mid-think); the
    heartbeat still fires each slice so inactivity watchdogs don't kill a live
    prompt.

    Returns the resolved response string, or ``None`` on timeout.
    Nr   )touch_activity_if_dueg        )
last_touchstartTg      ?)r0   z!waiting for user clarify response)r+   r(   gettools.environments.baser3   	Exceptionr1   time	monotonicminr   waitpopr)   r   remover   )
r   r0   r.   r3   	unlimiteddeadlineactivity_stateslice_s	remainingidss
             r   wait_for_responserE   g   sh    
 ) )Z(() ) ) ) ) ) ) ) ) ) ) ) ) ) )}t%AAAAAAA % % % $% 485>>S#8I Gttdn&6&6w&GH$(N$4$4t~?O?OPPNWGG 4>#3#33IA~~#y))G;G,, 	 ,!!.2UVVVW 
 < <Z&&&  !233 	<:$$JJz""" <""5#4d;;;< < < < < < < < < < < < < < < >s0   /33A AA"A8F&&F*-F*r   r   c                    t           5  t                              |           }|	 ddd           dS 	 ddd           n# 1 swxY w Y   |t          |          nd|_        |j                                         dS )zUnblock the agent thread waiting on ``clarify_id``.

    Returns True if an entry was found and resolved, False otherwise
    (already resolved, expired, or never existed).
    NF T)r+   r(   r6   r   r   r   set)r   r   r.   s      r   resolve_gateway_clarifyrI      s     
  Z((=                      '/&:S]]]EN	KOO4s   A  AAFinclude_choice_promptsrK   Optional[_ClarifyEntry]c                   t           5  t                              |           pg }|D ]8}t                              |          }||s|j        r|c cddd           S 9	 ddd           dS # 1 swxY w Y   dS )a  Return the oldest pending clarify entry for a session, or None.

    By default this only returns entries awaiting free-form text (open-ended
    clarifies, or a multi-choice clarify after the user picked ``Other``).
    Gateways may pass ``include_choice_prompts=True`` when the user has typed
    directly in response to an active multi-choice prompt; in that case the
    oldest unresolved clarify is returned so the text can resolve it instead
    of being queued as an unrelated follow-up turn.
    N)r+   r)   r6   r(   r   )r   rK   rD   cidr.   s        r   get_pending_for_sessionrO      s     
    --3 	 	CLL%%E}% )<                         s   A
A.A..A25A2r.   c                   t          |                                          }| j        s|S 	 t          |          dz
  }n# t          $ r d}Y nw xY wd|cxk    rt          | j                  k     rn n| j        |         S | j        D ]n}|                                t          |                                                                          k    r#t          |                                          c S o| j        r|S dS )u  Map typed choice replies to canonical choice text, otherwise keep or reject custom text.

    For native interactive multi-choice clarifies (button UI, awaiting_text=False):
      - Accept numeric selections ("2" → choice[1])
      - Accept exact choice label matches (case-insensitive)
      - Reject arbitrary prose (return None) so the message continues as a normal turn

    For text fallback or awaiting_text mode:
      - Accept any text (numeric/label/custom) after passing through coercion

    For open-ended clarifies (no choices):
      - Accept any text

    Returns None when the response should be rejected (arbitrary prose for native multi-choice).
       r   N)r   stripr   int
ValueErrorlencasefoldr   )r.   r   textidxchoices        r   _coerce_text_responser[      s     x==  D= $ii!m    	C$$$$#em$$$$$$$}S!! - ' '==??c&kk//11::<<<<v;;$$&&&&& =
  4s   ? AAc                ~    t          | d          }|dS t          ||          }|dS t          |j        |          S )zResolve the oldest pending clarify in ``session_key`` from typed text.

    Returns False if no pending clarify exists or if the response was rejected
    (arbitrary prose for native interactive multi-choice clarifies).
    TrJ   NF)rO   r[   rI   r   )r   r   r.   coerceds       r   !resolve_text_response_for_sessionr^      sT     $KMMME}u#E844Gu"  r   c                    t           5  t                              |           }|	 ddd           dS d|_        	 ddd           dS # 1 swxY w Y   dS )zFlip an entry into text-capture mode (user picked the 'Other' button).

    Returns True if the entry exists and was flipped, False otherwise.
    NFT)r+   r(   r6   r   )r   r.   s     r   mark_awaiting_textr`   	  s    
 
  Z((=        #                 s   AAAAc                    t           5  t                              |           pg }t          d |D                       cddd           S # 1 swxY w Y   dS )zEReturn True when this session has at least one pending clarify entry.c              3  N   K   | ] }t                               |          d uV  !d S N)r(   r6   .0rN   s     r   	<genexpr>zhas_pending.<locals>.<genexpr>  s3      @@S8<<$$D0@@@@@@r   N)r+   r)   r6   any)r   rD   s     r   has_pendingrh     s    	 A A  --3@@C@@@@@A A A A A A A A A A A A A A A A A As   5A

AArT   c                   t           5  t          t                              | g           pg           }d |D             }ddd           n# 1 swxY w Y   d}|D ]*}|d|_        |j                                         |dz  }+|S )a  Resolve and drop every pending clarify for a session.

    Used by session-boundary cleanup (e.g. ``/new``, gateway shutdown,
    cached-agent eviction) so blocked agent threads don't hang past the
    end of their session.  Returns the number of entries cancelled.
    c                D    g | ]}t                               |d           S rc   )r(   r=   rd   s     r   
<listcomp>z!clear_session.<locals>.<listcomp>&  s&    :::s8<<T**:::r   Nr   rG   rQ   )r+   r   r)   r=   r   r   rH   )r   rD   entries	cancelledr.   s        r   clear_sessionrn     s     
 ; ;>%%k266<"==::c:::; ; ; ; ; ; ; ; ; ; ; ; ; ; ; I 	 	=
 Q		s   7AAAconfigdictc                    |                      d          pi                      d          }|+|                      d          pi                      dd          }	 t          |          S # t          t          f$ r Y dS w xY w)ac  Resolve the clarify timeout (seconds) from an already-loaded config dict.

    Single source of truth shared by every surface (messaging gateway, CLI,
    TUI/desktop) so the timeout can't drift between them.  Resolution order:

    1. legacy top-level ``clarify.timeout`` if a user explicitly set it,
    2. else the canonical ``agent.clarify_timeout``,
    3. else 3600 (1 hour).

    ``<= 0`` is preserved verbatim and means *unlimited* to callers (never
    auto-skip while the user is still deciding); the waiting loops translate
    that into a null deadline.  A non-numeric value falls back to 3600.
    clarifyr0   Nagentclarify_timeout  )r6   rT   	TypeErrorrU   )ro   raws     r   resolve_clarify_timeoutrx   9  s     ::i  &B
+
+I
6
6C
{zz'""(b--.?FF3xxz"   tts   A( (A=<A=c                 d    	 ddl m}  t           |             pi           S # t          $ r Y dS w xY w)u  Read the clarify response timeout (seconds) from config.

    Defaults to 3600 (1 hour) — long enough that a user who steps away
    (meeting, AFK, slow to read) still finds a live entry when they tap
    the button, short enough that a genuinely abandoned prompt eventually
    unblocks the agent thread instead of pinning the running-agent guard
    forever.  The old 600s default evicted the entry mid-think, so a late
    tap landed on a dead entry and the agent hung on ``running: clarify``
    (#32762).

    Reads ``agent.clarify_timeout`` from config.yaml (see
    :func:`resolve_clarify_timeout` for the full resolution order).  Set to
    ``0`` (or negative) for an unlimited wait — never auto-skip while the user
    is still deciding.
    r   load_configru   )hermes_cli.configr{   rx   r8   rz   s    r   get_clarify_timeoutr}   P  sT     111111&{{}}':;;;   tts   ! 
//z*Dict[str, Callable[[_ClarifyEntry], None]]_notify_cbscbCallable[[_ClarifyEntry], None]Nonec                Z    t           5  |t          | <   ddd           dS # 1 swxY w Y   dS )zDRegister a per-session notify callback used by ``clarify_callback``.N)r+   r~   )r   r   s     r   register_notifyr   r  sv    	 & &#%K & & & & & & & & & & & & & & & & & &s    $$c                    t           5  t                              | d           ddd           n# 1 swxY w Y   t          |            dS )zLDrop the per-session notify callback and cancel any pending clarify entries.N)r+   r~   r=   rn   r   s    r   unregister_notifyr   x  s    	 + +T***+ + + + + + + + + + + + + + + +s   044)Optional[Callable[[_ClarifyEntry], None]]c                x    t           5  t                              |           cd d d            S # 1 swxY w Y   d S rc   )r+   r~   r6   r   s    r   
get_notifyr     s{    	 , ,{++, , , , , , , , , , , , , , , , , ,s   /33)
r   r   r   r   r   r   r   r   r   r   )r   r   r0   r1   r   r   )r   r   r   r   r   r   )r   r   rK   r   r   rL   )r.   r   r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   )r   r   r   rT   )ro   rp   r   rT   )r   rT   )r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   )&r#   
__future__r   loggingr%   r9   dataclassesr   r   typingr   r   r   r	   	getLoggerr    loggerr   RLockr+   r(   r$   r)   r/   rE   rI   rO   r[   r^   r`   rh   rn   rx   r}   r~   r   r   r   r'   r   r   <module>r      sE    > # " " " " "       ( ( ( ( ( ( ( ( 1 1 1 1 1 1 1 1 1 1 1 1		8	$	$ 
 
 
 
 
 
 
 
& 		%' ' ' ' '') ) ) ) )   22 2 2 2r   $ $)     2) ) ) )X   *
 
 
 
A A A A   8   .   > ;= < < < <& & & &   , , , , , ,r   