Pi Agent Web Search Tool Fix in 2026
Pi Coding Agent users struggle with tool registration. Models say they lack access even when tools are in the prompt. The fix: register at API level, not prompt level.
Pi Coding Agent users report that models say they cannot access web search tools even when the tool is defined in the system prompt. The fix: tool registration must happen at the system level through the extension API, not just in the prompt text. Putting a tool description in the system prompt tells the model about the tool but does not actually make it callable.
The common failure pattern
The typical setup looks like this: a developer adds a search tool description to their system prompt, expecting the model to call it when it needs web data. Instead, the model responds with some variation of: I do not have access to exa_web_search or I cannot use external tools.
# What does NOT work: tool in system prompt only
system_prompt = """
You are a helpful coding agent.
You have access to these tools:
- exa_web_search(query: str) -> list[dict]: Search the web
- read_file(path: str) -> str: Read a file
"""
# The model sees the tool description but cannot actually call it
# because no tool is registered at the API/framework levelThis confusion arises because documentation examples often mix prompt-level tool descriptions with actual tool registration. The model needs both: a registered tool function it can invoke, and optionally a system prompt description explaining when to use it.
How tool registration actually works
Tool calling in LLM agents requires registering tools at the API level, not the prompt level. The difference matters:
- Prompt-level: Text in the system message describing a tool. The model reads this but has no mechanism to execute the function.
- API-level: A tool definition passed in the tools parameter of the API call. The model can generate a tool call response, and the framework executes it.
# What DOES work: proper tool registration
import json
def web_search(query: str, num_results: int = 5) -> str:
"""Search the web and return results."""
import requests, os
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": os.environ["SCAVIO_API_KEY"]},
json={"query": query, "num_results": num_results}
)
return json.dumps(resp.json().get("organic_results", []))
# Register the tool with the model via API
tools = [
{
"type": "function",
"function": {
"name": "web_search",
"description": "Search the web for current information",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"num_results": {
"type": "integer",
"description": "Number of results",
"default": 5
}
},
"required": ["query"]
}
}
}
]
# Pass tools in the API call, not just in the prompt
response = client.chat.completions.create(
model="your-model",
messages=messages,
tools=tools
)Pi Agent extension API specifics
Pi Coding Agent uses an extension system for custom tools. The extension must be registered through the Pi configuration, not just referenced in prompts. The extension API expects a specific format for tool definitions that maps to the underlying model tool-calling protocol.
Common mistakes with Pi Agent tool registration:
- Defining the tool in the prompt but not in the extension config
- Using an incompatible tool schema format (missing required fields like parameter types)
- Not handling the tool response format the model expects (it needs to be a string, not a raw object)
- Forgetting to set environment variables that the tool function needs (API keys)
Multi-backend search architecture
A robust search tool should support multiple backends so the agent continues working if one provider is down or rate-limited. This pattern works across Pi Agent, LangChain, and custom frameworks.
import requests, os, json
class MultiSearchTool:
"""Search tool with fallback backends."""
def __init__(self):
self.backends = []
if os.environ.get("SCAVIO_API_KEY"):
self.backends.append(self._scavio_search)
if os.environ.get("BRAVE_API_KEY"):
self.backends.append(self._brave_search)
def search(self, query: str, num_results: int = 5) -> str:
for backend in self.backends:
try:
results = backend(query, num_results)
if results:
return json.dumps(results)
except Exception:
continue
return json.dumps({"error": "All search backends failed"})
def _scavio_search(self, query, num_results):
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": os.environ["SCAVIO_API_KEY"]},
json={"query": query, "num_results": num_results}
)
resp.raise_for_status()
return resp.json().get("organic_results", [])
def _brave_search(self, query, num_results):
resp = requests.get(
"https://api.search.brave.com/res/v1/web/search",
headers={"X-Subscription-Token": os.environ["BRAVE_API_KEY"]},
params={"q": query, "count": num_results}
)
resp.raise_for_status()
return resp.json().get("web", {}).get("results", [])Using MCP as a drop-in solution
If your agent framework supports MCP (Model Context Protocol), you can skip custom tool registration entirely. MCP servers expose tools through a standardized protocol that the framework handles automatically.
{
"mcpServers": {
"scavio": {
"command": "npx",
"args": ["-y", "scavio-search-mcp"],
"env": {
"SCAVIO_API_KEY": "your-key-here"
}
}
}
}With MCP, the tool is registered at the framework level automatically. The model receives the tool schema through the protocol, not through prompt text. This eliminates the most common source of tool registration failures.
Debugging checklist
When your agent says it cannot access a tool, work through this checklist in order: verify the tool is registered at the API/framework level (not just in the prompt), confirm the tool schema matches what the model expects, check that the tool function returns a string (not a dict or object), verify environment variables are set in the execution environment, and test the tool function independently before connecting it to the agent.