
    epj                       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 ddl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dqdZdrdZdsdZdtdZdudZ dvd"Z!dwd$Z"dxd)Z#dyd*Z$e G d+ d,                      Z% ej&        d-          Z'dzd0Z(d{d3Z)d|d5Z*d}d7Z+	 d~ddDZ,dddIZ- ej&        dJ          Z.dddNZ/ddPZ0dQdRddVZ1dQdRddXZ2	 	 ddd]Z3e G d^ d_                      Z4ddd`ddbZ5dudcZ6ddeZ7ddfZ8ddgddjZ9ddkZ:ddlZ;ddmZ<ddoZ=g dpZ>dS )uq  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.
* Tiered disclosure (July 2026 plan): the moment ANY deferrable (MCP/plugin)
  tools are present, they hide behind the bridge. What scales with catalog
  size is the *listing*, not the activation decision:
    - Tier 0 — no MCP/plugin tools: pure passthrough, everything eager.
    - Tier 1 — deferred tools whose catalog listing fits the listing budget
      (``min(threshold_pct`` of context — default 5% — ``, listing_max_tokens)``):
      bridge + skills-style listing (name + short description per tool),
      degrading to a names-only listing when the full form is over budget.
    - Tier 2 — per-tool listing over budget even names-only (e.g.
      Cloudflare's flat API surface, ~3,300 tools whose names alone are
      ~32K tokens): bare bridge + a one-line-per-server summary (server
      name + tool count) so the model still knows WHICH domains are
      reachable; individual tools are discoverable only via ``tool_search``.
* 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Tuple)
tool_errorztools.tool_searchtool_searchtool_describe	tool_callg      @T)frozenc                  p    e Zd ZU dZded<   ded<   ded<   ded<   d	Zded
<   dZ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autolisting  listing_max_tokens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                              }t          |                    dd                                                                                    }|d
v rd}n|dv rd}n	|dv r|}nd}t          dt          d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.
        Tr   g      @      )r   r   r   r   Foffr   )true1yeson)false0no)r   r(   r$   r                 Y@   2   r   r   r      i`  r   r   )r   r   r   r   r   r   )

isinstancedictr   getstriplower_safe_floatmaxmin	_safe_int)
clsr   enabled_rawr   r   r   r   listing_rawr   r   s
             7/home/thesage/.hermes/hermes-agent/tools/tool_search.pyfrom_rawzToolSearchConfig.from_rawd   s    $;;3vS,-D D D D%<<3uC,-D D D D#t$$ 	D3vS,-D D D D #'')V4455;;==CCEE...GG000GG111!GGG#CGGO$<$<cBBCUM!:!:;;q#b)CGG<N4O4OQS*T*T"U"UVV"1c*:*3CGG<R4S4SUV*W*W'Y 'Y  Z  Z #'')V4455;;==CCEE...GG000GG111!GGG c%377CW;X;XZ^1_1_&`&`aas'!5-1
 
 
 	
    N)r   r   r   r    )	__name__
__module____qualname____doc____annotations__r   r   classmethodr>    r?   r=   r   r   K   s         NNLLL  G #""""6
 6
 6
 [6
 6
 6
r?   r   valuer   fallbackr   r   c                T    	 t          |           S # t          t          f$ r |cY S w xY wN)r   	TypeError
ValueErrorrG   rH   s     r=   r9   r9      s<    5zzz"       ''r   c                T    	 t          |           S # t          t          f$ r |cY S w xY wrJ   )r   rK   rL   rM   s     r=   r6   r6      s<    U||z"   rN   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.configrQ   r1   r3   r2   r   r>   	Exceptionloggerdebug)_loadcfg	tools_cfges       r=   rQ   rQ      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)toolsetsr^   	frozensetrT   r]   s    r=   _core_tool_namesra      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_NAMESra   tools.registryrf   	get_entrytoolset
startswithrT   rb   rf   entrys      r=   is_deferrable_tool_namero      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.
    functionrb    )r3   rh   ro   append)rp   visible
deferrabletdfnrb   s         r=   classify_toolsr{      s     %'G')J 
 
VVJ%2vvfb!!$$$ "4(( 	b!!!!NN2Jr?   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dumpsrK   rL   r   r   mathceilCHARS_PER_TOKEN)rp   total_charsry   s      r=   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                0    | j         dk    rdS |dk    rdS dS )uE  Decide whether tool search should activate for the current assembly.

    ``"off"`` skips unconditionally. ``"on"`` and ``"auto"`` activate whenever
    at least one deferrable tool exists (there's no point swapping a no-op).

    Tiered-disclosure semantics (July 2026): the presence of ANY MCP/plugin
    tool activates the bridge — schemas always defer. What the threshold now
    controls is the *listing budget* (see :func:`listing_token_budget`), not
    activation. ``context_length`` is retained in the signature for
    backward compatibility with existing callers.
    r$   Fr   T)r   )r   r   r   s      r=   should_activater     s+      ~uAu4r?   c                    |r!|dk    rt          || j        dz  z            }nd}t          dt          | j        |                    S )zEffective token budget for the embedded catalog listing.

    ``min(listing_max_tokens, threshold_pct% of context)``. Without a known
    context size, the percentage leg falls back to a fixed 10K cutoff
    (5% of a typical 200K window).
    r   r-   i'  )r   r   r7   r8   r   )r   r   pct_legs      r=   listing_token_budgetr   *  sT      .1,,n(<u(DEFFq#f/99:::r?   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   rb   descriptionDict[str, Any]schemasourcesource_name)default_factory	List[str]_tokensN)r@   rA   rB   rC   rD   r   listr   rF   r?   r=   r   r   @  sn         OOIIIKKK t444G444444r?   r   z[A-Za-z0-9]+textr   c                R    | sg S d t                               |           D             S )Nc                6    g | ]}|                                 S rF   )r5   ).0ts     r=   
<listcomp>z_tokenize.<locals>.<listcomp>T  s     777!AGGII777r?   )	_TOKEN_REfindall)r   s    r=   	_tokenizer   Q  s1     	77y00667777r?   ry   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.
    rt   rb   ru   r   
parameters
properties _.-r   )r3   joinkeysreplace)ry   rz   rb   descparamsparam_names
name_wordss          r=   _entry_search_textr   W  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//+///r?   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   re   N)otherru   rg   mcpplugin)ri   rf   rj   rk   rl   rT   rm   s      r=   _classify_sourcer   i  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).
    rt   rb   ru   r   )rb   r   r   r   r   r   )r3   r   r   r   r   rv   )	rp   catalogry   rz   rb   r   r   r   rn   s	            r=   build_catalogr   w  s     #%G  VVJ%2vvfb!! 	vvmR((.B.t44#04455
 
 
 	uNr?         ?      ?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   r3   r   logr7   )r   r   r   r   r   r   r   r   scoredldoc_tfr   qdfidftfnorms                    r=   _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Lr?   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 rF   )r   r   )r   rZ   s     r=   r   z"search_catalog.<locals>.<listcomp>  s     333a3qy>>333r?   r.   g?c                    | d         S )Nr   rF   )xs    r=   <lambda>z search_catalog.<locals>.<lambda>  s
    ad r?   T)keyreversec                    g | ]\  }}|S rF   rF   )r   r   rZ   s      r=   r   z"search_catalog.<locals>.<listcomp>  s    )))$!QA)))r?   N)r   sumr7   r   setr   r3   r   rv   r5   rb   sort)r   r   r   r   r   r   r   rZ   seenr   r   scoredrn   sqls                  r=   search_catalogr     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))&%.))))r?   z[.!?\n]<   r   	max_charsc                   d                     | pd                                          }|sdS t                              |          }|r?|d|                                ||                                         dk    rdndz            }t          |          |k    r|S |d|         }d|v r|                    dd          d         }|                    d          dz   S )	a  First sentence of a tool description, clipped to ``max_chars``.

    Mirrors the skills-listing convention: one terse line per capability.
    Whitespace is collapsed; a hard clip never cuts mid-word unless the
    first word itself exceeds the budget.
    r   ru   Nr   r.   r   z,;: u   …)r   split_SENTENCE_END_REsearchstartr   rsplitrstrip)r   r   r   mclippeds        r=   _short_descr     s     88[&B--//00D r%%A GEQWWYYtAGGII#'='=!!1EEF
4yyI:I:G
g~~..a((+>>&!!E))r?   r   c                L    | pd}|                     d          r
|dd         }|S )zLHuman-facing group heading for a toolset, e.g. ``mcp-github`` -> ``github``.r   rg      N)rl   )r   labels     r=   _listing_group_labelr     s4    "7E abb	Lr?   r   
max_tokensrx   r   Optional[str]c               .    t          | |          \  }}|S )uN  Render a skills-style manifest of the deferred catalog.

    One line per tool — ``name: short description`` — grouped under a
    heading per source (MCP server / plugin toolset), exactly like the
    bundled-skills listing in the system prompt:

        github tools: (44)
        - create_issue: Open a new issue in a GitHub repository.
        - merge_pull_request: Merge an open pull request.
        ...

    Ordering is deterministic (groups and tools sorted by name) so the
    rendered block is byte-stable across assemblies of the same catalog —
    this keeps the request prefix cacheable across turns.

    Token-budget fallbacks (cheap chars/4 estimate, same rule as the
    activation gate):
      1. full listing (names + short descriptions)
      2. names-only listing, still grouped
      3. server-level summary — one line per MCP server / plugin toolset
         (name + tool count), so the model always knows WHICH domains are
         reachable through the bridge even when per-tool names don't fit
      4. ``None`` — only when the summary itself exceeds the budget
    r   )build_catalog_listing_with_form)rx   r   r   _forms       r=   build_catalog_listingr      s    : 2*TTTKD%Kr?   Tuple[Optional[str], str]c          
     X   | sdS i | D ]}|                     d          pi }|                     dd          }|s2t          |          \  }}t          |dk    r|nd          }                    |g                               |t          |                     dd                    f           sdS dfddt           dt           dd fd}d!fd}	d D             }
 |	 ||
                    r ||
          dfS d D             }
 |	 ||
                    r ||
          dfS t          fd          }|D ]Y}d|
|<    |	 ||
                    r>t          d |

                                D                       rdnd} ||
          |fc S ZdS )"u  Like :func:`build_catalog_listing` but also reports the form used.

    Returns ``(text, form)`` where ``form`` is ``"full"`` (names + short
    descriptions), ``"names"`` (names-only fallback), ``"mixed"`` (per-server
    degradation: small servers keep per-tool lines, oversized servers
    collapse to a name + tool-count summary line), ``"groups"`` (every
    server summarized), or ``"none"`` (over budget in every form).

    Degradation is PER SERVER, not global: one huge server (Cloudflare's
    3,320 flat tools) must not cost a small co-attached server (Linear's 24)
    its listing. Greedy fit, smallest rendered group first, is deterministic
    for a given catalog — byte-stable across assemblies, cache-safe.
    )Nnonert   rb   ru   r   r   r   r   moder   c                   t          |                    }|dk    r|  dt          |           dt           dS |  dt          |           dg}|dk    r+|D ]'\  }}|                    |rd| d	| nd|            (n2|                    d
                    d |D                                  d                    |          S )z>Render one server's block. mode: 'full' | 'names' | 'summary'.summaryz (u+    tools — names not listed; discover via `z`)z tools (z):fullz- z: , c              3      K   | ]	\  }}|V  
d S rJ   rF   )r   rb   r   s      r=   	<genexpr>zHbuild_catalog_listing_with_form.<locals>.render_group.<locals>.<genexpr>N  s&      "="=GD!4"="="="="="=r?   
)sortedr   TOOL_SEARCH_NAMErv   r   )r   r  rR   linesrb   r   groupss         r=   render_groupz5build_catalog_listing_with_form.<locals>.render_groupC  s   ve}%%9 : :E

 : :%5: : : ;113u::11126>># K K
dDI0$00$000k4kkJJJJK LL"="=u"="="===>>>yyr?   z)Deferred tool catalog (call schemas via `z`, invoke via `z`):modesDict[str, str]c                l     d                     g fdt                    D             z             S )Nr
  c                4    g | ]} ||                   S rF   rF   )r   lblr  r  s     r=   r   zEbuild_catalog_listing_with_form.<locals>.assemble.<locals>.<listcomp>U  s?     %@ %@ %@), &2\#uSz%B%B %@ %@ %@r?   )r   r  )r  r  headerr  s   `r=   assemblez1build_catalog_listing_with_form.<locals>.assembleT  s`    yy& %@ %@ %@ %@ %@06v%@ %@ %@ @ A A 	Ar?   r   rc   c                ^    t          j        t          |           t          z            k    S rJ   )r   r   r   r   )r   r   s    r=   fitsz-build_catalog_listing_with_form.<locals>.fitsX  s#    yT_455CCr?   c                    i | ]}|d S )r  rF   r   r  s     r=   
<dictcomp>z3build_catalog_listing_with_form.<locals>.<dictcomp>\  s    +++SS&+++r?   r  c                    i | ]}|d S )namesrF   r  s     r=   r  z3build_catalog_listing_with_form.<locals>.<dictcomp>a  s    ,,,cS',,,r?   r  c                <    t           | d                     | fS )Nr  )r   )r  r  s    r=   r   z1build_catalog_listing_with_form.<locals>.<lambda>i  s$    s<<W3M3M/N/N.NPS-T r?   )r   r  c              3  "   K   | ]
}|d k    V  dS )r  NrF   )r   r   s     r=   r	  z2build_catalog_listing_with_form.<locals>.<genexpr>m  s&      "J"Ja1	>"J"J"J"J"J"Jr?   r  mixed)r   r   r  r   r   r   )r  r  r   r   )r   r   r   rc   )r3   r   r   
setdefaultrv   r   TOOL_DESCRIBE_NAMETOOL_CALL_NAMEr  allvalues)rx   r   ry   rz   rb   r   r   r   r  r  r  by_sizer  formr  r  r  s    `            @@@r=   r   r   !  s   $  |/1F \ \VVJ%2vvfb!! 	.t44$Fg4E4E[[7SS%$$++T;rvvmUW?X?X3Y3Y,Z[[[[ |           I$I I5CI I IFA A A A A A A AD D D D D D ,+F+++EtHHUOO 'x&& -,V,,,EtHHUOO (x'' V!T!T!T!TUUUG ) )c
4   	)""J"J5<<>>"J"J"JJJW88PWD8E??D((((	)
 <r?   ru   deferred_countr   listing_formc           
        d|  dt            dt           d}|r|dk    r	|d|z   z  }n4|r2|dt            dt           d	z  }|d
k    r|dt           dz  }|d|z   z  }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 )%uk  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.

    When ``listing`` is provided (see :func:`build_catalog_listing`), it is
    embedded in the ``tool_search`` description so every deferred capability
    stays *visible* by name — the skills-listing pattern — closing the
    "model doesn't know what it doesn't know" gap while full parameter
    schemas remain deferred. ``listing_form`` selects the framing: per-tool
    forms ("full"/"names") tell the model it may skip the search when it
    sees the exact name; the server-summary form ("groups") tells it which
    DOMAINS are reachable and that search is mandatory for tool discovery.
    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.r  u  

The servers below are connected and their tools ARE available through this bridge. For any request in these domains, search here FIRST — do not claim the capability is unavailable and do not substitute a generic tool (terminal/browser) without searching.

u{   

Every deferred capability is listed below. If a tool name appears here, do NOT claim it is unavailable — load it with `z	` (skip `z'` when you already see the exact name).r   uP    For servers marked 'names not listed', the tools exist too — find them with `z(` before concluding anything is missing.z

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.rt   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rb   r   r   )r-  rt   rb   z-Exact tool name (as returned by tool_search).zExact tool name to invoke.z,Arguments for the tool, matching its schema.)rb   	argumentsr1  )r"  r#  r  )r(  r   r)  desc_searchdesc_describe	desc_calls         r=   bridge_tool_schemasr5  t  sQ   *	N. 	N 	N#	N 	N  	N 	N 	N   (<8++  '	'	
 
 (+"+ +-=+ + +	
 7""2+;2 2 2K
 	v''	T?O 	T 	T*	T 	T 	T 
	E+=	E 	E 	E  (*$ %-+v" "
 %.+\" "	# 	# ")	  	
 	
, *,$$,+Z! !# "(	 	 	
 	
$ &($ %-+G! !
 %-+Y& &	# 	# "( 5  	
 	
O< <r?   c                  n    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Zded<   dZ	ded<   dS )AssemblyResultz<Outcome of one assembly. Useful for tests and observability.rq   rp   rc   	activatedr   r   r(  deferred_tokensthreshold_tokenstierr  r   r)  N)
r@   rA   rB   rC   rD   r(  r9  r:  r;  r)  rF   r?   r=   r7  r7    s         FF####OOONO
 DMMMMLr?   r7  )r   r   Optional[ToolSearchConfig]c                  |t                      }d | D             }t          |          \  }}|st          |d          S t          |          }t	          |||          s<t          |dt          |          |t          |pd|j        dz  z            d          S d}d}t          ||          }	|j	        d	k    rt          ||	
          \  }}t          t          |          ||          }
||
z   }|dv rdnd}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 )rt   rb   )r3   rh   )r   ry   s     r=   r   z&assemble_tool_defs.<locals>.<listcomp>  sO     T T TrFF:&&,"11&99ARRR RRRr?   F)rp   r8  r   r-   )rp   r8  r(  r9  r:  r;  r  r$   r   )r   r)  )r  r  r   r.      zutool_search activated (tier %d): %d core/visible tools kept, %d deferred (~%d tokens), listing %s (budget ~%d tokens)T)rp   r8  r(  r9  r:  r;  r)  )rQ   r{   r7  r   r   r   r   r   r   r   r   r5  rU   info)rp   r   r   incomingrw   rx   r   r   r)  listing_budgetbridgeresultr;  s                r=   assemble_tool_defsrE    s   " ~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
 
 
 	
 GL)&.AAN~ ?>!3 !3 !3 Z'.:< < <FvF
  :::11D
KK	7c'llC
OO->n	   :)'!   r?   c                    | t           v S rJ   )rh   )rb   s    r=   is_bridge_toolrG  R  s    $$$r?   rn   c                L    | j         | j        | j        | j        pdd d         dS )Nru   i  rb   r   r   r   rI  )rn   s    r=   _format_search_hitrJ  V  s5    
,()/R#6  r?   c                    i | D ]2}t          |j                  }                    |d          dz   |<   3fdt                    D             S )aY  Return a compact, deterministic summary of connected deferred sources.

    Included only when search returns no matches. This gives the model enough
    evidence to retry with a source/action query instead of treating a lexical
    miss as proof that the capability is unavailable, without adding anything
    to the fixed per-turn prompt.
    r   r.   c                &    g | ]}||         d S ))rb   
tool_countrF   )r   rb   countss     r=   r   z-_available_source_summary.<locals>.<listcomp>n  s5        VD\22  r?   )r   r   r3   r  )r   rn   r   rN  s      @r=   _available_source_summaryrO  `  sz      F 1 1 %U%677

5!,,q0u   6NN   r?   )r   argscurrent_tool_defsc          
     \   |t                      }t          |                     d          pd                                          }|st	          d          S |                     d          }||j        }n6t          dt          |j        t          ||j                                      }t          |          \  }}t          |          }t          |||          }	|t          |          d |	D             d	}
|	s|rt          |          |
d
<   d|
d<   t          j        |
d          S )z?Execute the ``tool_search`` bridge tool. Returns a JSON string.Nr   ru   zquery is requiredr   r.   )r   c                ,    g | ]}t          |          S rF   )rJ  )r   hs     r=   r   z(dispatch_tool_search.<locals>.<listcomp>  s!    888a&q))888r?   )r   total_availablematchesavailable_sourceszNo lexical match was found, but the sources above are connected and their tools remain available. Retry tool_search with the service name plus a concrete action or object before concluding the capability is unavailable.hintFr   )rQ   r   r3   r4   r   r   r7   r8   r   r9   r{   r   r   r   rO  r   r   )rP  rQ  r   r   	raw_limitr   r   rx   r   hitsrD  s              r=   dispatch_tool_searchr\  t  sB   
 ~!!'R((..00E /-...!!I+As62IiId4e4effgg"#455MAzJ''G'5666Dw<<884888 F
  
G 
&?&H&H"#- 	v :f51111r?   c          	        t          |                     d          pd                                          }|st          d          S t	          |          st          d| d          S t          |          \  }}|D ]v}|                    d          pi }|                    d          |k    rDt          j        ||                    dd          |                    di           d	d
          c S wt          d| d          S )zAExecute the ``tool_describe`` bridge tool. Returns a JSON string.rb   ru   zname is required'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.rt   r   r   r0  FrY  z<' is not currently available. Re-run tool_search to refresh.)r   r3   r4   r   ro   r{   r   r   )rP  rQ  rb   r   rx   ry   rz   s          r=   dispatch_tool_describer_    sM    txx%2&&,,..D .,---"4(( 
[ [ [ [
 
 	
 ##455MAz # #VVJ%266&>>T!!:!vvmR88 ff\266  "	# # # # # # " NDNNN  r?   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.
    rt   rb   ru   )r   r3   ro   addr`   )rp   r  ry   rb   s       r=   scoped_deferrable_namesrb    sx     eeE  z""(b--fb99 	+D11 	IIdOOOUr?   c                   	 ddl m} |                    |           }t          |t                    sdS |                    d          dk    r|                    d          n|}t          |t                    sdS |                    d          }t          |t                    sdS |                    d          }t          |t                    r|sdS fd|D             }|sdS t          d	|  d
d                    |           d|d          S # t          $ r! t                              d| d           Y dS w xY w)u  Probe-validate ``tool_call`` arguments against the deferred tool's schema.

    A deferred tool's parameter schema is invisible to the model until it
    calls ``tool_describe`` — so models routinely invoke deferred tools
    "blind" by name alone, omitting required arguments. Dispatching such a
    call produces an opaque downstream failure (``KeyError: 'document_id'``)
    that tells the model nothing about what the tool expects, and cheap
    models loop on it until the iteration budget dies.

    Port of the describe-first probe-validation fix from nearai/ironclaw#5149:
    when required arguments are missing, return the tool's parameter schema
    instead of dispatching blind — the model repairs the call in one
    round-trip. Valid calls (and any call we can't confidently validate)
    dispatch untouched, so this can never block a legitimate invocation.

    Only *key absence* of schema-``required`` fields counts as invalid.
    No type checking, no null rejection — nullable/typed edge cases are the
    tool's own business, and ``coerce_tool_args`` already handles type repair
    downstream. Returns a JSON error string when invalid, ``None`` when the
    call should dispatch.
    r   re   Nr-  rt   r   r/  c                F    g | ]}t          |t                    |v|S rF   )r1   r   )r   rrP  s     r=   r   z/validate_deferred_call_args.<locals>.<listcomp>  s-    OOO*Q*<*<O$1r?   ztool_call to 'z#' is missing required argument(s): r  z. The tool was NOT invoked.zFRetry tool_call with 'arguments' matching the parameters schema above.)r   rX  z)validate_deferred_call_args failed for %sT)exc_info)ri   rf   
get_schemar1   r2   r3   r   r   r   rT   rU   rV   )rb   rP  	_registryr   rz   r   r/  missings    `      r=   validate_deferred_call_argsrj    s   ,888888%%d++&$'' 	4'-zz&'9'9Z'G'GVZZ
###V"d## 	4%%&$'' 	4::j))(D)) 	 	4OOOOhOOO 	4?T ? ?yy!!? ? ? 
 
 
 	
    @$QUVVVtts/   0D AD <*D (,D D (+D 'D?>D?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)``.
    rb   ru   Nz$tool_call requires a 'name' argumentztool_call cannot invoke 'z' (it is itself a bridge tool)r1  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   r3   r4   rh   r1   r   loadsJSONDecodeErrorr2   ro   )rP  rb   raw_argsrZ   s       r=   resolve_underlying_callrp    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#  rh   r   r   r7  rQ   ro   r{   r   r   r   r   r   r   r   r5  rE  rG  r\  r_  rp  rb  rj  )rG   r   rH   r   r   r   )rG   r   rH   r   r   r   )r   r   )r   r[   )rb   r   r   rc   )rp   rq   r   rr   )rp   r|   r   r   )r   r   r   r   r   r   r   rc   )r   r   r   r   r   r   )r   r   r   r   )ry   r   r   r   )rb   r   r   r   )rp   rq   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   r   r   r   r   )r   r   r   r   )rx   rq   r   r   r   r   )rx   rq   r   r   r   r   )Nru   )r(  r   r   r   r)  r   r   rq   )rp   rq   r   r   r   r<  r   r7  )rn   r   r   r   )r   r   r   rq   )rP  r   rQ  rq   r   r<  r   r   )rP  r   rQ  rq   r   r   )rp   rq   r   r[   )rb   r   rP  r   r   r   )rP  r   r   rk  )?rC   
__future__r   r   loggingr   redataclassesr   r   typingr   r   r   r	   r
   r   ri   r   	getLoggerrU   r  r"  r#  r`   rh   r   r   r9   r6   rQ   ra   ro   r{   r   r   r   r   compiler   r   r   r   r   r   r   r   r   r   r   r   r5  r7  rE  rG  rJ  rO  r\  r_  rb  rj  rp  __all__rF   r?   r=   <module>ry     s  # #J # " " " " "    				 ( ( ( ( ( ( ( ( = = = = = = = = = = = = = = = = % % % % % %		.	/	/ ! $ I/1C^TUU   $O
 O
 O
 O
 O
 O
 O
 O
d      / / / /&
 
 
 
   4   89 9 9 9"   .; ; ; ;, 
5 
5 
5 
5 
5 
5 
5 
5 BJ''	8 8 8 80 0 0 0$      : -1    >(* (* (* (* (*` 2:j)) * * * * **         H P P P P P Pj "w w w w w~        & %))-	F F F F F F\% % % %      . ?C!2 !2 !2 !2 !2 !2H   4   (2 2 2 2j       B  r?   