Skip to main content
ToolResolver is the one place PraisonAI looks for tools.py — whether it ships callables or BaseTool classes.
The user asks for a calculation; ToolResolver loads callables and BaseTool classes from tools.py for the agent.

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 ToolRegistrypraisonaiagents.toolspraisonai-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 the ToolRegistry for YAML pipeline access:
Then reference in your agents.yaml:
For bulk registration from a module:

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.
If you use the module-level helpers (resolve(), resolve_many(), list_available(), etc.) they share a single process-default ToolResolver. Call reset_default_resolver() between tenants or after os.chdir() — otherwise the first caller’s tools.py will be served to everyone.

When to call reset_default_resolver()


Sharing one resolver across the run

Tool discovery — scanning tools.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.
Under the hood, _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 into AgentsGenerator:
This composes with the existing 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.
praisonai --auto builds an AutoGenerator for planning and an AgentsGenerator for execution in the same process. Since PR #3067 both share one warm resolver cache via the context-local default — installed-tool discovery runs once per run, not twice.
The 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:
Example tools.py with BaseTool classes:

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.
All four methods enforce the 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.
Migration from AgentsGenerator (PR #2017). AgentsGenerator.load_tools_from_module, load_tools_from_module_class, and load_tools_from_package have been removed. Use the equivalent ToolResolver methods above. The new versions enforce the PRAISONAI_ALLOW_LOCAL_TOOLS security gate uniformly.
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


Local Tool Override Warning

When a class-based tool in your local tools/ 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:
This warns you when a local definition silently wins over a built-in or registered tool of the same name. To resolve it, either rename your local tool or remove the conflicting source from the chain.

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
See Security Environment Variables for details.

Per-Context Resolver (Multi-Project Safety)

When you call resolve_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
The 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_lock for concurrent access
Resolve cache:
  • 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 as None so repeated lookups don’t walk the ladder again
  • Transient failures are NOT cached: If praisonaiagents fails to import (ImportError) or an entry in TOOL_MAPPINGS exists but its optional dependency failed to load, the lookup is retried on the next call. Install the missing package and the next resolve() picks it up — no clear_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 on register_function() / clear(). Resolvers held only by ToolRegistry are 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_lock for concurrent access. Fixed in PR #2147 — an earlier lock-free fast-path read could race with ToolRegistry.invalidate() and miss freshly-registered tools; the cache lookup now happens inside the lock.
Before PR #2079, missing optional dependencies could get stuck as None in the cache. Install the dep, call resolve() again, and it just works.

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 register ToolSource 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.
The built-in chain (local tools.py → wrapper registry → praisonaiagentspraisonai-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.pywrapper-registrypraisonaiagentspraisonai-toolscore-registry). Prepend your custom source to search it first while keeping all built-in sources:
Auto-wiring with 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:
Class tools and the cache: When 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

Call reset_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.
This clears the context-local resolver cache, forcing the next tool resolution to create a fresh resolver anchored to the current working directory. clear_cache() now clears both caches — useful after editing tools.py and after registering new tools in the wrapper ToolRegistry at runtime.

Best Practices

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.
Use plain Python functions for praisonaiagents agents. Reserve BaseTool classes for crewai-style flows or when you need complex tool state management.
Don’t import ToolResolver from praisonaiagents — it lives in the wrapper at praisonai.tool_resolver. The wrapper handles YAML-based tool resolution.
Set PRAISONAI_ALLOW_LOCAL_TOOLS=true only in development or trusted deployment environments. This prevents arbitrary code execution from untrusted working directories.

Project-Local Tools

Drop @tool functions in .praisonai/tools/ and reference them from YAML agents

Tool 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