
    `gjj                    P   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d0dZd1dZd2dZd3dZ 	 	 	 	 	 	 	 	 d4dZ!e G d d             Z" ejF                  d      Z$d5dZ%d6dZ&d7dZ'd8dZ(	 d9	 	 	 	 	 	 	 	 	 	 	 	 	 d:dZ)d;d<dZ*d=d Z+e G d! d"             Z,ddd#	 	 	 	 	 	 	 d>d$Z-d1d%Z.d?d&Z/dd'	 	 	 	 	 d@d(Z0	 	 	 	 dAd)Z1dBd*Z2dCd+Z3g d,Z4y)Du  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                  L    e Zd ZU dZded<   ded<   ded<   ded<   edd	       Zy
)ToolSearchConfigzDResolved, validated tool-search configuration for a single assembly.strenabledfloatthreshold_pctintsearch_default_limitmax_search_limitc                L   |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        |j                  d	d            j	                         j                         }|d
v rd}n|dv rd}n	|dv r|}nd}t        |j                  d      d      }t        dt        d|            }t        dt        dt        |j                  d      d                  }t        dt        |t        |j                  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)clsrawenabled_rawr   r   r   r   s          D/root/.hermes/venv/lib/python3.12/site-packages/tools/tool_search.pyfrom_rawzToolSearchConfig.from_rawH   sS    $;vT,-D D%<uD,-D D#t$vT,-D D #'')V45;;=CCE..G00G11!GG#CGGO$<dCCUM!:;q#b)CGG<N4OQS*T"UV"1c*:*3CGG<R4SUV*W'Y  Z '!5-	
 	
    N)r3   r   returnz'ToolSearchConfig')__name__
__module____qualname____doc____annotations__classmethodr6    r7   r5   r   r   ?   s.    NL)
 )
r7   r   c                H    	 t        |       S # t        t        f$ r |cY S w xY wN)r   	TypeError
ValueErrorvaluefallbacks     r5   r1   r1   u   s*    5zz"    
 !!c                H    	 t        |       S # t        t        f$ r |cY S w xY wrA   )r   rB   rC   rD   s     r5   r.   r.   |   s*    U|z" rG   c                 x   	 ddl m}   |        xs i }t        |j                  d      t              r|j                  d      ni }t        |t              si }t
        j                  |j                  d            S # t        $ r5}t        j                  d|       t
        j                  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.configrJ   r)   r+   r*   r   r6   	Exceptionloggerdebug)_loadcfg	tools_cfges       r5   rJ   rJ      s    	/:gm(237773CT(JCGGG$PR	)T*I((})EFF /<a@((../s   A8A; ;	B9*B4.B94B9c                 X    	 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)toolsetsrV   	frozensetrM   rU   s    r5   _core_tool_namesrY      s-    /+,, {s    ))c                    | t         v ry| t               v ry	 ddlm} |j	                  |       }|y|j
                  j                  d      ryy# t        $ r Y y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   registrymcp-T)BRIDGE_TOOL_NAMESrY   tools.registryr\   	get_entrytoolset
startswithrM   namer\   entrys      r5   is_deferrable_tool_namerf      si       !!
+""4(===##F+ s   A A 	AAc                    g }g }| D ]`  }|j                  d      xs i }|j                  dd      }|t        v r3t        |      r|j                  |       P|j                  |       b ||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.
    functionrd    )r+   r^   rf   append)	tool_defsvisible
deferrabletdfnrd   s         r5   classify_toolsrp      s}     %'G')J 
VVJ%2vvfb!$$ "4(b!NN2
 Jr7   c           	         d}| D ]'  }	 |t        t        j                  |dd            z  }) t        t        j                  |t        z              S # t        t        f$ r |t        t        |            z  }Y ww xY w)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dumpsrB   rC   r   r   mathceilCHARS_PER_TOKEN)rk   total_charsrn   s      r5   estimate_tokens_from_schemasr}      s|     K (	(3tzz"5ZXYYK(
 tyy6788 :& 	(3s2w<'K	(s   $A&A=<A=c                    | j                   dk(  ry|dk  ry| j                   dk(  ry|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   )configdeferrable_tokenscontext_lengththreshold_tokenss       r5   should_activater      sk     ~~A~~^q0 !F**>V-A-AE-IJK 000r7   c                  `    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<   y)CatalogEntryzEOne deferrable tool, in a form the bridge tools can search and serve.r   rd   descriptionDict[str, Any]schemasourcesource_name)default_factory	List[str]_tokensN)r9   r:   r;   r<   r=   r   listr   r?   r7   r5   r   r   
  s2    O
IK t4GY4r7   r   z[A-Za-z0-9]+c                v    | sg S t         j                  |       D cg c]  }|j                          c}S c c}w rA   )	_TOKEN_REfindallr-   )textts     r5   	_tokenizer     s/    	(0067!AGGI777s   6c                   | j                  d      xs i }|j                  dd      }|j                  dd      xs d}|j                  d      xs i j                  d      xs i }dj                  |j                               }|j                  dd      j                  d	d      j                  d
d      j                  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.
    rh   rd   ri   r   
parameters
properties _.-rs   )r+   joinkeysreplace)rn   ro   rd   descparamsparam_names
name_wordss          r5   _entry_search_textr   !  s     

		!rB66&"D66-$*Dvvl#)r..|<BF((6;;=)Kc3'//S9AA#sKSSTWY\]J\4&+//r7   c                    	 ddl m} |j                  |       }|y|j                  j	                  d      rd|j                  fS d|j                  fS # t
        $ r Y yw xY w)z=Return (source_kind, source_name) for a registered tool name.r   r[   )otherri   r]   mcpplugin)r_   r\   r`   ra   rb   rM   rc   s      r5   _classify_sourcer   3  sd    	+""4(= ==##F+5==))%--(( s   A (A A 	AAc                   g }| D ]  }|j                  d      xs i }|j                  dd      }|s-|j                  dd      xs d}t        |      \  }}t        |||||t        t	        |                  }|j                  |        |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).
    rh   rd   ri   r   )rd   r   r   r   r   r   )r+   r   r   r   r   rj   )	rk   catalogrn   ro   rd   r   r   r   re   s	            r5   build_catalogr   A  s     #%G VVJ%2vvfb!vvmR(.B.t4#045
 	u  Nr7   c           
     |   |syd}t        |      }	i }
|D ]  }|
j                  |d      dz   |
|<    | D ]  }|j                  |d      }|dk(  rt        j                  d||z
  dz   |dz   z  z         }|
j                  |d      }|dk(  rW||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      ?)rv   r+   ry   logr/   )query_tokens
doc_tokensdoc_lengthsavg_dldoc_freqn_docsk1bscoredldoc_tfr   qdfidftfnorms                    r5   _bm25_scorer   [  s     E	ZBF )JJq!$q(q	) 	\\!Q7hhqFRK#-"s(;;<ZZ17R!V}R1q51r6C<L3L+L%M MNt	 Lr7   c           	        | r|dk  rg S t        |      }|sg S | D cg c]  }t        |j                         }}t        |      t	        t        |      d      z  }i }| D ]6  }t        |j                        }|D ]  }	|j                  |	d      dz   ||	<    8 t        |       }
g }| D ]5  }t        ||j                  ||||
      }|dkD  s#|j                  ||f       7 |sG|j                         }| D ]2  }||j                  j                         v s |j                  d|f       4 |j                  d d       |d| D cg c]  \  }}|	 c}}S c c}w c c}}w )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   r'   g?c                    | d   S )Nr   r?   )xs    r5   <lambda>z search_catalog.<locals>.<lambda>  s
    ad r7   T)keyreverseN)r   rv   r   sumr/   setr+   r   rj   r-   rd   sort)r   querylimitr   rS   r   r   r   seenr   r   scoredre   sqlr   s                   r5   search_catalogr   z  sq    eqj	U#L	 ,33a3qyy>3K3C$4a 88F!H 1199~ 	1A",,q!,q0HQK	11 \F/1F &emm[& &*q5MM1e*%	& [[] 	,EUZZ%%''sEl+	, KKNDK1 %.)$!QA))1 40 *s   EEc           
         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.rh   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rd   r   r   )r   rh   rd   z-Exact tool name (as returned by tool_search).zExact tool name to invoke.z,Arguments for the tool, matching its schema.)rd   	argumentsr   )TOOL_DESCRIBE_NAMETOOL_CALL_NAMETOOL_SEARCH_NAME)deferred_countdesc_searchdesc_describe	desc_calls       r5   bridge_tool_schemasr     s?    .! "#$ %  !N	N  ??O>P Q*++R	T 
	++=*> ?E	E  (*$ %-+v"
 %.+\"	# ")		
, *,$$,+Z!# "(		
$ &($ %-+G!
 %-+Y&	# "( 5	
O< <r7   c                  P    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
<   y)AssemblyResultz<Outcome of one assembly. Useful for tests and observability.List[Dict[str, Any]]rk   bool	activatedr   r   r   deferred_tokensr   N)r9   r:   r;   r<   r=   r   r   r   r?   r7   r5   r   r     s/    F##ONCOScr7   r   )r   r   c               d   |
t               }| D cg c].  }|j                  d      xs i j                  d      t        vr|0 }}t        |      \  }}|st	        |d      S t        |      }t        |||      s6t	        |dt        |      |t        |xs d|j                  dz  z              S t        t        |            }||z   }	t        |xs d|j                  dz  z        }
t        j                  dt        |      t        |      ||
       t	        |	d	t        |      ||
      S c c}w )
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).
    rh   rd   F)rk   r   r   r&   )rk   r   r   r   r   zZtool_search activated: %d core/visible tools kept, %d deferred (~%d tokens, threshold ~%d)T)rJ   r+   r^   rp   r   r}   r   rv   r   r   r   rN   info)rk   r   r   rn   incomingrl   rm   r   bridgeresultr   s              r5   assemble_tool_defsr     sK   " ~ ' TrFF:&,"11&9ARR  TH T )2GZEBB4Z@6#4nEz?- ."5A&:N:NQV:V!WX
 	
 !Z1FvFN/aF4H4H54PQR
KKdGc*o'8:J
 :)) 5Ts   3D-c                    | t         v S rA   )r^   )rd   s    r5   is_bridge_toolr   O  s    $$$r7   c                n    | j                   | j                  | j                  | j                  xs dd d dS )Nri   i  rd   r   r   r   r   )re   s    r5   _format_search_hitr   S  s8    

,,(())/R#6 r7   )r   c          
        |
t               }t        | j                  d      xs d      j                         }|st	        j
                  ddid      S | j                  d      }||j                  }n4t        dt        |j                  t        ||j                                    }t        |      \  }}t        |      }t        |||	      }	t	        j
                  |t        |      |	D 
cg c]  }
t        |
       c}
d
d      S c c}
w )z?Execute the ``tool_search`` bridge tool. Returns a JSON string.r   ri   errorzquery is requiredFrt   r   r'   )r   )r   total_availablematches)rJ   r   r+   r,   rw   rx   r   r/   r0   r   r1   rp   r   r   rv   r   )argscurrent_tool_defsr   r   	raw_limitr   r   rm   r   hitshs              r5   dispatch_tool_searchr   ]  s    
 ~!'R(..0Ezz7$78uMM!I++As622IiIdId4efg"#45MAzJ'G'56D::w<378a&q)8 	  9s   -D
c          	        t        | j                  d      xs d      j                         }|st        j                  ddid      S t        |      st        j                  dd| did      S t        |      \  }}|D ]h  }|j                  d	      xs i }|j                  d      |k(  s-t        j                  ||j                  d
d      |j                  di       dd      c S  t        j                  dd| did      S )zAExecute the ``tool_describe`` bridge tool. Returns a JSON string.rd   ri   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.rh   r   r   r   z<' is not currently available. Re-run tool_search to refresh.)r   r+   r,   rw   rx   rf   rp   )r   r   rd   r   rm   rn   ro   s          r5   dispatch_tool_describer   x  s    txx%2&,,.Dzz7$67eLL"4(zzD6 _ _

  	 ##45MAz #VVJ%266&>T!::!vvmR8 ff\26 "	# ## ::1TFVW r7   c                    t               }| D ]G  }|j                  d      xs i j                  dd      }|s+t        |      s7|j                  |       I t	        |      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.
    rh   rd   ri   )r   r+   rf   addrX   )rk   namesrn   rd   s       r5   scoped_deferrable_namesr    s]     eE z"(b--fb9+D1IIdO Ur7   c                   t        | j                  d      xs d      j                         }|sdi dfS |t        v r	di d| dfS | j                  d      }|i }t	        |t               r	 t        j                  |      }t	        |t              sdi d	fS t        |      s	di d
| dfS ||dfS # t
        j                  $ r}di d| fcY d}~S d}~ww xY w)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)``.
    rd   ri   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,   r^   r)   rw   loadsJSONDecodeErrorr*   rf   )r   rd   raw_argsrS   s       r5   resolve_underlying_callr    s    txx%2&,,.DR???  R4TF:XYYYxx$H(C 	Mzz(+H h%RBBB"4(Rv G G
 	
 4 ## 	MHLLL	Ms   +B. .CCCC)r   r   r   r^   r   r   r   rJ   rf   rp   r}   r   r   r   r   r   r   r   r   r  r  )rE   r   rF   r   r8   r   )rE   r   rF   r   r8   r   )r8   r   )r8   frozenset[str])rd   r   r8   r   )rk   r   r8   z1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]])rk   zIterable[Dict[str, Any]]r8   r   )r   r   r   r   r   Optional[int]r8   r   )r   r   r8   r   )rn   r   r8   r   )rd   r   r8   zTuple[str, str])rk   r   r8   List[CatalogEntry])g      ?g      ?)r   r   r   r   r   z	List[int]r   r   r   zDict[str, int]r   r   r   r   r   r   r8   r   )r   )r   r	  r   r   r   r   r8   r	  )r   r   r8   r   )rk   r   r   r  r   Optional[ToolSearchConfig]r8   r   )re   r   r8   r   )r   r   r   r   r   r
  r8   r   )r   r   r   r   r8   r   )rk   r   r8   r  )r   r   r8   z3Tuple[Optional[str], Dict[str, Any], Optional[str]])5r<   
__future__r   rw   loggingry   redataclassesr   r   typingr   r   r   r	   r
   r   	getLoggerrN   r   r   r   rX   r^   r{   r   r1   r.   rJ   rY   rf   rp   r}   r   r   compiler   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  __all__r?   r7   r5   <module>r     s  6 #    	 ( = =			.	/ ! $ /1C^TU   $2
 2
 2
j/&
489"111 "1 
	1@ 
5 
5 
5 BJJ'	80$: -1&05(25  %* 6;>(*`Tx    %))-	6#6 "6 '	6
 6|% ?C,@ "< HK6.BGJ8( Br7   