tool_loader

Auto-discover and load tools from a directory of Python scripts.

Each .py file in the tools directory must expose either:

Single-tool format (one tool per file):

  • TOOL_NAME – unique name for the tool (str).

  • TOOL_DESCRIPTION – human-readable description (str).

  • TOOL_PARAMETERS – JSON Schema object for accepted args (dict).

  • async def run(**kwargs) -> str – the tool handler.

Multi-tool format (multiple tools per file):

  • TOOLS – a list of dicts, each with keys: name, description, parameters, handler.

Malformed files are logged as warnings but do not prevent the rest of the tools from loading.

tool_loader.load_tool_manifest_allowlist(tools_path)[source]

Read the manifest allow-list of tool script basenames.

Loads and parses tools/_manifest.json to obtain the explicit set of *.py filenames that load_tools is permitted to import. This allow-list is the security gate that stops arbitrary or stray files in the tools directory from being executed at load time, so a missing or unreadable manifest deliberately yields an empty set (and therefore no tools load) rather than falling back to “load everything”.

It locates the file via _manifest_path, reads it from the filesystem, and JSON-decodes it; any read or parse error is logged and swallowed, returning an empty set. No Redis, network, or LLM interaction. Called by load_tools (to filter the directory scan) and by append_tool_manifest (to merge in a new entry).

Parameters:

tools_path (Path) – Directory containing the tool scripts and the manifest.

Returns:

The allowed basenames (e.g. {"foo.py", "bar.py"}); empty if the manifest is missing, unreadable, or has no allow list.

Return type:

set[str]

tool_loader.append_tool_manifest(tool_basename, tools_dir='tools')[source]

Atomically add a tool basename to the manifest allow-list.

Extends tools/_manifest.json so that a newly written tool script becomes loadable on the next scan. This is the write counterpart to load_tool_manifest_allowlist and exists so tools that author other tools at runtime can self-register without a manual manifest edit; the write is done atomically so a concurrent or crashed run never leaves a half-written manifest.

It resolves the manifest via _manifest_path (raising if it does not exist), merges tool_basename into the current allow-list read back via load_tool_manifest_allowlist, then writes the JSON to a temp file in the same directory and os.replace-es it over the original (cleaning up the temp file on error). Filesystem only — no Redis, network, or LLM. Called by the write_python_tool tool handler (tools/write_python_tool.py), which invokes it via asyncio.to_thread after persisting a generated tool file.

Parameters:
  • tool_basename (str) – The *.py filename to allow (e.g. "foo.py").

  • tools_dir (str | Path) – Directory holding the tool scripts and manifest; defaults to "tools".

Raises:

FileNotFoundError – If the manifest file does not already exist.

Return type:

None

tool_loader.load_tools(directory, registry)[source]

Discover, import, and register every allow-listed tool script.

Walks a tools directory and turns each permitted *.py file into one or more registered tools on registry. This is the entry point that populates the live tool set the LLM can call, supporting both the single-tool module format (TOOL_NAME / TOOL_DESCRIPTION / TOOL_PARAMETERS / run) and the multi-tool TOOLS list format, plus optional aliases and per-tool flags. It is written to be resilient: a malformed or failing file is logged and skipped so one bad tool never blocks the rest.

It reads the manifest allow-list via load_tool_manifest_allowlist and refuses to load anything not listed (an empty allow-list loads nothing). For each allowed file it imports the module through importlib — reusing an already-imported tools.<stem> from sys.modules when present, or executing the file fresh and registering it under both its canonical and a private alias name — then mutates registry._tools directly to insert ToolDefinition entries (including any TOOL_ALIASES). Touches the filesystem (directory glob and module exec) but no Redis, network, or LLM. Called from many places that build or rebuild the registry: the classifier index builders (classifiers/build_tool_index.py, classifiers/update_tool_embeddings.py), the live reload_tools tool, the write_python_tool and import_mcp_tool handlers after authoring a new tool, and several tests.

Parameters:
  • directory (str | Path) – Path to the tools directory to scan (e.g. "tools"). A missing directory is warned about and skipped.

  • registry (ToolRegistry) – The ToolRegistry to populate; its internal _tools mapping is mutated in place.

Return type:

None