Tool Availability Gating

Hide tools from the LLM when environment dependencies are missing

Tool availability gating filters unavailable tools at schema-build time, preventing the LLM from hallucinating calls to tools that can’t run.

python
import os
from praisonaiagents import Agent
from praisonaiagents import tool

@tool(availability=lambda: (bool(os.getenv("SERP_API_KEY")), "SERP_API_KEY not set"))
def search_web(query: str) -> str:
    """Search the web for information."""
    return f"Search results for: {query}"

agent = Agent(name="Researcher", tools=[search_web])
agent.start("Research quantum computing")
# Tool hidden if the key is missing. Once the key is set, a subsequent transient
# probe failure within 30 s keeps the tool available (last-good).

The user requests work that needs a gated tool; unavailable tools are hidden from the schema so the model cannot call them.

Diagram
ProbeRegistryAgentProbeRegistryAgentwithin 30 sdaemon momentarily busysustained outage (>30 s later)build schemacheck_availability()Truecache True (30 s) + stamp last_successtool in schemabuild schema againtool in schema (cached, no probe)build schema (cache TTL expired)check_availability()raises Exceptionlast_success < 30 s ago → serve last-goodtool STILL in schema (DEBUG log)build schemacheck_availability()raises Exceptiongrace expired → hide + cache False + WARNINGtool hidden
Diagram

Availability Gating

🔧 Tool Registry

✅ Check Available

📋 LLM Schema

🤖 Agent Uses Tool

Quick Start

  1. Decorator with Availability Check
    python
    import os
    from praisonaiagents import tool
    
    @tool(availability=lambda: (bool(os.getenv("SERP_API_KEY")), "SERP_API_KEY not set"))
    def search_web(query: str) -> str:
        """Search the web for information."""
        api_key = os.getenv("SERP_API_KEY")
        # ... search implementation
        return f"Search results for: {query}"
    
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Researcher", 
        instructions="Research topics the user asks about.",
        tools=[search_web]
    )
    # If SERP_API_KEY missing → tool hidden from LLM
    # If SERP_API_KEY set → tool appears and works normally
    agent.start("Research quantum computing")
  2. Class-Based Tool with Availability
    python
    from praisonaiagents import BaseTool
    
    class DatabaseTool(BaseTool):
        name = "query_database"
        description = "Query the application database"
    
        def __init__(self, connection_string: str = None):
            super().__init__()
            self.connection_string = connection_string or os.getenv("DATABASE_URL")
    
        def check_availability(self) -> tuple[bool, str]:
            if not self.connection_string:
                return False, "DATABASE_URL not configured"
    
            try:
                # Quick connection test (must be fast, no I/O heavy operations)
                import psycopg2
                return True, ""
            except ImportError:
                return False, "psycopg2 package not installed"
    
        def run(self, query: str) -> str:
            # Implementation here
            return f"Query result: {query}"

How It Works

Diagram
LLMToolRegistryAgentLLMToolRegistryAgentTool not visible to LLMalt[Available][Unavailable]Get available toolscheck_availability()(is_available, reason)Include in schemaCall toolSkip tool (hidden)
PhaseBehaviorPerformance
Schema BuildAvailability checks run onceZero runtime cost
Tool ExecutionOnly available tools includedProbes cached 30 s; no per-call overhead
LLM InteractionOnly sees usable toolsPrevents hallucination

Implementation Methods

Function Decorator

python
from praisonaiagents import tool
import os

# Simple environment check
@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing"))
def api_tool(query: str) -> str:
    return f"API result: {query}"

# Complex dependency check  
def check_docker_available():
    try:
        import docker
        client = docker.from_env()
        client.ping()  # Quick ping, not heavy I/O
        return True, ""
    except Exception as e:
        return False, f"Docker unavailable: {e}"

@tool(availability=check_docker_available)
def docker_command(cmd: str) -> str:
    """Run Docker commands."""
    return f"Docker: {cmd}"

BaseTool Protocol

python
from praisonaiagents import BaseTool
from praisonaiagents.tools.protocols import ToolAvailabilityProtocol

class CloudTool(BaseTool, ToolAvailabilityProtocol):
    name = "cloud_deploy"
    description = "Deploy to cloud services"

    def check_availability(self) -> tuple[bool, str]:
        # Check multiple dependencies
        if not os.getenv("AWS_ACCESS_KEY"):
            return False, "AWS credentials not configured"

        try:
            import boto3
            # Quick credential test (fast operation only)
            boto3.Session().get_credentials()
            return True, ""
        except Exception as e:
            return False, f"AWS SDK error: {e}"

    def run(self, service: str) -> str:
        return f"Deployed {service} to cloud"

Registry Functions

python
from praisonaiagents.tools import list_available_tools, get_registry

# Get only available tools
available_tools = list_available_tools()
print(f"Available: {len(available_tools)} tools")

# Get all tools (including unavailable)
all_tools = get_registry().list_tools()
print(f"Total registered: {len(all_tools)} tools")

# Check specific tool
web_tool = get_registry().get("search_web")
if hasattr(web_tool, 'check_availability'):
    is_available, reason = web_tool.check_availability()
    print(f"Web tool available: {is_available}")
    if not is_available:
        print(f"Reason: {reason}")

Availability Rules

Behavior Patterns

Diagram

Yes

No

No

Yes

True

False

Exception + last_success < 30 s ago

Exception + no recent success

Schema Build

Cached < 30 s?

Use cached result

Has check_availability?

Always Available

Run Check

Result?

Include in Schema
stamp last_success

Hide from LLM
cache False for 30 s

Serve last-good
debug log only

Hide from LLM
cache False + warning log

  1. No Check Method: Tool is always considered available
  2. Check Returns True: Tool included in LLM schema; last-success timestamp stamped
  3. Check Returns False: Tool hidden from LLM; cached False for 30 s
  4. Check Throws Exception: Flaky — served last-good within the grace window, otherwise hidden (see below)

The full behaviour, before and after the last-good grace window landed:

ScenarioBeforeAfter
Probe returns Truetool available, cached True for 30 sunchanged; last_success also stamped
Probe returns Falsetool hidden, cached False for 30 sunchanged
Probe raises (no prior success ever)tool hidden, cached False, WARNING loggedunchanged (hidden, WARNING)
Probe raises within 30 s of a recent successtool hidden, cached False, WARNING loggedtool stays available (last-good), no negative cached, DEBUG logged
Probe raises more than 30 s after last successtool hidden, cached False, WARNING loggedunchanged (hidden, WARNING)
register(tool_v2, name="x", overwrite=True) when x was recently healthyreplacement inherits cache + last-success — a broken v2 can look available for up to 30 scache + last-success evicted; the new tool starts clean
unregister("x") / clear()cache entry lingeredevicted; no stale growth

Exception Handling

A raised probe is treated as flaky, not final:

  • If the tool succeeded within the last 30 seconds, the last-good result is served and a DEBUG line is logged. The failure is not cached, so the next probe can recover immediately.
  • Otherwise (no prior success, or the last success is older than 30 s), the tool is hidden, cached as unavailable for 30 s, and a WARNING is logged.
python
# Result depends on when the last successful probe was:
def flaky_availability_check():
    raise ValueError("Network unreachable")

@tool(availability=flaky_availability_check)
def network_tool(host: str) -> str:
    return f"Ping {host}"

# Never succeeded / >30 s since last success:
# WARNING: Availability check failed for tool 'network_tool': Network unreachable
# → hidden from the LLM for 30 s

# Recent success then transient failure (within 30 s):
# DEBUG:   Availability check failed for tool 'network_tool' but serving last-good within grace window: Network unreachable
# → tool stays in the schema; no negative cached

This makes long-lived agents and gateways stable across brief daemon hiccups, import glitches, and network blips.

Plain Function Registry

python
from praisonaiagents import register_tool

def simple_function(text: str) -> str:
    return f"Processed: {text}"

# Plain functions are always available (no protocol support yet)
register_tool(simple_function)

Availability Caching

Availability checks are cached per-tool for 30 seconds so probes aren’t re-run on every schema build.

StateTTLBehaviour
Cached True30 sReused on every subsequent schema build; probe skipped
Cached False30 sReused; tool stays hidden without re-probing
Last successful probe timestamp30 sEnables the grace window (see Exception Handling)

The TTL and grace window are internal defaults (both 30 s) on ToolRegistry and are not user-tunable.

When cache entries are cleared:

  • registry.unregister(name) — evicts the tool’s cache and last-success timestamp.
  • registry.register(new_tool, name=..., overwrite=True) — when the replacement is a different instance, prior availability state is evicted so a broken replacement can’t inherit the previous tool’s “healthy” status.
  • registry.clear() — wipes everything.

Configuration Patterns

Environment-Based Availability

python
import os
from praisonaiagents import tool

def check_environment(required_vars: list[str]):
    """Factory for environment-based availability checks."""
    def check():
        missing = [var for var in required_vars if not os.getenv(var)]
        if missing:
            return False, f"Missing environment variables: {', '.join(missing)}"
        return True, ""
    return check

@tool(availability=check_environment(["OPENAI_API_KEY", "PINECONE_API_KEY"]))
def ai_research(topic: str) -> str:
    """Research topics using AI and vector search."""
    return f"AI research on: {topic}"

@tool(availability=check_environment(["SLACK_TOKEN"]))  
def notify_team(message: str) -> str:
    """Send notifications to team Slack."""
    return f"Notified team: {message}"

Service Discovery

python
from praisonaiagents import tool
import socket

def check_service_available(host: str, port: int):
    """Check if a network service is reachable."""
    def check():
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(2)  # Keep fast
                s.connect((host, port))
                return True, ""
        except Exception as e:
            return False, f"Service {host}:{port} unreachable: {e}"
    return check

@tool(availability=check_service_available("localhost", 5432))
def query_local_db(sql: str) -> str:
    """Query local PostgreSQL database."""
    return f"SQL result: {sql}"

@tool(availability=check_service_available("redis-server", 6379))
def cache_data(key: str, value: str) -> str:
    """Cache data in Redis."""
    return f"Cached {key}: {value}"

Conditional Tool Loading

python
from praisonaiagents import Agent
from praisonaiagents.tools import list_available_tools

# Build agent with only available tools
available_tools = list_available_tools()
print(f"Loading agent with {len(available_tools)} available tools")

agent = Agent(
    name="AdaptiveAgent",
    instructions="Use whatever tools are available in the current environment.",
    tools=available_tools
)

# Agent automatically adapts to environment capabilities

Best Practices

Keep Checks Fast

Availability checks run at schema-build time and must be fast (< 100ms recommended).

Good: Environment variable checks, import tests, quick pings

Bad: Full API calls, heavy file operations, long network requests

python
# Fast check
@tool(availability=lambda: (bool(os.getenv("API_KEY")), "API_KEY missing"))

# Slow check (avoid)
def slow_check():
    import requests
    requests.get("https://api.example.com/health", timeout=30)  # Too slow!
    return True, ""
Let the Registry Cache Your Probes

The registry already caches every probe result for 30 s — see Availability Caching. Don’t wrap probes in @lru_cache unless you need caching longer than 30 s.

Fail Fast on Missing Dependencies

Check critical dependencies first, avoid unnecessary work.

python
def check_ml_stack():
    # Check imports first (fast)
    try:
        import torch
        import transformers
    except ImportError as e:
        return False, f"Missing ML dependencies: {e}"

    # Then check GPU availability (slower)
    if not torch.cuda.is_available():
        return False, "CUDA not available"

    return True, ""
Graceful Degradation

Design tools to degrade gracefully when dependencies are partially available.

python
@tool(availability=lambda: (True, ""))  # Always available
def search_content(query: str) -> str:
    """Search using best available method."""

    # Try premium search first
    if os.getenv("PREMIUM_SEARCH_KEY"):
        return premium_search(query)

    # Fall back to basic search
    return basic_search(query)

Related