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.
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.
Quick Start
- 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") - 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
| Phase | Behavior | Performance |
|---|---|---|
| Schema Build | Availability checks run once | Zero runtime cost |
| Tool Execution | Only available tools included | Probes cached 30 s; no per-call overhead |
| LLM Interaction | Only sees usable tools | Prevents hallucination |
Implementation Methods
Function Decorator
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
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
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
- No Check Method: Tool is always considered available
- Check Returns True: Tool included in LLM schema; last-success timestamp stamped
- Check Returns False: Tool hidden from LLM; cached
Falsefor 30 s - 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:
| Scenario | Before | After |
|---|---|---|
Probe returns True | tool available, cached True for 30 s | unchanged; last_success also stamped |
Probe returns False | tool hidden, cached False for 30 s | unchanged |
| Probe raises (no prior success ever) | tool hidden, cached False, WARNING logged | unchanged (hidden, WARNING) |
| Probe raises within 30 s of a recent success | tool hidden, cached False, WARNING logged | tool stays available (last-good), no negative cached, DEBUG logged |
| Probe raises more than 30 s after last success | tool hidden, cached False, WARNING logged | unchanged (hidden, WARNING) |
register(tool_v2, name="x", overwrite=True) when x was recently healthy | replacement inherits cache + last-success — a broken v2 can look available for up to 30 s | cache + last-success evicted; the new tool starts clean |
unregister("x") / clear() | cache entry lingered | evicted; 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
DEBUGline 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
WARNINGis logged.
# 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 cachedThis makes long-lived agents and gateways stable across brief daemon hiccups, import glitches, and network blips.
Plain Function Registry
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.
| State | TTL | Behaviour |
|---|---|---|
Cached True | 30 s | Reused on every subsequent schema build; probe skipped |
Cached False | 30 s | Reused; tool stays hidden without re-probing |
| Last successful probe timestamp | 30 s | Enables 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
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
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
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 capabilitiesBest 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
# 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.
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.
@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)