BEFORE_TOOL_DEFINITIONS Hook

Inspect and rewrite the tool definitions an agent advertises to the LLM before each request — filter tools, edit descriptions, or constrain schemas per session

The BEFORE_TOOL_DEFINITIONS hook fires right after the tool list is assembled for a request and before it reaches the LLM — letting you drop tools, rewrite descriptions, or constrain parameter schemas on the fly.

python
from praisonaiagents import Agent
from praisonaiagents.hooks import HookEvent, BeforeToolDefinitionsInput

def hide_dangerous(data: BeforeToolDefinitionsInput) -> None:
    data.tool_definitions[:] = [
        t for t in data.tool_definitions
        if t.get("function", {}).get("name") != "execute_shell"
    ]

agent = Agent(
    name="Safe Agent",
    instructions="Help with safe tasks only.",
    hooks={HookEvent.BEFORE_TOOL_DEFINITIONS: hide_dangerous},
)

agent.start("What can you do?")

The user prompts the agent; the hook trims tool definitions before the LLM sees them.

Diagram

Tool Definitions Hook

build tools

BeforeToolDefinitionsInput

mutations adopted in-place

🤖 Agent

📋 Deep Copy\ntool_definitions

🪝 BEFORE_TOOL_DEFINITIONS\nHook / Plugin

🧠 LLM Request

Quick Start

  1. Drop a Tool by Name

    Remove a tool from the LLM’s view for every request — the tool stays registered on the agent but the model never sees it:

    python
    from praisonaiagents import Agent
    from praisonaiagents.hooks import HookEvent, BeforeToolDefinitionsInput
    
    def hide_dangerous_tools(data: BeforeToolDefinitionsInput) -> None:
        data.tool_definitions[:] = [
            t for t in data.tool_definitions
            if t.get("function", {}).get("name") != "execute_shell"
        ]
    
    agent = Agent(
        name="Safe Agent",
        instructions="Help users with safe tasks only.",
        hooks={HookEvent.BEFORE_TOOL_DEFINITIONS: hide_dangerous_tools},
    )
    
    agent.start("What can you do?")
  2. Rewrite a Tool Description

    Append usage guidance to a tool description before it reaches the model:

    python
    from praisonaiagents import Agent
    from praisonaiagents.hooks import HookEvent, BeforeToolDefinitionsInput
    
    def annotate_tools(data: BeforeToolDefinitionsInput) -> None:
        for tool in data.tool_definitions:
            fn = tool.get("function", {})
            if fn.get("name") == "web_search":
                fn["description"] = (
                    fn.get("description", "") +
                    " Always cite the URL in your answer."
                )
    
    agent = Agent(
        name="Researcher",
        instructions="Search the web and cite sources.",
        hooks={HookEvent.BEFORE_TOOL_DEFINITIONS: annotate_tools},
    )
    
    agent.start("What is the latest on AI safety?")
  3. Async Hook

    The hook fires on both sync and async agent paths. Use an async function when you need async lookups:

    python
    import asyncio
    from praisonaiagents import Agent
    from praisonaiagents.hooks import HookEvent, BeforeToolDefinitionsInput
    
    async def filter_by_session(data: BeforeToolDefinitionsInput) -> None:
        allowed = await fetch_allowed_tools(data.session_id)
        data.tool_definitions[:] = [
            t for t in data.tool_definitions
            if t.get("function", {}).get("name") in allowed
        ]
    
    agent = Agent(
        name="Session-Scoped Agent",
        instructions="Operate within your session's permissions.",
        hooks={HookEvent.BEFORE_TOOL_DEFINITIONS: filter_by_session},
    )
  4. As a Plugin

    Plugins implement before_tool_definitions() as a method:

    python
    from praisonaiagents import Agent, Plugin
    from praisonaiagents.hooks import BeforeToolDefinitionsInput
    
    class ToolFilterPlugin(Plugin):
        def before_tool_definitions(self, data: BeforeToolDefinitionsInput) -> None:
            data.tool_definitions[:] = [
                t for t in data.tool_definitions
                if not t.get("function", {}).get("name", "").startswith("internal_")
            ]
    
    agent = Agent(
        name="Plugin Agent",
        instructions="Help the user.",
        plugins=[ToolFilterPlugin()],
    )

How It Works

Diagram
LLMBEFORE_TOOL_DEFINITIONS HookHookRegistryChatMixinAgentLLMBEFORE_TOOL_DEFINITIONS HookHookRegistryChatMixinAgentalt[no hook registered][hook registered]chat(user_message)format_tools() → tool_definitionshas_hooks(BEFORE_TOOL_DEFINITIONS)?falserequest with unchanged tool_definitions (no deep-copy)truedeep copy tool_definitionsBeforeToolDefinitionsInput(tool_definitions, model, agent_name, session_id)mutate tool_definitions in-placerequest with (possibly modified) tool_definitionsresponse

Deep copy guarantee (when a hook is registered): The agent deep-copies tool_definitions before passing them to the hook. This means:

  • Hook mutations cannot accidentally poison the agent’s internal tool cache.
  • Each request starts from the clean, registered tool list.
  • Only in-place mutations (data.tool_definitions[:] = ...) are adopted.

This event inherits the same registry, so multiple hooks run in priority order — see Ordering hooks.


BeforeToolDefinitionsInput Fields

FieldTypeDescription
tool_definitionsList[Dict[str, Any]]The assembled OpenAI-style tool definitions. Mutate in-place.
modelstrThe LLM model name for this request
agent_namestrName of the agent firing the hook
session_idstrSession identifier (for per-session filtering)

Mutation Pattern

Always mutate tool_definitions in-place with slice assignment. Reassigning the variable does nothing:

python
def hook(data: BeforeToolDefinitionsInput) -> None:
    # ✅ Correct — slice assignment mutates in-place
    data.tool_definitions[:] = [
        t for t in data.tool_definitions if keep(t)
    ]

    # ❌ Wrong — reassigning the variable is ignored by the runtime
    data.tool_definitions = [t for t in data.tool_definitions if keep(t)]

Best Practices

Zero overhead when no hook is registered

BEFORE_TOOL_DEFINITIONS costs nothing when no hook is registered — the runtime checks has_hooks() before doing any work (including the deep copy of tool_definitions) on both sync (chat) and async (achat) paths. The check is a lock-guarded any() with no snapshot copy or priority sort (PR #5853). Adding this hook is safe on the tool-calling hot path; the deep-copy and hook execution only run when you actually opt in.

python
from praisonaiagents import Agent
from praisonaiagents.hooks import HookEvent, BeforeToolDefinitionsInput

def read_file(path: str) -> str:
    """Read a file."""
    return f"contents of {path}"

def delete_file(path: str) -> str:
    """Delete a file."""
    return f"deleted {path}"

# No BEFORE_TOOL_DEFINITIONS hook registered → the runtime skips the deep-copy
# and hook plumbing entirely. Same cost as an agent that never had hooks wired up.
agent = Agent(name="Fast", instructions="Be helpful.", tools=[read_file, delete_file])
agent.start("Read notes.txt")

# Only pay the cost when you actually opt in:
def drop_delete(data: BeforeToolDefinitionsInput) -> None:
    data.tool_definitions[:] = [
        t for t in data.tool_definitions
        if t.get("function", {}).get("name") != "delete_file"
    ]

agent = Agent(
    name="Safer",
    instructions="Be helpful.",
    tools=[read_file, delete_file],
    hooks={HookEvent.BEFORE_TOOL_DEFINITIONS: drop_delete},
)
agent.start("Read notes.txt")   # Now the deep-copy + hook fire on every request.
Use session_id to scope per-user tool access

The session_id field lets you load per-session permissions from a store and filter tools accordingly. This is the recommended pattern for multi-tenant bots where different users get different tool sets.

Prefer description rewriting over tool removal

Removing a tool stops the model from calling it but can break agent plans that depend on it. Rewriting the description (e.g. adding "(disabled for this session)") lets the model know the tool exists but should not be called.

Keep hooks fast — they run on every request

The hook fires before every LLM call. Avoid slow I/O in sync hooks; use async hooks for database or network lookups.

Test with the deep-copy guarantee in mind

Because the runtime deep-copies before calling your hook, changes to one request’s tool list never affect the next. You cannot cache mutations across requests via the hook input itself.


Related