Dynamic Tool Schemas

Update tool schemas at runtime so agents always see current parameter limits and descriptions

python
from praisonaiagents import Agent, tool

@tool
def search(query: str, limit: int = 10) -> str:
    return f"Results for {query} (limit={limit})"

agent = Agent(name="assistant", tools=[search])
agent.start("Find recent papers on context compaction.")

Dynamic tool schemas let your tool’s parameter limits and descriptions reflect live configuration, so the agent always sees the truth.

The user invokes the agent; dynamic schema overrides refresh parameter limits before the LLM sees the tool definition.

Diagram

Dynamic Schema Flow

📋 Request

🔧 Tool Registry

âš¡ Override Callable

📊 Schema Update

🤖 LLM Receives Schema

Quick Start

  1. Simple Agent with Dynamic Tool

    Use the @tool decorator with dynamic_schema_overrides to create a tool whose schema changes based on runtime state.

    python
    from praisonaiagents import Agent, tool
    
    # Runtime configuration that can change
    current_limit = {"max_concurrent": 1}
    
    @tool(
        dynamic_schema_overrides=lambda base: {
            **base,
            "function": {
                **base["function"],
                "description": f"Delegate a task. Current concurrency limit: {current_limit['max_concurrent']}",
                "parameters": {
                    **base["function"]["parameters"],
                    "properties": {
                        **base["function"]["parameters"]["properties"],
                        "priority": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": current_limit["max_concurrent"],
                            "description": f"Task priority (1-{current_limit['max_concurrent']})"
                        }
                    }
                }
            },
        }
    )
    def delegate_task(task: str, priority: int = 1) -> str:
        return f"Delegated: {task} (priority: {priority})"
    
    agent = Agent(
        name="Coordinator",
        instructions="Delegate work to sub-agents using available priority levels.",
        tools=[delegate_task],
    )
    
    # The schema reflects current limits
    current_limit["max_concurrent"] = 5  # Schema updates on next agent turn
    agent.start("Delegate three research tasks with different priorities")

    The user delegates research; dynamic tool schemas expose only the fields each sub-task needs.

  2. Using BaseTool Subclass

    For more control, subclass BaseTool and provide the override in the constructor.

    python
    from praisonaiagents import Agent, BaseTool
    
    class ConfigurableSearchTool(BaseTool):
        def __init__(self, api_keys_available: dict):
            self.api_keys_available = api_keys_available
            super().__init__(
                dynamic_schema_overrides=self._update_schema_for_available_sources
            )
    
        def _update_schema_for_available_sources(self, base_schema):
            # Filter enum options based on available API keys
            available_sources = [
                source for source, has_key in self.api_keys_available.items() 
                if has_key
            ]
    
            return {
                **base_schema,
                "function": {
                    **base_schema["function"],
                    "parameters": {
                        **base_schema["function"]["parameters"],
                        "properties": {
                            **base_schema["function"]["parameters"]["properties"],
                            "source": {
                                "type": "string",
                                "enum": available_sources,
                                "description": f"Search source. Available: {', '.join(available_sources)}"
                            }
                        }
                    }
                }
            }
    
        def run(self, query: str, source: str) -> str:
            return f"Searched '{query}' using {source}"
    
    # Configure available APIs
    search_tool = ConfigurableSearchTool({
        "google": True,
        "bing": False,
        "duckduckgo": True
    })
    
    agent = Agent(
        name="Researcher", 
        tools=[search_tool]
    )
    
    agent.start("Research the latest AI developments")

How It Works

Diagram
LLMOverrideRegistryAgentUserLLMOverrideRegistryAgentUserRequest with toolsget_tool_definitions()Call override functionModified schemaUpdated tool definitionsSend tools with current schemaResponse using correct limits

The override function is called every time the agent needs tool definitions, ensuring schemas always reflect current runtime state.

ComponentPurpose
Override FunctionUpdates schema based on current configuration
RegistryManages tools and applies overrides on each read
Base SchemaOriginal tool parameter structure
Runtime StateCurrent limits, API keys, or other dynamic values

Filtering the advertised schema

get_tool_definitions() accepts an optional permission_resolver= — a (tool_name) -> bool callable that drops a tool from the advertised schema when it returns False (deny = hide). Passing None (the default) advertises every available tool, so existing calls stay fully backward-compatible.

python
from praisonaiagents.tools.registry import get_tool_definitions
from praisonaiagents.permissions import PermissionManager

mgr = PermissionManager()
mgr.load_rules_from_config({"write_file": "deny"})

# Deny hides the tool from the advertised schema
defs = get_tool_definitions(permission_resolver=lambda name: not mgr.is_denied(name))
ArgTypeDefaultDescription
permission_resolverCallable[[str], bool] \NoneNoneReturns False to drop a tool from the schema (deny = hide). Fail-open on error.

Common Patterns

Pattern A: Concurrency Limits

Reflect runtime concurrency settings in parameter constraints.

python
import os

@tool(
    dynamic_schema_overrides=lambda base: {
        **base,
        "function": {
            **base["function"],
            "parameters": {
                **base["function"]["parameters"],
                "properties": {
                    **base["function"]["parameters"]["properties"],
                    "max_workers": {
                        "type": "integer",
                        "maximum": os.cpu_count(),
                        "description": f"Max workers (system has {os.cpu_count()} CPUs)"
                    }
                }
            }
        }
    }
)
def parallel_process(data: list, max_workers: int = 1) -> list:
    return [f"processed_{item}" for item in data]

Pattern B: API Key Filtering

Show only services with valid API keys in enum options.

python
import os

def get_available_models():
    available = []
    if os.getenv("OPENAI_API_KEY"):
        available.extend(["gpt-4", "gpt-3.5-turbo"])
    if os.getenv("ANTHROPIC_API_KEY"):
        available.extend(["claude-3-opus", "claude-3-sonnet"])
    return available

@tool(
    dynamic_schema_overrides=lambda base: {
        **base,
        "function": {
            **base["function"],
            "parameters": {
                **base["function"]["parameters"],
                "properties": {
                    **base["function"]["parameters"]["properties"],
                    "model": {
                        "type": "string",
                        "enum": get_available_models(),
                        "description": "Available AI models"
                    }
                }
            }
        }
    }
)
def generate_text(prompt: str, model: str) -> str:
    return f"Generated text using {model}: {prompt}"

Pattern C: Live Status Updates

Include current quotas or status information in tool descriptions.

python
@tool(
    dynamic_schema_overrides=lambda base: {
        **base,
        "function": {
            **base["function"],
            "description": f"{base['function']['description']} (Quota: {get_remaining_quota()}/1000)"
        }
    }
)
def api_call(endpoint: str) -> dict:
    return {"result": f"Called {endpoint}"}

Best Practices

Keep Override Functions Fast and Pure

Override functions run on every schema read, so avoid expensive operations like network calls or file I/O. Cache expensive computations and update them periodically rather than computing on each schema request.

python
# Good: Fast computation
@tool(dynamic_schema_overrides=lambda base: update_cpu_count(base))

# Avoid: Slow network calls in override
@tool(dynamic_schema_overrides=lambda base: fetch_api_limits_from_server(base))
Return Fresh Dict Objects

Always return a new dictionary rather than mutating the input schema. The base schema is already a deep copy, but your modifications should create new objects to avoid side effects.

python
# Good: Create new structure
def my_override(base_schema):
    return {
        **base_schema,
        "function": {
            **base_schema["function"],
            "description": "Updated description"
        }
    }

# Avoid: Mutating input
def bad_override(base_schema):
    base_schema["function"]["description"] = "Updated"  # Don't mutate
    return base_schema
Handle Failures Gracefully

If your override function raises an exception, the tool falls back to its base schema with a warning logged. Don’t rely on the override for critical functionality—it should enhance the schema, not make it functional.

python
def safe_override(base_schema):
    try:
        return update_with_live_config(base_schema)
    except Exception as e:
        # Log the error but don't re-raise
        logging.warning(f"Schema override failed: {e}")
        return base_schema  # Graceful fallback
Use Decorator Form for Simple Functions

Prefer the @tool(dynamic_schema_overrides=...) decorator for plain functions. Only subclass BaseTool when you need richer lifecycle methods or complex state management.

python
# Preferred: Simple decorator
@tool(dynamic_schema_overrides=lambda base: update_schema(base))
def my_function(x: str) -> str:
    return x

# Use subclass only when needed
class ComplexTool(BaseTool):
    def __init__(self):
        super().__init__(dynamic_schema_overrides=self.complex_override)

Related