ToolResolver is the one place PraisonAI looks for tools.py — whether it ships callables or BaseTool classes.
Quick Start
1
Basic Usage
Drop a
tools.py next to your YAML/script, set the environment variable, and the resolver picks both kinds up automatically:2
Direct Python Usage
When embedding PraisonAI in your own Python code:
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, plain Agent(tools=[...]) in Python, or LocalManagedAgent(config=LocalManagedConfig(tools=[...])) on the managed_local backend, 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, and now not in the local-managed backend either.”See Local Managed Agents → Tool Name Resolution for the managed-local specifics (alias translation and compute-bridge wrapping run around resolution, not through it).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, PR #2935)
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. As of PR #2935, the file-path branches of the same four flags also route through ToolResolver.load_functions_from_module (previously each site hand-rolled _safe_loader.load_user_module + inspect.getmembers). As of PraisonAI#3122 the wrapper no longer re-implements praisonai-tools package discovery — that source is now owned exclusively by ToolResolver._resolve_from_praisonai_tools, keeping a single implementation for external-tool discovery. The wrapper still builds its unique sources (default dirs, TEMPLATE.yaml tools_sources, --tools/--tools-dir, and the PRAISONAI_ALLOW_TEMPLATE_TOOLS-gated tools.py autoload) and hands them to the resolver as a registry.
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()
Sharing one resolver across the run
Tool discovery — scanningtools.py, praisonaiagents.tools, praisonai-tools, wrapper ToolRegistry registrations, and the core plugin registry — is not cheap. PraisonAI reuses one resolver per context (per agent / per request / per CLI run) so this discovery happens once, not once per generator construction.
_get_default_resolver() is backed by a contextvars.ContextVar. Every code path that used to build a fresh ToolResolver() now flows through this helper by default:
Passing an explicit resolver (DI)
For tests or multi-tenant code, pass your own resolver intoAgentsGenerator:
adapter_registry= and tool_timeout_executor= DI parameters and works identically for the sync (generate_crew_and_kickoff) and async (agenerate_crew_and_kickoff) paths.
If you’re switching tenants mid-process, prefer
reset_default_resolver() (covered above under Multi-tenant usage) plus a fresh AgentsGenerator per tenant. Passing tool_resolver= gives you tighter control when you already manage resolver lifetimes yourself.resolve_tools() helper used by recipes and templates has its own small memoisation layer on top of this — a bounded LRU keyed on the registry object so per-agent calls in a workflow build share one resolver. See Tools Resolve → Resolver memoisation.
Two Flavours of tools.py
If you need to resolve class tools by name (e.g. from a YAML
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.
The
.praisonai/tools/ merge is additive and gated. With PRAISONAI_ALLOW_LOCAL_TOOLS=true, @tool functions in .praisonai/tools/*.py are merged into the resolved dict keyed by tool name — so a @tool(name="weather_lookup") is referenceable from a YAML tools: list as weather_lookup. When the gate is off (default), the resolver skips the directory walk-up and git rev-parse subprocess entirely (perf guard from commit 12df267). See #3107.PR #2935 consolidation. The four hand-rolled
load_user_module + inspect.getmembers copies in the legacy CLI (--tools file.py, --rewrite-tools file.py, --expand-tools file.py, research --tools file.py) now route through this single method with functions_only=True, skip_private=True. Behaviour is identical to before, but there is only one owner — silent drift between the four paths is no longer possible. The module_name argument ("rewrite_tools_module" / "expand_tools_module") keeps simultaneous loads from shadowing each other under sys.modules.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.Naming key: YAML vs praisonai run
The key used to reference a .praisonai/tools/ callable differs between the two surfaces — this is the single most common source of “why doesn’t my tool show up?” confusion.
The YAML resolver keys everything by tool name so YAML lists stay flat, so a
@tool(name="weather_lookup") is referenceable as weather_lookup, not weather.weather_lookup. Fixed in #3107.
Configuration Options
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()
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
Project-Local Tools
Drop
@tool functions in .praisonai/tools/ and reference them from YAML agentsTool Discovery Order
The tier order that resolves a tool name across every surface
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

