o
    PL
j`                     @   s2  U 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 ddl	m
Z
mZmZmZmZ eeZdejdefddZd	edefd
dZddee dee fddZG dd dZdZi Zee
eeef f ed< e Zde
defddZ dddZ!G dd dZ"e" Z#defddZ$ddefddZ%dS ) aV  Central registry for all hermes-agent tools.

Each tool file calls ``registry.register()`` at module level to declare its
schema, handler, toolset membership, and availability check.  ``model_tools.py``
queries the registry instead of maintaining its own parallel data structures.

Import chain (circular-import safe):
    tools/registry.py  (no imports from model_tools or tool files)
           ^
    tools/*.py  (import from tools.registry at module level)
           ^
    model_tools.py  (imports tools.registry + all tool modules)
           ^
    run_agent.py, cli.py, batch_runner.py, etc.
    N)Path)CallableDictListOptionalSetnodereturnc                 C   sV   t | tjrt | jtjsdS | jj}t |tjo*|jdko*t |jtjo*|jj	dkS )zHReturn True when *node* is a ``registry.register(...)`` call expression.Fregisterregistry)

isinstanceastExprvalueCallfunc	AttributeattrNameid)r   r    r   2/home/kuhnn/.hermes/hermes-agent/tools/registry.py_is_registry_register_call   s   
r   module_pathc              	   C   sP   z| j dd}tj|t| d}W n ttfy   Y dS w tdd |jD S )zReturn True when the module contains a top-level ``registry.register(...)`` call.

    Only inspects module-body statements so that helper modules which happen
    to call ``registry.register()`` inside a function are not picked up.
    zutf-8)encoding)filenameFc                 s   s    | ]}t |V  qd S N)r   ).0stmtr   r   r   	<genexpr>6   s    z*_module_registers_tools.<locals>.<genexpr>)	read_textr   parsestrOSErrorSyntaxErroranybody)r   sourcetreer   r   r   _module_registers_tools*   s   r)   	tools_dirc                 C   s   | durt | nt t j}dd t|dD }g }|D ]'}zt| || W q t	yF } zt
d|| W Y d}~qd}~ww |S )zLImport built-in self-registering tool modules and return their module names.Nc                 S   s*   g | ]}|j d vrt|rd|j qS )>   __init__.pymcp_tool.pyregistry.pyztools.)namer)   stem)r   pathr   r   r   
<listcomp><   s    

z*discover_builtin_tools.<locals>.<listcomp>z*.pyz#Could not import tool module %s: %s)r   __file__resolveparentsortedglob	importlibimport_moduleappend	Exceptionloggerwarning)r*   
tools_pathmodule_namesimportedmod_nameer   r   r   discover_builtin_tools9   s   
rB   c                   @   s    e Zd ZdZdZ	dddZdS )	ToolEntryz&Metadata for a single registered tool.r.   toolsetschemahandlercheck_fnrequires_envis_asyncdescriptionemojimax_result_size_charsdynamic_schema_overridesNc                 C   sF   || _ || _|| _|| _|| _|| _|| _|| _|	| _|
| _	|| _
d S r   rD   )selfr.   rE   rF   rG   rH   rI   rJ   rK   rL   rM   rN   r   r   r   __init__V   s   
zToolEntry.__init__)NN)__name__
__module____qualname____doc__	__slots__rP   r   r   r   r   rC   M   s
    rC   g      >@_check_fn_cachefnc                 C   s   t  }t$ t| }|dur#|\}}|| tk r#|W  d   S W d   n1 s-w   Y  zt|  }W n tyD   d}Y nw t ||ft| < W d   |S 1 sYw   Y  |S )zIReturn bool(fn()), TTL-cached across calls. Swallows exceptions as False.NF)time	monotonic_check_fn_cache_lockrV   get_CHECK_FN_TTL_SECONDSboolr:   )rW   nowcachedtsr   r   r   r   _check_fn_cached~   s*   
 
ra   c                   C   s2   t  t  W d   dS 1 sw   Y  dS )zDrop all cached ``check_fn`` results. Call after config changes that
    affect tool availability (e.g. ``hermes tools enable``).N)rZ   rV   clearr   r   r   r   invalidate_check_fn_cache   s   
"rc   c                   @   s  e Zd ZdZdd Zdeee ee	e
f f fddZdee fddZdee	e
f fd	d
Zde	de
dB defddZde	dee fddZdee	 fddZde	dee	 fddZde	de	ddfddZdee	e	f fddZde	dee	 fddZ								dMde	de	d ed!e
d"e
d#ed$ed%e	d&e	d'eeB dB d(e
d)efd*d+Zde	ddfd,d-ZdNd.ee	 d/edee fd0d1Zde	d2ede	fd3d4ZdOde	d5eeB dB deeB fd6d7Z dee	 fd8d9Z!de	dee fd:d;Z"de	dee	 fd<d=Z#dPde	d5e	de	fd?d@Z$dee	e	f fdAdBZ%de	defdCdDZ&dee	ef fdEdFZ'dee	ef fdGdHZ(dee	ef fdIdJZ)dNd/efdKdLZ*dS )QToolRegistryzISingleton registry that collects tool schemas + handlers from tool files.c                 C   s&   i | _ i | _i | _t | _d| _d S )Nr   )_tools_toolset_checks_toolset_aliases	threadingRLock_lock_generationrO   r   r   r   rP      s
   

zToolRegistry.__init__r	   c                 C   sB   | j  t| j t| jfW  d   S 1 sw   Y  dS )zBReturn a coherent snapshot of registry entries and toolset checks.N)rj   listre   valuesdictrf   rl   r   r   r   _snapshot_state   s   $zToolRegistry._snapshot_statec                 C      |   d S )z4Return a stable snapshot of registered tool entries.r   rp   rl   r   r   r   _snapshot_entries      zToolRegistry._snapshot_entriesc                 C   rq   )z8Return a stable snapshot of toolset availability checks.   rr   rl   r   r   r   _snapshot_toolset_checks   rt   z%ToolRegistry._snapshot_toolset_checksrE   checkNc                 C   s6   |sdS zt | W S  ty   td| Y dS w )zQRun a toolset check, treating missing or failing checks as unavailable/available.Tz,Toolset %s check raised; marking unavailableF)r]   r:   r;   debugrO   rE   rw   r   r   r   _evaluate_toolset_check   s   z$ToolRegistry._evaluate_toolset_checkr.   c                 C   6   | j  | j|W  d   S 1 sw   Y  dS )z0Return a registered tool entry by name, or None.N)rj   re   r[   )rO   r.   r   r   r   	get_entry      
$zToolRegistry.get_entryc                 C      t dd |  D S )z;Return sorted unique toolset names present in the registry.c                 S      h | ]}|j qS r   rE   r   entryr   r   r   	<setcomp>       z<ToolRegistry.get_registered_toolset_names.<locals>.<setcomp>r5   rs   rl   r   r   r   get_registered_toolset_names      z)ToolRegistry.get_registered_toolset_namesc                    s   t  fdd|  D S )z:Return sorted tool names registered under a given toolset.c                 3   s     | ]}|j  kr|jV  qd S r   rE   r.   r   r   r   r   r      s    
z:ToolRegistry.get_tool_names_for_toolset.<locals>.<genexpr>r   )rO   rE   r   r   r   get_tool_names_for_toolset   s   z'ToolRegistry.get_tool_names_for_toolsetaliasc                 C   sl   | j ) | j|}|r||krtd||| || j|< |  jd7  _W d   dS 1 s/w   Y  dS )z8Register an explicit alias for a canonical toolset name.z4Toolset alias collision: '%s' (%s) overwritten by %sru   N)rj   rg   r[   r;   r<   rk   )rO   r   rE   existingr   r   r   register_toolset_alias   s   
"z#ToolRegistry.register_toolset_aliasc                 C   s4   | j  t| jW  d   S 1 sw   Y  dS )z=Return a snapshot of ``{alias: canonical_toolset}`` mappings.N)rj   ro   rg   rl   r   r   r   get_registered_toolset_aliases   s   $z+ToolRegistry.get_registered_toolset_aliasesc                 C   r{   )z8Return the canonical toolset name for an alias, or None.N)rj   rg   r[   )rO   r   r   r   r   get_toolset_alias_target   r}   z%ToolRegistry.get_toolset_alias_targetF rF   rG   rH   rI   rJ   rK   rL   rM   rN   overridec                 C   s
  | j x | j|}|rF|j|krF|jdo|d}|r(td|||j n|r4td|||j ntd|||j 	 W d   dS t	||||||pOg ||pW|dd|	|
|d| j|< |rl|| j
vrl|| j
|< |  jd	7  _W d   dS 1 s~w   Y  dS )
a  Register a tool.  Called at module-import time by each tool file.

        ``override=True`` is an explicit opt-in for plugins that intend to
        replace an existing built-in tool implementation (e.g. swap the
        default browser tool for a headed-Chrome CDP backend). Without it,
        registrations that would shadow an existing tool from a different
        toolset are rejected to prevent accidental overwrites.
        zmcp-z8Tool '%s': MCP toolset '%s' overwriting MCP toolset '%s'zOTool '%s': toolset '%s' overriding existing toolset '%s' (override=True opt-in)zTool registration REJECTED: '%s' (toolset '%s') would shadow existing tool from toolset '%s'. Pass override=True to register() if the replacement is intentional, or deregister the existing tool first.NrK   r   rD   ru   )rj   re   r[   rE   
startswithr;   rx   infoerrorrC   rf   rk   )rO   r.   rE   rF   rG   rH   rI   rJ   rK   rL   rM   rN   r   r   both_mcpr   r   r   r
      sN   !
"zToolRegistry.registerc                    s   | j H | j|d  du r	 W d   dS t fdd| j D }|s=| j jd  fdd| j D | _|  j	d7  _	W d   n1 sNw   Y  t
d| dS )a  Remove a tool from the registry.

        Also cleans up the toolset check if no other tools remain in the
        same toolset.  Used by MCP dynamic tool discovery to nuke-and-repave
        when a server sends ``notifications/tools/list_changed``.
        Nc                 3   s    | ]	}|j  j kV  qd S r   r   r   rA   r   r   r   r   @  s    
z*ToolRegistry.deregister.<locals>.<genexpr>c                    s    i | ]\}}| j kr||qS r   r   )r   r   targetr   r   r   
<dictcomp>E  s
    
z+ToolRegistry.deregister.<locals>.<dictcomp>ru   zDeregistered tool: %s)rj   re   popr%   rn   rf   rE   rg   itemsrk   r;   rx   )rO   r.   toolset_still_existsr   r   r   
deregister3  s    
zToolRegistry.deregister
tool_namesquietc                 C   s   g }i }dd |   D }t|D ]i}||}|sq|jr9|j|vr+t|j||j< ||j s9|s8td| qi |jd|ji}|j	durrz|	 }	t
|	trW||	 W n tyq }
 ztd||
 W Y d}
~
nd}
~
ww |d|d q|S )	a"  Return OpenAI-format tool schemas for the requested tool names.

        Only tools whose ``check_fn()`` returns True (or have no check_fn)
        are included. ``check_fn()`` results are cached for ~30 s via
        :func:`_check_fn_cached` to amortize repeat probes (check_terminal_
        requirements probes modal/docker, browser checks probe playwright,
        etc.); TTL chosen so env-var changes (``hermes tools enable foo``)
        still take effect in near-real-time without forcing a full cache
        flush on every call.
        c                 S   s   i | ]}|j |qS r   r.   r   r   r   r   r   a  s    z0ToolRegistry.get_definitions.<locals>.<dictcomp>z"Tool %s unavailable (check failed)r.   NzCdynamic_schema_overrides for tool %s raised %s; using static schemafunction)typer   )rs   r5   r[   rH   ra   r;   rx   rF   r.   rN   r   ro   updater:   r<   r9   )rO   r   r   resultcheck_resultsentries_by_namer.   r   schema_with_name	overridesexcr   r   r   get_definitionsQ  s<   





zToolRegistry.get_definitionsargsc           
      K   s   |  |}|stdd| iS z|jr'ddlm} ||j|fi |W S |j|fi |W S  tys } z7t	d|| dt
|j d| }zddlm} ||}	W n tya   |}	Y nw td|	iW  Y d	}~S d	}~ww )
zExecute a tool handler by name.

        * Async handlers are bridged automatically via ``_run_async()``.
        * All exceptions are caught and returned as ``{"error": "..."}``
          for consistent error format.
        r   zUnknown tool: r   )
_run_asynczTool %s dispatch error: %szTool execution failed: z: )_sanitize_tool_errorN)r|   jsondumpsrJ   model_toolsr   rG   r:   r;   	exceptionr   rQ   r   )
rO   r.   r   kwargsr   r   rA   rawr   	sanitizedr   r   r   dispatch  s(   
zToolRegistry.dispatchdefaultc                 C   s:   |  |}|r|jdur|jS |dur|S ddlm} |S )zBReturn per-tool max result size, or *default* (or global default).Nr   )DEFAULT_RESULT_SIZE_CHARS)r|   rM   tools.budget_configr   )rO   r.   r   r   r   r   r   r   get_max_result_size  s   
z ToolRegistry.get_max_result_sizec                 C   r~   )z0Return sorted list of all registered tool names.c                 s   s    | ]}|j V  qd S r   r   r   r   r   r   r     s    z2ToolRegistry.get_all_tool_names.<locals>.<genexpr>r   rl   r   r   r   get_all_tool_names  r   zToolRegistry.get_all_tool_namesc                 C      |  |}|r
|jS dS )u   Return a tool's raw schema dict, bypassing check_fn filtering.

        Useful for token estimation and introspection where availability
        doesn't matter — only the schema content does.
        N)r|   rF   rO   r.   r   r   r   r   
get_schema  s   
zToolRegistry.get_schemac                 C   r   )z.Return the toolset a tool belongs to, or None.N)r|   rE   r   r   r   r   get_toolset_for_tool  s   
z!ToolRegistry.get_toolset_for_tool   ⚡c                 C   s   |  |}|r|jr|jS |S )z3Return the emoji for a tool, or *default* if unset.)r|   rL   )rO   r.   r   r   r   r   r   	get_emoji  s   
zToolRegistry.get_emojic                 C   s   dd |   D S )z?Return ``{tool_name: toolset_name}`` for every registered tool.c                 S   s   i | ]}|j |jqS r   )r.   rE   r   r   r   r   r     s    z8ToolRegistry.get_tool_to_toolset_map.<locals>.<dictcomp>)rs   rl   r   r   r   get_tool_to_toolset_map  s   z$ToolRegistry.get_tool_to_toolset_mapc                 C   s>   | j  | j|}W d   n1 sw   Y  | ||S )zCheck if a toolset's requirements are met.

        Returns False (rather than crashing) when the check function raises
        an unexpected exception (e.g. network error, missing import, bad config).
        N)rj   rf   r[   rz   ry   r   r   r   is_toolset_available  s   z!ToolRegistry.is_toolset_availablec                    s2      \}tdd |D } fdd|D S )z7Return ``{toolset: available_bool}`` for every toolset.c                 S   r   r   r   r   r   r   r   r     r   z:ToolRegistry.check_toolset_requirements.<locals>.<setcomp>c              	      s    i | ]}|  ||qS r   )rz   r[   )r   rE   rO   toolset_checksr   r   r     s    z;ToolRegistry.check_toolset_requirements.<locals>.<dictcomp>)rp   r5   )rO   entriestoolsetsr   r   r   check_toolset_requirements  s
   z'ToolRegistry.check_toolset_requirementsc                 C   s   i }|   \}}|D ]=}|j}||vr#| |||g dg d||< || d |j |jrG|jD ]}||| d vrF|| d | q3q
|S )z'Return toolset metadata for UI display.r   )	availabletoolsrK   requirementsr   r   )rp   rE   rz   r[   r9   r.   rI   )rO   r   r   r   r   r`   envr   r   r   get_available_toolsets  s&   


z#ToolRegistry.get_available_toolsetsc                 C   s   i }|   \}}|D ]@}|j}||vr |g ||dg d||< |j|| d vr3|| d |j |jD ]}||| d vrI|| d | q6q
|S )zABuild a TOOLSET_REQUIREMENTS-compatible dict for backward compat.N)r.   env_varsrH   	setup_urlr   r   r   )rp   rE   r[   r.   r9   rI   )rO   r   r   r   r   r`   r   r   r   r   get_toolset_requirements  s&   

z%ToolRegistry.get_toolset_requirementsc                    s   g }g }t  }|  \}}|D ]/}|j  |v rq|  |  | r-|  q| |j fdd|D d q||fS )zDReturn (available_toolsets, unavailable_info) like the old function.c                    s   g | ]
}|j  kr|jqS r   r   r   r`   r   r   r1     s    z8ToolRegistry.check_tool_availability.<locals>.<listcomp>)r.   r   r   )setrp   rE   addrz   r[   r9   rI   )rO   r   r   unavailableseenr   r   r   r   r   r   check_tool_availability	  s"   

z$ToolRegistry.check_tool_availability)NNFr   r   NNF)Fr   )r   )+rQ   rR   rS   rT   rP   tupler   rC   r   r"   r   rp   rs   rv   r]   rz   r   r|   r   r   r   r   r   ro   rm   intfloatr
   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rd      sx    "
	


I 5$ 
	
	rd   c                 K   s(   dt | i}|r|| tj|ddS )zReturn a JSON error string for tool handlers.

    >>> tool_error("file not found")
    '{"error": "file not found"}'
    >>> tool_error("bad input", success=False)
    '{"error": "bad input", "success": false}'
    r   Fensure_ascii)r"   r   r   r   )messageextrar   r   r   r   
tool_error3  s   
r   c                 K   s$   | durt j| ddS t j|ddS )a  Return a JSON result string for tool handlers.

    Accepts a dict positional arg *or* keyword arguments (not both):

    >>> tool_result(success=True, count=42)
    '{"success": true, "count": 42}'
    >>> tool_result({"key": "value"})
    '{"key": "value"}'
    NFr   )r   r   )datar   r   r   r   tool_resultA  s   
r   r   )r	   N)&rT   r   r7   r   loggingrh   rX   pathlibr   typingr   r   r   r   r   	getLoggerrQ   r;   ASTr]   r   r)   r"   rB   rC   r\   rV   r   r   __annotations__LockrZ   ra   rc   rd   r   r   r   r   r   r   r   <module>   s4    
,
   