Skip to content

v0.19.0

Latest

Choose a tag to compare

@github-actions github-actions released this 23 Oct 17:57
· 29 commits to master since this release

Installation

pip install toolregistry==0.19.0

Added

  • Public validation error types (#340): ValidationError / ErrorDetail and SchemaValidationError / SchemaErrorDetail are now importable from toolregistry. Tool.validate_parameters() raises the first pair and raw JSON Schema validation raises the second; previously both lived only behind toolregistry._vendor, so a downstream server could not map a validation failure to its own error format without importing a private path. The names are re-exports of the classes actually raised, pinned by identity in tests/test_validation_errors.py.
  • Relevance floor on discover_tools fuzzy results (#330): BM25 hits scoring below min_score_ratio of the best hit are dropped, so a query with one clear match is no longer padded with unrelated tools that merely share a token. The default is 0.3; ToolDiscoveryTool(..., min_score_ratio=...) and enable_tool_discovery(min_score_ratio=...) tune it, and 0 disables the floor. The floor is relative because BM25 scores are not comparable across queries; the best hit is always kept and exact-name lookups bypass it.
  • Per-source filters in declarative config (#297): MCPSource and OpenAPISource accept include / exclude, so a config entry can narrow an MCP server or OpenAPI spec instead of registering everything. Each value may be a bare string or a list of fnmatch globs; the two sides are mutually exclusive. An omitted filter means no filtering, while an empty list is an explicit empty filter that matches nothing. Unknown keys on a source entry or under auth now raise ConfigError naming the entry and the offending keys instead of being silently ignored.
  • call_deferred tolerates client-normalized tool names (#277): _target_tool is resolved by collapsing / and - to _ and folding case, so a name an MCP client rewrote (weather_get_current) reaches the registered tool (weather-get-current). An exact registered spelling always wins over a normalized match, so two names that collide after normalization still dispatch to their own tool. On a miss the error lists close registered names (Did you mean: datetime-now?) so the model can self-correct in one turn.
  • discover_tools input validation and CJK search (#278): query now carries minLength: 1 plus a non-whitespace pattern, and top_k carries minimum: 1; both also raise ValueError at runtime. A zero-dependency tokenizer adds character bigrams for CJK runs before BM25 indexing, so Chinese queries match Chinese tool descriptions instead of silently returning nothing. The discover_tools description now states which tool text queries are matched against.
  • Execution hint metadata (#280): ToolMetadata gains title, read_only_hint, destructive_hint, idempotent_hint and open_world_hint. All default to None (unset / unknown), which stays distinguishable from an explicit False. ToolMetadata.execution_hints returns the resolved dict[str, bool], deriving read_only_hint from ToolTag.READ_ONLY and destructive_hint from ToolTag.DESTRUCTIVE, and filling destructive_hint=False for a read-only tool so the pair cannot contradict the protocol defaults. Keys map 1:1 onto the MCP readOnlyHint / destructiveHint / idempotentHint / openWorldHint annotations. discover_tools declares itself read-only; call_deferred makes no claim because its effect depends on the target tool. Declaring both a read-only and a destructive hint raises a UserWarning, and the config loader warns about unrecognised keys under tool_metadata.<tool>.
  • Registration-time include / exclude filters (#281): register() and register_async() accept include or exclude for the mcp, openapi, and langchain sources, so a source can be narrowed as it is registered instead of registering everything and disabling what is unwanted. Patterns are fnmatch globs matched against both the raw source name (the MCP tool name, the OpenAPI operationId, the LangChain tool name) and the post-namespace registered name. include and exclude are mutually exclusive, and passing either to native / class raises ValueError. Filtered-out tools never reach the registry, so they stay invisible to discover_tools and the admin panel, and the filter is re-applied on refresh so excluded tools neither reappear nor get reported as removed.
  • Tool overrides (#282): override(name, tool, create=False) replaces a registered tool and records the displaced definition, restore_override(name) puts it back, and list_overrides() returns the displaced definitions. Both directions emit the new ChangeEventType.OVERRIDE event; override(..., create=True) on an unregistered name also emits REGISTER, so listeners keyed on registration still learn the tool appeared. A refresh leaves overridden names untouched, re-records the displaced definition from the remote so restore_override() reverts to what the source serves now, and reports the suppressed names in a new RefreshResult.overridden tuple instead of counting them as unchanged. A plain register() onto an overridden name drops the override record, symmetric with unregistering, and namespace-rewriting operations (merge, spinoff, reduce_namespace) keep override records keyed to the renamed tools.
  • Replacement on plain registration is now visible (#290): registering onto an existing name still replaces the stored tool, but now emits a UserWarning naming the discarded definition together with a new ChangeEventType.REPLACE event whose metadata["replaced_tool"] carries it. Pass allow_replace=True to register() / register_async() or to the register_*_tools integration methods to acknowledge the replacement and suppress the warning; the event still fires, so observation is not gated on the flag. Re-registering a field-equal Tool — including the same object under the same namespace — is not a replacement, and when the discarded tool held an override the warning says the recorded override was also dropped.

Changed

  • LangChain integration is structural (#313) (breaking): source="langchain" now accepts any object with name, description, a parameter schema (input_schema, args_schema, parameters, or args), and a sync/async runner. langchain-core and langchain-community are no longer installed by toolregistry[langchain] (the extra is now empty) and are not imported at runtime. The old unconditional del input_schema["description"] raised KeyError for any args model without a docstring, on langchain-core 0.3.x and 1.x alike; registration is now version-independent, pydantic-v1 args_schema models are read through schema() so required is preserved, and the sunset langchain-community dependency is gone. Real BaseTool compatibility is checked by a new scheduled langchain-drift workflow against langchain-core 0.3.x and 1.x on Python 3.10 and 3.13.
  • update_namespace(force=False) preserves the existing namespace (#310) (behavior change): an already-qualified tool is returned unchanged. Previously the namespace field was rewritten to the new value while name kept the old prefix, leaving the two fields inconsistent. Pass force=True to replace it.
  • Tool.qualified_name returns name (#310) (behavior change): for hand-built tools whose namespace / method_name fields disagree with name, the property now returns the authoritative name instead of reconstructing {namespace}-{method_name}. Tools created through register() or the from_* factories are unaffected.
  • Discovery index rebuild is deferred for register/unregister (#289): REGISTER / UNREGISTER now mark the BM25 index stale instead of rebuilding it per event, matching the existing ENABLE / DISABLE / METADATA_UPDATE path. Bulk registration pays one rebuild at the next discover(); deferred-tool descriptions are still refreshed eagerly.
  • Config loader rejects unknown keys (#297) (behavior change): a misspelled key on a tool source entry or under auth now raises ConfigError with the entry index and valid key set. Configs that relied on a silently ignored key will fail at load time instead of partially applying.
  • name_sep applies to every registration source (#302): native functions and class methods now honor ToolRegistry(name_sep="."), matching MCP, OpenAPI, and LangChain. Tool.from_function(..., sep=...) and the new read-only ToolRegistry.name_sep expose the same rule. Default - behavior is unchanged.
  • Tool results with no JSON projection now fail loudly (breaking) (#279): a result that cannot be encoded to JSON raises a TypeError naming the tool and the type instead of being silently converted with str(). The message suggests the fix (to_dict(), model_dump(), tolist(), or returning a str). invoke() surfaces this as an ErrorResult, so the public "never raises" contract is unchanged.
  • Process-backend results are encoded in the parent (#279): the process worker no longer stringifies its result, so the encoder — and the raise above — now apply on the default execution mode rather than only on the thread backend. A tool result crossing a process boundary must therefore be picklable.

Fixed

  • A homogeneous corpus no longer turns a working query into nothing (#342): when every token of a multi-token query was above the document-frequency floor, the token filter left nothing to search and discover() returned [] — so adding a word to file (4 hits) produced file on disk (0 hits). The filter now falls back to the query's capability tokens (tokens that appear in at least one tool name) and keeps the rarest, which reproduces the single-word result on a corpus where every tool says "work with a file on disk". A query made only of boilerplate description words has no capability token and still returns nothing, which is what #332 wanted.
  • VALIDATION_ERRORS catches both validation failures (#342): ValidationError (signature-derived tools) and SchemaValidationError (raw JSON Schema) are siblings from different vendored modules, so except ValidationError never caught the second. The new tuple is exported from toolregistry so a caller can map either failure to one outcome with except VALIDATION_ERRORS: without enumerating the classes.
  • Discovery results carry the declared output schema (#337): discover_tools returned only the input parameters, so a model discovering a deferred tool learned what it takes but not what it returns — even though the tool emits structuredContent conforming to a declared outputSchema. A result for a tool with ToolMetadata(extra={"output_schema": ...}) now includes that schema beside the input one, inside function for OpenAI-style definitions and at the top level for the flat openai-responses, Anthropic and Gemini shapes. Both exact-name and fuzzy results carry it, and tools without a declared schema are unchanged.
  • A repeated enable_tool_discovery() reports ignored options (#336): the second call returns the existing discoverer unchanged, so min_score_ratio, max_df_ratio and field_weights passed to it used to be dropped without a word — which made retuning look broken. Differing values now raise a UserWarning naming them and pointing at disable_tool_discovery(); omitted options and values that match the live instance stay silent, so idempotent calls do not become noisy. The docstring states that the first call wins.
  • A term shared by a few tools stays findable (#334): document frequency is only treated as a signal once a term appears in at least three indexed tools, so in a four-tool registry a word used by two tools is searched instead of being dropped at the 50% max_df_ratio boundary. A single-token query is never filtered: one word is a capability keyword rather than prose padded with stopwords, so draw or search is not discarded for being common, while multi-word stopword queries such as the a of still return nothing.
  • A directly constructed ToolDiscoveryTool tracks registry changes (#334): the discoverer now subscribes to REGISTER / UNREGISTER / OVERRIDE / ENABLE / DISABLE / METADATA_UPDATE itself, so a tool registered after construction is searchable at the next discover() without a manual rebuild_index(), and the document-frequency map no longer describes a corpus that has moved on. close() unsubscribes (idempotently); disable_tool_discovery() calls it.
  • Query tokens present in most tools no longer produce filler hits (#332): discover_tools drops query tokens that appear in at least max_df_ratio of the indexed tools (default 0.5; configurable on ToolDiscoveryTool and enable_tool_discovery, 0 disables) before scoring. On a corpus with no weather tool, weather forecast for a city now returns nothing instead of near-tied matches driven by for and a, and the a of returns nothing. The filter is corpus-adaptive rather than a stopword list, requires the term to occur in at least three indexed tools, and composes with the min_score_ratio floor. min_score_ratio and max_df_ratio now reject bool values.
  • MCP stdio scripts run under the ToolRegistry interpreter (#323): a .py source now starts with sys.executable instead of the python found on PATH. When a virtualenv or conda environment was not first on PATH, the server started in a different interpreter and died at import, which the client could only report as Connection closed. Callers that need a specific interpreter can still pass the {"command": ..., "args": [...]} form.
  • Native tools survive a cloudpickle round-trip on Python 3.11+ (#319): Tool no longer puts its dynamically generated parameter TypedDict into the pickle state. cloudpickle could serialize that type but not load it again (TypeError: cannot inherit from both a TypedDict type and a non-TypedDict base class), so a native tool was unusable across a process boundary. The validation model is regenerated from the wrapped function on load; if regeneration fails, the tool loads without parameter validation and emits a UserWarning instead of failing the unpickle.
  • Executor lifecycle follow-ups (#321): a process pool created while the parent was single-threaded now migrates from fork to forkserver/spawn when the process later grows threads (or warns once if work is still in flight), so new workers are never forked from a threaded parent. ToolRegistry.close(wait=True) / close_async(wait=True) drain in-flight and queued execution; the default remains non-blocking.
  • Python 3.14 deprecation in the executor backends (#314): the thread and process backends use inspect.iscoroutinefunction instead of the deprecated asyncio.iscoroutinefunction, so python -W error users can execute tools on Python 3.14 and later.
  • Execution resources are released by close() (#311): ToolRegistry.close() and close_async() now shut down the thread and process execution backends in addition to the remote integrations; the backends recreate their pools lazily, so the registry remains usable and repeated calls are safe. Pools are created lazily on first submit, so a registry that never executes anything holds no worker threads or IPC pipes. The process backend forks when the parent is single-threaded and starts workers through forkserver (or spawn) once other threads are running, so scripts, REPLs, and piped input keep working while threaded hosts no longer fork. A threaded file-based entry point needs the standard if __name__ == "__main__": guard; a threaded parent with a non-importable __main__ warns and falls back to fork. close() abandons in-flight work by default; pass wait=True to drain it (see #321). Leaked registries are also closed at pytest module teardown, the shared async runtime is stopped between modules, and CI jobs carry explicit timeouts. The dedicated LangChain job is kept so the suite always has a langchain-free run.
  • spinoff() and reduce_namespace() honor the registry separator (#310): namespace operations now use Tool.namespace / Tool.method_name instead of parsing keys with ".". spinoff() works under the default -, matches name_sep, avoids matching a longer namespace (math vs math_ops), and the returned registry inherits the source separator and execution mode. reduce_namespace() flattens only tools in the single remaining namespace, so unqualified tools are no longer truncated (helper no longer becomes r), and collisions raise instead of overwriting. Names are rebuilt from the fields rather than blind-sliced, merge() converts incoming names to the receiving registry's separator and carries incoming override records, and changing name_sep at runtime re-keys registered tools. Disabled state moves with the tool through spinoff, merge, reduce_namespace, and separator changes, so a disabled destructive tool cannot come back live, and every operation that renames tools marks the discovery index stale so discover() / call_deferred() use the new keys. Tool.qualified_name returns the registered name.
  • Replacement warnings no longer skip hooks or events (#301): the replacement UserWarning is emitted after post-register hooks and the REGISTER / REPLACE events. With warnings configured as errors, the exception now follows a consistent registry state that every listener has observed.
  • Filter warnings report the caller's file and line (#300): ToolNameFilter.warn_if_all_filtered_out() and the replacement warning use a caller-attributed helper. AsyncRuntime.run_sync() carries the synchronous caller's location into the coroutine, so the sync bridge no longer reports integration internals; executor-thread paths fall back to the immediate caller.
  • Remote registration and refresh agree on namespaced names (#295): both paths derive the registry key with the registry separator. With name_sep=".", an include that matches at registration no longer misses there and match at refresh, and refresh no longer produces a double prefix such as gh.gh-list_issues.
  • Remote filters run before tool construction (#296): MCP, OpenAPI, and LangChain apply include / exclude to the raw and qualified names before building the wrapper. An explicitly excluded malformed operation or failing schema extraction can no longer abort the whole source.
  • discover_tools no longer advertises disabled tools (#276): the BM25 index is built from enabled tools only, matching get_deferred_summaries(), and the exact-name path rejects disabled tools. ENABLE / DISABLE / METADATA_UPDATE mark the index stale rather than rebuilding per event, so a bulk disable or profile application pays one rebuild at the next discover(). This also fixes a stale per-document deferred flag that made discover() omit schema and report deferred: false for a tool that get_deferred_summaries() listed as deferred.
  • Non-serializable tool results are JSON-encoded (#279): dataclasses, Pydantic models, objects exposing to_dict() / tolist(), enums, paths, decimals, UUIDs, datetimes, bytes and sets reach the wire as valid JSON instead of Python repr() text. Top-level and nested projections now agree, and set encoding is deterministic.
  • Process-backend pickling failures explain the backend (#288): a result that cannot be pickled out of the worker (a generator, a lambda, a live connection) now surfaces as UnpicklableResultError, naming the process backend and asking for a picklable value — the thread backend skips pickling but still requires an encodable result — instead of a bare TypeError / AttributeError from pickle. Native (class-based) tools use the process backend by default and get the same guidance; MCP and OpenAPI tools are unaffected because their natural_backend="inline" forces the thread backend.
  • ToolMetadata.all_tags tolerates plain-string tags: update_tool_metadata(tags=["x"]) no longer aborts index rebuilding with AttributeError.
  • Registration filters match normalized names and surface empty results (#291): a bare string passed to include / exclude is now a single pattern instead of an iterable of characters, so a one-item JSON/YAML include: name works. Patterns match four spellings — the raw source name, the post-namespace registered name, and the snake_case form of each — so include=["bash_execute"] selects a tool advertised as bash-execute. MCP and OpenAPI registration emit a UserWarning when an active filter leaves the source empty, distinguishing "matched none" (include) from "excluded all" (exclude); LangChain registers one tool per call and stays silent. Matching remains case-sensitive, and a normalized spelling selects every tool whose normalized name collides.

Full Changelog: v0.18.1...v0.19.0