Tool Discovery Order

How PraisonAI resolves a tool name — local file, praisonaiagents built-ins, praisonai-tools, or a plugin

When you ask for tools=["read_notes"], PraisonAI checks four places in a fixed order and stops at the first match.

python
from praisonaiagents import Agent
import os

os.environ["PRAISONAI_ALLOW_LOCAL_TOOLS"] = "true"

agent = Agent(
    name="Notes Helper",
    instructions="Use local tools when available.",
    tools=["read_notes"],
)
agent.start("Read my latest notes")

The user lists a tool by name on Agent(…); PraisonAI resolves it through local files, built-ins, praisonai-tools, then plugins.

Diagram

Tool Discovery Pipeline

📄 tools.py or tools/

🔧 praisonaiagents.tools.TOOL_MAPPINGS

📦 praisonai-tools package

🔌 plugin registry entry_points

❌ not found

How It Works

Diagram
Tool ResolverAgentUserTool ResolverAgentUserAgent(tools=[“read_notes”])Resolve “read_notes”Check local → built-in → package → pluginFirst match winsRuns task with resolved tool

The same pipeline runs for Agent(tools=[...]), LocalManagedAgent(config=LocalManagedConfig(tools=[...])), YAML tools:, and CLI --tools.

Quick Start

  1. Enable Local Tools

    Set the environment variable to allow loading local tool files:

    bash
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true
  2. Create a Local Tool

    Create tools.py in your project directory:

    python
    def read_notes(query: str) -> str:
        """Read local notes matching the query."""
        return f"Notes about: {query}"
  3. Declare tools by name on your Agent
    python
    from praisonaiagents import Agent
    import os
    
    os.environ["PRAISONAI_ALLOW_LOCAL_TOOLS"] = "true"
    
    agent = Agent(
        name="Research assistant",
        instructions="Use available tools to answer questions.",
        tools=["read_notes", "internet_search"],
    )
    agent.start("What did I write about caching and what's new in the news?")
  4. PraisonAI resolves through the 4-layer pipeline

    PraisonAI finds read_notes from your local tools.py (Tier 1) and internet_search from the built-in SDK (Tier 2) — no extra configuration needed.


How It Works

The user names a tool; PraisonAI walks the four tiers in order and stops at the first match.

Diagram
tool_resolverAgentUsertool_resolverAgentUsertools=[“read_notes”]resolve(“read_notes”)Tier 1 local tools.py?Matched tool (first hit wins)Answer using the tool

The Four Tiers

From the tool_resolver.py module docstring — first match wins:

TierSourceWhen it wins
1Local tools.py (backward compat, custom tools, custom variables)Your own function with that name exists in tools.py, tools/, or .praisonai/tools/*.py (project-local or user-global)
2praisonaiagents.tools.TOOL_MAPPINGS (built-in SDK tools)The name is a built-in like internet_search, execute_code, etc.
3praisonai-tools package (external tools, optional)The name exists in the optional praisonai-tools install
4Tool registry (plugins via entry_points)A third-party plugin registered under the canonical praisonai.tools entry-point group (or the deprecated praisonaiagents.tools / praisonai.tool_sources aliases, which still resolve but emit a one-cycle DeprecationWarning)

Tier 1 now covers three concrete locations: (a) ./tools.py at your CWD, (b) a ./tools/ folder, and (c) .praisonai/tools/.py — walked from the project up to the git root, plus ~/.praisonai/tools/.py user-global. The .praisonai/tools/ layer is auto-loaded on praisonai run with no --tools flag; see Project-Local Tools.


Runtime string-name resolution inside an Agent

The four tiers above cover config-time resolution — tools.py/YAML names loaded by the CLI’s praisonai_code.tool_resolver. When you pass a tool name as a string to Agent(tools=[...]), the agent runtime uses a separate 3-tier resolver in praisonaiagents.tools.resolver at execution time.

python
from praisonaiagents import Agent

agent = Agent(
    name="Research assistant",
    instructions="Use available tools to answer questions.",
    tools=["internet_search", "my_registered_tool"],
)
agent.start("Search the news")

internet_search resolves from built-in TOOL_MAPPINGS; my_registered_tool resolves from the tool registry if you registered it. With only built-ins installed, the first name works out of the box.

The runtime resolver walks three tiers — first match wins:

TierSourceSDK location
1Tool registry (explicitly registered tools)praisonaiagents.tools.registry.get_registry().get(name)
2Built-in lazy toolspraisonaiagents.tools.TOOL_MAPPINGS[name]
3praisonai-tools package (optional)praisonai_tools.<name>
Diagram

🔌 Tool registry

🔧 TOOL_MAPPINGS built-ins

📦 praisonai-tools optional

⚠️ Not found → warn + skip

If a name resolves nowhere, the resolver logs a warning and skips it — the agent keeps running. A partially-installed or broken praisonai-tools no longer crashes the run either: the resolver flips its “available” flag off and continues.


Publishing a tool package

Publish a pip-installable tool under the canonical praisonai.tools entry-point group and agents resolve it by name — no manual import.

toml
[project.entry-points."praisonai.tools"]
my_tool = "my_package.tools:MyTool"

Once installed, the tool auto-resolves in Agent(tools=["my_tool"]), praisonai run --tools my_tool, and YAML tools: [my_tool].

python
from praisonaiagents import Agent

agent = Agent(
    name="Research assistant",
    instructions="Use available tools to answer questions.",
    tools=["my_tool"],
)
agent.start("Do the thing with my_tool")

The legacy groups praisonaiagents.tools and praisonai.tool_sources still resolve tools registered under them but emit a one-cycle DeprecationWarning on first discovery. On a name collision, the canonical group wins — an alias entry with the same name is skipped, never overwritten.

Diagram

Entry-point discovery

skipped on name
collision

skipped on name
collision

first hit

first hit

📦 praisonai.tools
(canonical)

🔌 ToolRegistry

⚠️ praisonaiagents.tools
(deprecated alias)

⚠️ praisonai.tool_sources
(deprecated alias)

DeprecationWarning

Precedence ladder: praisonai.tools (canonical, wins) > praisonaiagents.tools (deprecated alias) > praisonai.tool_sources (deprecated alias).


Precedence Example


When praisonai-tools Is Not Installed

As of PR #2550, failures from the praisonai-tools package are logged, not silently skipped. You’ll see them at LOGLEVEL=INFO:

Tool 'some_tool' exists in TOOL_MAPPINGS but failed to load: <error>

This means tool resolution failures are now visible and debuggable, rather than passing silently through to an unresolved tool error at runtime.


YAML tools: Blocks

resolve_all_from_yaml walks both the top-level tools: list and the agents: shape, so both formats work:

yaml
# Top-level tools list
tools:
  - internet_search
  - read_notes

# Per-agent tools (also supported)
agents:
  researcher:
    tools:
      - internet_search
  note_taker:
    tools:
      - read_notes

Tasks nested under agents can also declare their own tools: lists — the resolver walks all of them.

Referencing .praisonai/tools/ @tool functions

A @tool function dropped into .praisonai/tools/*.py is now resolvable by its tool name from any YAML tools: list — no explicit --tools flag, no tools.py in cwd required. The same PRAISONAI_ALLOW_LOCAL_TOOLS=true opt-in that gates praisonai run also gates the YAML resolver.

python
# .praisonai/tools/weather.py
from praisonaiagents import tool

@tool(name="weather_lookup")
def weather(city: str) -> str:
    """Get the weather for a city."""
    return f"sunny in {city}"
yaml
# agents.yaml — weather_lookup is resolved from .praisonai/tools/
roles:
  researcher:
    tools:
      - weather_lookup

The tool is keyed by the @tool(name="…") value if given, otherwise by the function __name__ — not by the module-namespaced form weather.weather_lookup. Fixed in #3107.


Backward Compatibility

The praisonai.* import paths for tool_resolver, _safe_loader, tool_registry, and _framework_availability stay live via identity-preserving sys.modules shims. Each shim does:

python
import praisonai_code.tool_resolver as _impl
sys.modules[__name__] = _impl

This means praisonai.tool_resolver is praisonai_code.tool_resolver — both names point at the same object. The same identity holds for _framework_availability, _safe_loader, and tool_registry, as asserted in test_c5_backward_compat.py::test_module_identity.


Best Practices

Prefer named tools over lambdas

Use string names like tools=["internet_search"] rather than inline lambdas. Named tools are resolved through the full 4-tier pipeline, making them easier to override and debug.

python
# ✅ Named tool — discoverable and overridable
agent = Agent(tools=["internet_search"])

# ⚠️ Inline lambda — bypasses discovery pipeline
agent = Agent(tools=[lambda q: q.upper()])
Use built-in tools first

Before writing a custom tools.py, check if PraisonAI already has the tool built-in. Run praisonai tools list to see all available built-in tools.

Enable local tools only when needed

Set PRAISONAI_ALLOW_LOCAL_TOOLS=true only in environments where you trust the local tools.py. This opt-in prevents accidental execution of untrusted code.

Gate local tools with PRAISONAI_ALLOW_LOCAL_TOOLS

The PRAISONAI_ALLOW_LOCAL_TOOLS environment variable must be set to true before Tier 1 (local tools.py) is consulted. This is an opt-in security gate — without it, tools.py is not loaded and the resolver logs why it was skipped (visible at LOGLEVEL=INFO). Only SDK built-ins and plugins are then considered.

bash
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
Understand name collision resolution

When the same name appears in multiple tiers, Tier 1 always wins. If you define def search(...) in tools.py and a plugin also registers search, your local version takes precedence. Use this intentionally to shadow or override built-in tools.

Register third-party tools via entry_points

Plugins register under the canonical praisonai.tools entry-point group in pyproject.toml:

toml
[project.entry-points."praisonai.tools"]
my_tool = "my_package.tools:MyTool"

Once installed, the tool auto-resolves by name in Agent(tools=["my_tool"]), praisonai run --tools my_tool, and YAML tools: [my_tool] — no manual import required.

The legacy groups praisonaiagents.tools and praisonai.tool_sources still resolve tools registered under them but emit a one-cycle DeprecationWarning on first discovery. On a name collision, the canonical group wins — an alias entry with the same name is skipped, never overwritten.

Debug resolution with LOGLEVEL=INFO

Set LOGLEVEL=INFO to see exactly which tier resolved each tool. The log line Resolved 'search' from source 'local-tools.py' confirms which source won.

bash
LOGLEVEL=INFO PRAISONAI_ALLOW_LOCAL_TOOLS=true python agent.py

Configuration Options

OptionTypeDefaultDescription
PRAISONAI_ALLOW_LOCAL_TOOLSenv var (bool-like string)falseOpt-in flag that enables loading local tools.py / tools/ (Tier 1) and .praisonai/tools/*.py @tool functions on the YAML resolver path (fixed in #3107).
LOGLEVELenv var (string)unsetSet to INFO to log which tier resolved each tool name.
Tool Resolver Python Reference

Python reference for the tool resolver module


Related