
    jj                    0   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m	Z	 ddl
mZmZmZmZmZmZ  ej        d          ZdZdZd	Z eeeeh          Zd
Z ed           G d d                      Zd[dZd\dZd]dZd^dZd_dZd`d!Zdad#Z dbd(Z!e G d) d*                      Z" ej#        d+          Z$dcd.Z%ddd1Z&ded3Z'dfd5Z(	 dgdhdBZ)didjdGZ*dkdIZ+e G dJ dK                      Z,dddLdldNZ-d_dOZ.dmdQZ/ddRdndUZ0dodVZ1dpdWZ2dqdYZ3g dZZ4dS )ru  Progressive tool disclosure ("tool search") for Hermes Agent.

When enabled, MCP and non-core plugin tools are replaced in the model-visible
tools array by three bridge tools — ``tool_search``, ``tool_describe``,
``tool_call`` — and surfaced on demand. Core Hermes tools never defer.

Design constraints this module is built around (see ``openclaw-tool-search-report``
for the full rationale):

* Core tools defined in ``toolsets._HERMES_CORE_TOOLS`` are *never* deferred.
  Always-load means always-load. No exceptions.
* The threshold gate runs every assembly: when deferrable tools would consume
  less than ``threshold_pct`` of the model's context window (default 10%),
  tool search is a no-op and the tools array passes through unchanged.
* The catalog is stateless across turns and tools-array assemblies. It is
  rebuilt from the current tool-defs list every time. This is the lesson
  from OpenClaw's cron regression (openclaw/openclaw#84141): a session-keyed
  catalog that drifts out of sync with the live tool registry produces
  silent tool dropouts.
* Bridge tools route through ``model_tools.handle_function_call`` exactly
  like a direct call, so guardrails, plugin pre/post hooks, approval flows,
  and tool-result truncation all fire identically.
* Display and trajectory unwrap is implemented here so the user (CLI activity
  feed, gateway, saved trajectories) always sees the underlying tool, not
  the bridge.
    )annotationsN)	dataclassfield)AnyDictIterableListOptionalTupleztools.tool_searchtool_searchtool_describe	tool_callg      @T)frozenc                  T    e Zd ZU dZded<   ded<   ded<   ded<   edd            ZdS )ToolSearchConfigzDResolved, validated tool-search configuration for a single assembly.strenabledfloatthreshold_pctintsearch_default_limitmax_search_limitrawr   return'ToolSearchConfig'c                   |du r | dddd          S |du r | dddd          S t          |t                    s | dddd          S t          |                    d	d                                                                                    }|d
v rd}n|dv rd}n	|dv r|}nd}t          |                    d          d          }t          dt          d|                    }t          dt          dt          |                    d          d                              }t          dt          |t          |                    d          d                              } | ||||          S )ay  Build a config from a raw dict / bool / None.

        Accepts the legacy bool shape (``tools.tool_search: true``) and the
        dict shape (``tools.tool_search: {enabled: auto, ...}``). Validates
        and clamps every numeric field; unknown values fall back to safe
        defaults rather than raising, so a typo in user config does not
        break the agent.
        Tautog      $@      )r   r   r   r   Foffr   )true1yeson)false0no)r   r$   r    r                 Y@   2   r   r   )

isinstancedictr   getstriplower_safe_floatmaxmin	_safe_int)clsr   enabled_rawr   r   r   r   s          6/home/ubuntu/.hermes/hermes-agent/tools/tool_search.pyfrom_rawzToolSearchConfig.from_rawH   s    $;;3vT,-D D D D%<<3uD,-D D D D#t$$ 	D3vT,-D D D D #'')V4455;;==CCEE...GG000GG111!GGG#CGGO$<$<dCCCUM!:!:;;q#b)CGG<N4O4OQS*T*T"U"UVV"1c*:*3CGG<R4S4SUV*W*W'Y 'Y  Z  Z s'!5-	
 
 
 	
    N)r   r   r   r   )__name__
__module____qualname____doc____annotations__classmethodr8    r9   r7   r   r   ?   sg         NNLLL)
 )
 )
 [)
 )
 )
r9   r   valuer   fallbackr   r   c                T    	 t          |           S # t          t          f$ r |cY S w xY wN)r   	TypeError
ValueErrorrA   rB   s     r7   r4   r4   u   s<    5zzz"       ''r   c                T    	 t          |           S # t          t          f$ r |cY S w xY wrD   )r   rE   rF   rG   s     r7   r1   r1   |   s<    U||z"   rH   c                    	 ddl m}   |             pi }t          |                    d          t                    r|                    d          ni }t          |t                    si }t
                              |                    d                    S # t          $ r?}t          	                    d|           t
                              d          cY d}~S d}~ww xY w)z2Load tool-search config from the user config file.r   )load_configtoolsr   z%Failed to load tool-search config: %sN)
hermes_cli.configrK   r,   r.   r-   r   r8   	Exceptionloggerdebug)_loadcfg	tools_cfges       r7   rK   rK      s    	/::::::eggm(237773C3CT(J(JRCGGG$$$PR	)T** 	I((})E)EFFF / / /<a@@@((......../s   BB 
C !4CC C frozenset[str]c                 j    	 ddl m}  t          |           S # t          $ r t                      cY S w xY w)zReturn the set of tool names that must NEVER be deferred.

    Imported lazily because ``toolsets`` imports from ``tools.registry``
    and we don't want a hard cycle.
    r   _HERMES_CORE_TOOLS)toolsetsrX   	frozensetrN   rW   s    r7   _core_tool_namesr[      sS    //////+,,,   {{s    22namer   boolc                    | t           v rdS | t                      v rdS 	 ddlm} |                    |           }|dS |j                            d          rdS dS # t          $ r Y dS w xY w)aF  Return True if a tool with this name is *eligible* for deferral.

    A tool is deferrable iff it is registered with an MCP toolset prefix
    OR it is not in ``_HERMES_CORE_TOOLS``. Core tools are never deferred
    even when their toolset is technically plugin-provided (this protects
    against accidental shadowing).
    Fr   registryNmcp-T)BRIDGE_TOOL_NAMESr[   tools.registryr`   	get_entrytoolset
startswithrN   r\   r`   entrys      r7   is_deferrable_tool_nameri      s        u!!!!u
++++++""4((=5=##F++ 	4t   uus   A A 
A*)A*	tool_defsList[Dict[str, Any]]1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]c                    g }g }| D ]s}|                     d          pi }|                     dd          }|t          v r9t          |          r|                    |           ^|                    |           t||fS )zSplit a tool-defs list into (visible, deferrable).

    ``visible`` retains every tool that must stay in the model-facing array:
    every core tool, plus any tool we can't classify. ``deferrable`` is the
    candidate set for catalog entry.
    functionr\    )r.   rb   ri   append)rj   visible
deferrabletdfnr\   s         r7   classify_toolsru      s     %'G')J 
 
VVJ%2vvfb!!$$$ "4(( 	b!!!!NN2Jr9   Iterable[Dict[str, Any]]c           	        d}| D ]`}	 |t          t          j        |dd                    z  }+# t          t          f$ r" |t          t          |                    z  }Y ]w xY wt          t          j        |t          z                      S )uS  Estimate the token cost of a tool-defs list via the chars/4 rule.

    Cheap and stable across providers. The number doesn't need to be exact —
    it gates the activate/skip decision, and a typical 200K context with a
    10% threshold means the decision flips around 20K tokens of schema.
    Order-of-magnitude precision is fine.
    r   F),:)ensure_ascii
separators)
lenjsondumpsrE   rF   r   r   mathceilCHARS_PER_TOKEN)rj   total_charsrs   s      r7   estimate_tokens_from_schemasr      s     K ( (	(3tz"5ZXXXYYYKK:& 	( 	( 	(3s2ww<<'KKK	(ty677888s   '00A#"A#configdeferrable_tokenscontext_lengthOptional[int]c                    | j         dk    rdS |dk    rdS | j         dk    rdS |r|dk    r|dk    S t          || j        dz  z            }||k    S )u]  Decide whether tool search should activate for the current assembly.

    ``"off"`` skips unconditionally. ``"on"`` activates unconditionally
    (as long as there is at least one deferrable tool — there's no point
    swapping a no-op). ``"auto"`` activates when the deferrable schemas
    would consume ``threshold_pct`` of context or more.
    r    Fr   r$   Ti N  r)   )r   r   r   )r   r   r   threshold_tokenss       r7   should_activater      s~     ~uAu~t +^q00 !F**>V-AE-IJKK 000r9   c                  h    e Zd ZU dZded<   ded<   ded<   ded<   ded<    ee	          Zd
ed<   dS )CatalogEntryzEOne deferrable tool, in a form the bridge tools can search and serve.r   r\   descriptionDict[str, Any]schemasourcesource_name)default_factory	List[str]_tokensN)r:   r;   r<   r=   r>   r   listr   r@   r9   r7   r   r   
  sn         OOIIIKKK t444G444444r9   r   z[A-Za-z0-9]+textr   c                R    | sg S d t                               |           D             S )Nc                6    g | ]}|                                 S r@   )r0   ).0ts     r7   
<listcomp>z_tokenize.<locals>.<listcomp>  s     777!AGGII777r9   )	_TOKEN_REfindall)r   s    r7   	_tokenizer     s1     	77y00667777r9   rs   r   c                   |                      d          pi }|                     dd          }|                     dd          pd}|                     d          pi                      d          pi }d                    |                                          }|                    dd                              d	d                              d
d                              dd          }| d| d| S )uX  Build the search-text blob for a deferrable tool.

    Includes the tool name (with underscores broken into words so BM25 can
    match against query terms), the description, and the names of the
    top-level parameters. Schema bodies are deliberately excluded —
    indexing them adds noise without improving recall in our measurement.
    rn   r\   ro   r   
parameters
properties _.-ry   )r.   joinkeysreplace)rs   rt   r\   descparamsparam_names
name_wordss          r7   _entry_search_textr   !  s     

			!rB66&"D66-$$*Dvvl##)r..|<<BF((6;;==))Kc3''//S99AA#sKKSSTWY\]]J//4//+///r9   Tuple[str, str]c                    	 ddl m} |                    |           }|dS |j                            d          r	d|j        fS d|j        fS # t
          $ r Y dS w xY w)z=Return (source_kind, source_name) for a registered tool name.r   r_   N)otherro   ra   mcpplugin)rc   r`   rd   re   rf   rN   rg   s      r7   _classify_sourcer   3  s    	++++++""4((= ==##F++ 	*5=))%-((   }}s   A "A A 
AAList[CatalogEntry]c                P   g }| D ]}|                     d          pi }|                     dd          }|s2|                     dd          pd}t          |          \  }}t          |||||t          t	          |                              }|                    |           |S )zBuild the deferred-tool catalog from a tool-defs list.

    Caller is expected to pass only the deferrable subset (``classify_tools``
    returns it as the second element).
    rn   r\   ro   r   )r\   r   r   r   r   r   )r.   r   r   r   r   rp   )	rj   catalogrs   rt   r\   r   r   r   rh   s	            r7   build_catalogr   A  s     #%G  VVJ%2vvfb!! 	vvmR((.B.t44#04455
 
 
 	uNr9         ?      ?query_tokens
doc_tokensdoc_lengths	List[int]avg_dldoc_freqDict[str, int]n_docsk1bc           
        |sdS d}t          |          }	i }
|D ]}|
                    |d          dz   |
|<   | D ]}|                    |d          }|dk    rt          j        d||z
  dz   |dz   z  z             }|
                    |d          }|dk    r_||dz   z  ||d|z
  ||	z  t	          |d          z  z   z  z   z  }|||z  z  }|S )u  Standard BM25 score for one query against one document.

    Inlined small implementation rather than adding a dependency. Performance
    is fine — the catalog is bounded by N (tools) typically < 500, and we
    score against the in-memory tokens list.
    r(   r   r*   g      ?g      ?)r|   r.   r   logr2   )r   r   r   r   r   r   r   r   scoredldoc_tfr   qdfidftfnorms                    r7   _bm25_scorer   [  s     sE	ZBF ) )JJq!$$q(q		 	 	\\!Q77hqFRK#-"s(;;<<ZZ177R!V}R1q51r6C<L<L3L+L%M MNtLr9   r   r   querylimitc           	        | r|dk    rg S t          |          }|sg S d | D             }t          |          t          t          |          d          z  }i }| D ]7}t	          |j                  }|D ]}	|                    |	d          dz   ||	<   8t          |           }
g }| D ]8}t          ||j        ||||
          }|dk    r|                    ||f           9|sK|	                                }| D ]4}||j
        	                                v r|                    d|f           5|                    d d           d |d	|         D             S )
u  Return the top-``limit`` catalog entries for ``query`` by BM25.

    Falls back to a stable name-substring match when BM25 yields no hits
    above zero. That ensures a query like ``"github"`` against a catalog
    where every tool is named ``github_*`` still returns results — BM25
    can underperform when query and document share only one token that
    appears in every document (zero IDF).
    r   c                6    g | ]}t          |j                  S r@   )r|   r   )r   rT   s     r7   r   z"search_catalog.<locals>.<listcomp>  s     333a3qy>>333r9   r*   g?c                    | d         S )Nr   r@   )xs    r7   <lambda>z search_catalog.<locals>.<lambda>  s
    ad r9   T)keyreversec                    g | ]\  }}|S r@   r@   )r   r   rT   s      r7   r   z"search_catalog.<locals>.<listcomp>  s    )))$!QA)))r9   N)r   sumr2   r|   setr   r.   r   rp   r0   r\   sort)r   r   r   r   r   r   r   rT   seenr   r   scoredrh   sqls                  r7   search_catalogr   z  s     eqjj	U##L 	 437333KC$4$4a 8 88F!H 1 119~~ 	1 	1A",,q!,,q0HQKK	1\\F/1F & &em[& &* *q55MM1e*%%% ,[[]] 	, 	,EUZ%%''''sEl+++
KKNNDK111))&%.))))r9   deferred_countc           
     
   d|  dt            dt           d}dt           dt           d}dt            d	}d
t          |dddddddddgdddd
t           |dddddidgdddd
t          |ddddddddddgdddgS )u  Build the bridge tool schemas to inject in place of deferred tools.

    The schemas are intentionally short — every byte added here is a byte
    the user pays on every turn. Descriptions are tuned to be unambiguous
    about the call sequence the model should follow.
    zSearch zu additional tools that are loaded on demand. Returns up to ``limit`` matches with name and description. Follow with `z0` to load a tool's full parameter schema, then `zs` to invoke it. Tools listed at the top of this system prompt are already available and do not need to be searched.z4Load the full JSON schema for one tool returned by `z`. Required before `z'` if the tool's parameters are unknown.zhInvoke a deferred tool by name with the given arguments. Argument shape matches the tool's schema (see `zM`). Policy, hooks, and approvals run exactly as for any directly-listed tool.rn   objectstringzIKeywords describing the capability you need (e.g. 'create github issue').)typer   integerz/Maximum number of results to return. Default 5.)r   r   r   )r   r   requiredr\   r   r   )r   rn   r\   z-Exact tool name (as returned by tool_search).zExact tool name to invoke.z,Arguments for the tool, matching its schema.)r\   	argumentsr   )TOOL_DESCRIBE_NAMETOOL_CALL_NAMETOOL_SEARCH_NAME)r   desc_searchdesc_describe	desc_calls       r7   bridge_tool_schemasr     s   	N. 	N 	N#	N 	N  	N 	N 	N 	T?O 	T 	T*	T 	T 	T 
	E+=	E 	E 	E  (*$ %-+v" "
 %.+\" "	# 	# ")	  	
 	
, *,$$,+Z! !# "(	 	 	
 	
$ &($ %-+G! !
 %-+Y& &	# 	# "( 5  	
 	
O< <r9   c                  R    e Zd ZU dZded<   ded<   dZded<   dZded	<   dZded
<   dS )AssemblyResultz<Outcome of one assembly. Useful for tests and observability.rk   rj   r]   	activatedr   r   r   deferred_tokensr   N)r:   r;   r<   r=   r>   r   r   r   r@   r9   r7   r   r     sa         FF####OOONOr9   r   )r   r   Optional[ToolSearchConfig]c               f   |t                      }d | D             }t          |          \  }}|st          |d          S t          |          }t	          |||          s;t          |dt          |          |t          |pd|j        dz  z                      S t          t          |                    }||z   }t          |pd|j        dz  z            }	t          
                    dt          |          t          |          ||	           t          |d	t          |          ||	          S )
a#  Return the tool-defs list the model should actually see.

    When tool search is inactive (off, no deferrable tools, or below
    threshold), this is a passthrough. When active, MCP and plugin tools
    are stripped from the visible list and replaced with the three bridge
    tools. Core tools are *never* deferred regardless of config.

    Idempotent: calling with bridge tools already in the input is a no-op
    (they classify as non-core/non-deferrable but their names are reserved,
    so they are filtered out of the deferrable set).
    Nc                t    g | ]5}|                     d           pi                      d          t          v3|6S )rn   r\   )r.   rb   )r   rs   s     r7   r   z&assemble_tool_defs.<locals>.<listcomp>'  sO     T T TrFF:&&,"11&99ARRR RRRr9   F)rj   r   r   r)   )rj   r   r   r   r   zZtool_search activated: %d core/visible tools kept, %d deferred (~%d tokens, threshold ~%d)T)rK   ru   r   r   r   r|   r   r   r   rO   info)
rj   r   r   incomingrq   rr   r   bridgeresultr   s
             r7   assemble_tool_defsr     sk   " ~T TY T T TH )22GZ CEBBBB4Z@@6#4nEE 
z??- ."5A&:NQV:V!WXX
 
 
 	
 !Z11FvFN/aF4H54PQRR
KKdGc*oo'8:J  
 :))   r9   c                    | t           v S rD   )rb   )r\   s    r7   is_bridge_toolr   O  s    $$$r9   rh   c                L    | j         | j        | j        | j        pdd d         dS )Nro   i  r\   r   r   r   r   )rh   s    r7   _format_search_hitr  S  s5    
,()/R#6  r9   )r   argscurrent_tool_defsc          
     4   |t                      }t          |                     d          pd                                          }|st	          j        ddid          S |                     d          }||j        }n6t          d	t          |j	        t          ||j                                      }t          |          \  }}t          |          }t          |||
          }	t	          j        |t          |          d |	D             dd          S )z?Execute the ``tool_search`` bridge tool. Returns a JSON string.Nr   ro   errorzquery is requiredFrz   r   r*   )r   c                ,    g | ]}t          |          S r@   )r  )r   hs     r7   r   z(dispatch_tool_search.<locals>.<listcomp>t  s!    888a&q))888r9   )r   total_availablematches)rK   r   r.   r/   r}   r~   r   r2   r3   r   r4   ru   r   r   r|   )
r  r  r   r   	raw_limitr   r   rr   r   hitss
             r7   dispatch_tool_searchr  ]  s"   
 ~!!'R((..00E Nz7$78uMMMM!!I+As62IiId4e4effgg"#455MAzJ''G'5666D:w<<884888  	   r9   c          	     F   t          |                     d          pd                                          }|st          j        ddid          S t          |          st          j        dd| did          S t          |          \  }}|D ]v}|                    d	          pi }|                    d          |k    rDt          j        ||                    d
d          |                    di           dd          c S wt          j        dd| did          S )zAExecute the ``tool_describe`` bridge tool. Returns a JSON string.r\   ro   r  zname is requiredFr  'z' is not a deferrable tool. If you see it in the tools list already, call it directly; otherwise check the spelling against tool_search.rn   r   r   r   z<' is not currently available. Re-run tool_search to refresh.)r   r.   r/   r}   r~   ri   ru   )r  r  r\   r   rr   rs   rt   s          r7   dispatch_tool_describer  x  s    txx%2&&,,..D Mz7$67eLLLL"4(( z_D _ _ _

    	 ##455MAz # #VVJ%266&>>T!!:!vvmR88 ff\266  "	# # # # # # " :WTWWW   r9   c                    t                      }| D ]S}|                    d          pi                     dd          }|r$t          |          r|                    |           Tt	          |          S )ae  Return the set of deferrable tool names present in ``tool_defs``.

    ``tool_defs`` is expected to be the *pre-assembly* tool list for the
    current session's toolset scope (i.e. what
    ``get_tool_definitions(skip_tool_search_assembly=True)`` returns for the
    session's enabled/disabled toolsets). The resulting set is the universe of
    tools the session may legitimately reach through ``tool_call``. Used as a
    scoping gate by both the ``model_tools`` bridge dispatch and the
    ``tool_executor`` unwrap so a restricted-toolset session can never invoke
    an out-of-scope tool via the bridge.
    rn   r\   ro   )r   r.   ri   addrZ   )rj   namesrs   r\   s       r7   scoped_deferrable_namesr    sx     eeE  z""(b--fb99 	+D11 	IIdOOOUr9   3Tuple[Optional[str], Dict[str, Any], Optional[str]]c                   t          |                     d          pd                                          }|sdi dfS |t          v r	di d| dfS |                     d          }|i }t	          |t                     r:	 t          j        |          }n$# t
          j        $ r}di d| fcY d}~S d}~ww xY wt	          |t                    sdi d	fS t          |          s	di d
| dfS ||dfS )a?  Parse a ``tool_call`` invocation into (underlying_name, args, error_msg).

    Used by:
    * the dispatcher in ``model_tools.handle_function_call``,
    * the display layer (so the activity feed shows the underlying tool),
    * the trajectory recorder.

    On parse error, returns ``(None, {}, error_message)``.
    r\   ro   Nz$tool_call requires a 'name' argumentztool_call cannot invoke 'z' (it is itself a bridge tool)r   z)tool_call 'arguments' is not valid JSON: z'tool_call 'arguments' must be an objectr  z|' is not a deferrable tool. If it appears in the model-facing tools list already, call it directly instead of via tool_call.)
r   r.   r/   rb   r,   r}   loadsJSONDecodeErrorr-   ri   )r  r\   raw_argsrT   s       r7   resolve_underlying_callr    sd    txx%2&&,,..D @R???   RYTYYYYYxx$$H(C   M	Mz(++HH# 	M 	M 	MLLLLLLLLLL	Mh%% CRBBB"4(( 
RG G G G
 	
 4s   ?B B5#B0*B50B5)r   r   r   rb   r   r   r   rK   ri   ru   r   r   r   r   r   r   r   r  r  r  r  )rA   r   rB   r   r   r   )rA   r   rB   r   r   r   )r   r   )r   rU   )r\   r   r   r]   )rj   rk   r   rl   )rj   rv   r   r   )r   r   r   r   r   r   r   r]   )r   r   r   r   )rs   r   r   r   )r\   r   r   r   )rj   rk   r   r   )r   r   )r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   )r   )r   r   r   r   r   r   r   r   )r   r   r   rk   )rj   rk   r   r   r   r   r   r   )rh   r   r   r   )r  r   r  rk   r   r   r   r   )r  r   r  rk   r   r   )rj   rk   r   rU   )r  r   r   r  )5r=   
__future__r   r}   loggingr   redataclassesr   r   typingr   r   r   r	   r
   r   	getLoggerrO   r   r   r   rZ   rb   r   r   r4   r1   rK   r[   ri   ru   r   r   r   compiler   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r  __all__r@   r9   r7   <module>r#     s   6 # " " " " "    				 ( ( ( ( ( ( ( ( = = = = = = = = = = = = = = = =		.	/	/ ! $ I/1C^TUU   $2
 2
 2
 2
 2
 2
 2
 2
j      / / / /&
 
 
 
   4   89 9 9 9"1 1 1 1@ 
5 
5 
5 
5 
5 
5 
5 
5 BJ''	8 8 8 80 0 0 0$      : -1    >(* (* (* (* (*`T T T Tx         %))-	6 6 6 6 6 6|% % % %    ?C     6   8   (       B  r9   