ToolResolver is the one place PraisonAI looks for tools.py — whether it ships callables or BaseTool classes.
Quick Start
Basic Usage
Drop a
tools.py next to your YAML/script, set the environment variable, and the resolver picks both kinds up automatically:Resolution Order
Tools are resolved in a specific order, with the first match winning: CLI, YAML, recipes, templates, and Python all share this resolution chain. See CLI Reference for--tools usage.
One resolution chain for every surface. Whether you load tools via the CLI
--tools flag, a YAML tools: list in agents.yaml, a recipe’s tools: list, or Agent(tools=[...]) in Python, PraisonAI walks the same five-source chain: local tools.py → wrapper ToolRegistry → praisonaiagents.tools → praisonai-tools → core SDK plugins. Tools registered through any of these become visible everywhere — no more “works in Python but not in the recipe.”Discovery matches resolution. As of PR #2476,
praisonai tools list and tools info enumerate every source in this chain — including tools registered through the wrapper ToolRegistry (register_function) and core SDK registry (entry-point plugins). Each tool surfaces with its authoritative source via ToolResolver.list_available_sources(), so attribution matches the callable resolve() would actually return — no more “resolves at run time but invisible to the CLI.”Tools appear under one of four source labels: builtin (praisonaiagents.tools), local (your tools.py), external (praisonai-tools package), or registered (wrapper ToolRegistry or core SDK entry-point plugin). See CLI tools reference for the full label table and --source filter usage.Custom-sources subtlety: a resolver built with an explicit sources= list does not enumerate the default built-in / external / core-registry sources in discovery (it would misrepresent what resolve() can return). The wrapper ToolRegistry is always enumerated when present, regardless of custom sources.Diagnostics and template dep-checks match resolution too. As of PR #2642,
praisonai tools doctor and praisonai tools discover — and the YAML template dependency checker that runs when you load a template — all consult the same ToolResolver source list. So a tool that resolves at run time (via praisonai_tools, the wrapper ToolRegistry, the core registry, or an entry-point plugin) will no longer be flagged as “missing” by the doctor, hidden from discover, or blocked by the template loader. tools doctor now reports the full ~151 built-in tools instead of a subset.Concretely, praisonai tools list / tools info, ToolsDoctor.diagnose(), and DependencyChecker.check_tool() all consult ToolResolver.list_available_sources(). Their outputs now agree with resolve() — a tool that resolves at run time is visible everywhere; a tool that doesn’t resolve is reported as missing everywhere. Each site degrades gracefully to its previous partial scan when the resolver is unavailable. See Tools Doctor, Tools Discover, and Strict Tools Mode for the updated diagnostic documentation.Registering Tools at Runtime
Register custom tools through theToolRegistry for YAML pipeline access:
agents.yaml:
How It Works
The resolver delegates to_safe_loader.load_user_module for consistent environment variable checking and CWD path-traversal guard. The loaded module is reflected to extract either plain functions or tool class instances, then cached as an immutable view for thread safety.
The wrapper now invokes resolve() once per YAML-referenced tool name, with results cached via the resolve cache to avoid repeated lookups.
Directory mode iterates each .py file and unions the results using the same security gates and extraction logic.
CLI and recipes use the same resolver (PR #1857, PR #2059, PR #2499)
praisonai --tools tavily_search,my_tool "..." goes through ToolResolver.resolve(name, instantiate=True), identical to the YAML path. Recipe and template tool loading via resolve_tools() also delegates to ToolResolver (PR #2059), so wrapper ToolRegistry registrations, praisonai-tools package tools, and core SDK plugin tools are reachable from recipes and templates — not just the agent build path. As of PR #2499 the same chain also handles --rewrite-tools (query-rewrite), --expand-tools (prompt-expansion), and research --tools.
Reachable from the CLI (default, YAML, and Python surfaces). The --tools flag on default run, YAML tools: lists, and Agent(tools=[...]) in Python all resolve names through this same chain. As of PR #2681, run --output actions also honours --tools/--toolset — previously they were dropped in actions mode. A name unknown to every source prints Warning: Unknown tool '<name>' and is skipped. See Run and the CLI reference.
Multi-tenant usage
ToolResolver resolves tools.py eagerly at construction time using Path.resolve(). The path captured is the CWD when the resolver was created — not the CWD when resolve() is called later. This makes behaviour predictable in multi-tenant gateways where each tenant has a different working directory.
When to call reset_default_resolver()
| Situation | Call it? |
|---|---|
| Single-tenant CLI | ❌ No |
| Multi-tenant gateway switching CWDs per request | ✅ Yes, before each tenant |
| Long-running server with hot-reload of tools | ✅ Yes, after tools change |
| Test setup/teardown | ✅ Yes, in a fixture |
You always pass an explicit tools_py_path | ❌ No |
Two Flavours of tools.py
What’s in tools.py | Method | Returned shape |
|---|---|---|
| Plain Python functions | get_local_callables() | List[Callable] |
praisonai_tools.BaseTool / praisonai.tools.BaseTool / langchain_community.tools.* classes | get_local_tool_classes() | Dict[str, ToolInstance] (instantiated) |
| A directory of *.py files (each may contain BaseTool subclasses) | get_local_tool_classes_from_dir(tools_dir) | Dict[str, ToolInstance] (merged across files) |
tools: list), call resolver.resolve("ToolClassName", instantiate=True) — see Common Patterns → Resolving Class Tools below.
Example tools.py with functions:
High-Level Loading Methods
For embedders and advanced users,ToolResolver exposes four convenience methods that combine resolution, instantiation, and security gating in a single call.
- Resolve everything in a YAML config
- Load functions from a module
- Load BaseTool classes from a module
- Load functions from a package directory
PRAISONAI_ALLOW_LOCAL_TOOLS=true gate and CWD path constraints. See Security.
| Method | Returns | Purpose |
|---|---|---|
resolve_all_from_yaml(yaml_config) | Dict[str, Callable] | One call returns every tool referenced in a parsed YAML config, with classes instantiated and tools.py / tools/ merged in. |
load_functions_from_module(path) | Dict[str, Callable] | Load plain Python functions from a single .py file. |
load_classes_from_module(path) | Dict[str, Callable] | Load and instantiate BaseTool subclasses and langchain_community.tools.* classes from a single .py file. |
load_functions_from_package(path) | Dict[str, Callable] | Iterate *.py in a directory (skipping __*.py) and union the functions. |
list_available_sources() | Dict[str, str] | Returns {name: source} where source is one of "local", "builtin", "external", "registered". Reflects the chain resolve() would actually use — used by praisonai tools list / tools info for authoritative source attribution. |
PR #2017 security fix.
load_functions_from_package() now goes through _safe_loader, honouring PRAISONAI_ALLOW_LOCAL_TOOLS and the CWD path constraint. The previous AgentsGenerator.load_tools_from_package used importlib.import_module() directly and bypassed these checks.Configuration Options
| Parameter | Where | Type | Default | Description |
|---|---|---|---|---|
tools_py_path | ToolResolver(...) constructor | Optional[str] | "tools.py" | Path to the tools.py file to load (resolved eagerly against the current working directory). |
instantiate | resolver.resolve(name, ...) | bool | False | When True, class tools (BaseTool subclasses, langchain_community.tools.* classes) are instantiated before being returned. Plain function tools are returned as-is. Safe with cache warm-up via has_tool() (since PR #1858). |
source_registry | ToolResolver(...) constructor | Optional[ToolSourceRegistry] | Process-default (lazy, discovers praisonai.tool_sources entry points) | Injects a registry supplying third-party tool sources. See Tool Source Registry. Pass ToolSourceRegistry(discover_entry_points=False) to opt out of third-party sources. |
The
tools/ directory case takes an explicit tools_dir argument and is not bound to the constructor’s tools_py_path.Common Patterns
- Loading from Custom Path
- Reloading After Edits
- Mixed Function and Class Tools
- Loading from tools/ Directory
- Resolving Class Tools
Local Tool Override Warning
When a class-based tool in your localtools/ directory shares a name with a tool already resolved from the resolution chain (wrapper registry, praisonaiagents.tools, etc.), resolve_all_from_yaml() logs a warning:
Hot-Reload: invalidate_local_tools_dir()
In long-running processes (file watchers, dev servers), the tools/ directory class scan is cached per-resolver. After editing tools/*.py, trigger a re-scan:
clear_cache() also clears the directory cache alongside the resolve cache.
Security
Security enforcement is handled by_safe_loader.load_user_module:
- Environment gate: Requires
PRAISONAI_ALLOW_LOCAL_TOOLS=true - CWD constraint: Refuses paths outside current working directory
- Path traversal protection: Prevents
../style attacks
Per-Context Resolver (Multi-Project Safety)
When you callresolve_tool(name) without passing a resolver, PraisonAI now uses a context-local default resolver instead of a single process-wide singleton. Each agent/task/request anchors to its own working directory.
reset_default_resolver()
| When to call | Daemons or IDE plugins that switch the working directory between projects |
| What it does | Clears the cached ToolResolver in the current context so the next call re-anchors to the new CWD |
| Import | from praisonai.tool_resolver import reset_default_resolver |
Caching Behaviour
- Per-context cache: Each context caches its own tools.py content
- First call: Loads and caches tools.py content
ToolResolver maintains two separate caches for performance:
Local tools.py cache:
- First call: Loads and caches tools.py content
- Subsequent calls: Returns cached immutable view (
MappingProxyType) - Thread safety: Uses
_local_tools_lockfor concurrent access
- Per-tool caching: Memoises
resolve(name)results for each tool name - Cacheable failures: A tool genuinely absent from every source (local
tools.py,praisonaiagents.tools,praisonai-tools, registry) is cached asNoneso repeated lookups don’t walk the ladder again - Transient failures are NOT cached: If
praisonaiagentsfails to import (ImportError) or an entry inTOOL_MAPPINGSexists but its optional dependency failed to load, the lookup is retried on the next call. Install the missing package and the nextresolve()picks it up — noclear_cache()needed invalidate(name=None): Clear one tool or the entire resolve cache.ToolRegistry.set_resolver()appends to a weak-ref list (PR #2122) — all registered resolvers are notified onregister_function()/clear(). Resolvers held only byToolRegistryare GC’d; dead refs are cleaned lazily on each invalidation. Multi-tenant gateways can wire one resolver per tenant without overwriting each other.- Thread safety: Uses
_resolve_cache_lockfor concurrent access. Fixed in PR #2147 — an earlier lock-free fast-path read could race withToolRegistry.invalidate()and miss freshly-registered tools; the cache lookup now happens inside the lock.
ToolSource protocol
ToolSource is a supported extensibility hook — pass custom sources via ToolResolver(sources=[...]) to fully control resolution order.
resolve() walks self._sources when custom sources are provided. Each source’s lookup(name) is called in order; the first non-None result wins. A source that raises an exception does not poison the cache — allow_none_cache=False ensures the next call after a dependency installs can still resolve the tool.
Entry-point tool sources
Third-party packages can registerToolSource implementations via the praisonai.tool_sources entry-point group — ToolResolver appends them after the default 5-step chain. See Tool Source Registry for the full pattern, plugin authoring, and opt-out.
tools.py → wrapper registry → praisonaiagents → praisonai-tools → core registry) is owned by ToolResolver.default_sources() and is unaffected by the registry — entry-point sources are always appended, never inserted, so a third-party plugin cannot silently shadow a first-party tool.
Composing custom and default sources:
The default_sources(registry=None) method returns the historical 5-step resolution chain as a list of ToolSource objects (local-tools.py → wrapper-registry → praisonaiagents → praisonai-tools → core-registry). Prepend your custom source to search it first while keeping all built-in sources:
registry=:
Passing registry= to ToolResolver(...) automatically calls registry.set_resolver(self). This means register_function() on the registry will automatically invalidate the resolver’s cache — no manual set_resolver() call needed:
tools.py exports BaseTool subclasses (rather than plain functions), pass instantiate=True to get a ready-to-use instance back. The cache returns the same instantiation path whether or not has_tool() or validate_yaml_tools() warmed the cache first — so YAML validation flows that check tools exist before resolving them no longer return uninstantiated classes (fixed in PR #1858).
Resetting the Default Resolver
Callreset_default_resolver() to clear the context-local resolver cache.
Useful between tenants, on CWD change, or in test setup so that local
tools.py resolution is not affected by previous calls.
clear_cache() now clears both caches — useful after editing tools.py and after registering new tools in the wrapper ToolRegistry at runtime.
Best Practices
Keep tools.py in your CWD
Keep tools.py in your CWD
Place
tools.py in your current working directory. Paths outside CWD are refused even with the environment variable set. This prevents path traversal attacks from HTTP API callers.Prefer functions for praisonaiagents
Prefer functions for praisonaiagents
Use plain Python functions for
praisonaiagents agents. Reserve BaseTool classes for crewai-style flows or when you need complex tool state management.Import from the wrapper package
Import from the wrapper package
Don’t import
ToolResolver from praisonaiagents — it lives in the wrapper at praisonai.tool_resolver. The wrapper handles YAML-based tool resolution.Set environment variable only in trusted environments
Set environment variable only in trusted environments
Set
PRAISONAI_ALLOW_LOCAL_TOOLS=true only in development or trusted deployment environments. This prevents arbitrary code execution from untrusted working directories.Related
Tool Source Registry
Plug third-party tool sources via entry points
Tools Resolve (CLI)
Recipe and template tool resolution via resolve_tools()
Create Custom Tools
Build tools for agents and YAML configs
Security Environment Variables
Environment variable security controls
Tools
General tools documentation

