How to Debug "Unknown Tool" Errors in Needle: A Complete Troubleshooting Guide

Needle raises "unknown tool" errors when a function call name returned by the LLM cannot be found in the agent's internal _functions registry, which is populated at instantiation from the tools argument or via the @tool decorator.

The needle library from cactus-compute/needle resolves tool calls through a central registry that maps function names to callables. When the LLM returns a tool invocation that doesn't match any entry in this registry, Needle immediately returns an error instead of executing code. Understanding how this registry is built and queried is essential for resolving these debugging scenarios.

How Needle Registers Tools

Needle constructs the tool registry during agent instantiation in needle/__init__.py. The Needle class initializes a dictionary called _functions that stores all available tools for the session.

When you pass a list to the tools parameter, the _resolve() method processes each entry:


# From needle/__init__.py - the _resolve() logic

def _resolve(self, tool):
    if callable(tool):
        if hasattr(tool, "_needle_tool"):
            # From @tool decorator

            schema = tool._needle_tool
        else:
            # Plain function - build schema dynamically

            schema = build_schema(tool)
        self._functions[schema["name"]] = tool

Tools enter the registry in two ways:

  • Using the @tool decorator – Defined in needle/agent/tools.py, this decorator attaches a JSON schema dict to the function via the _needle_tool attribute. The _resolve() method extracts this schema and uses its "name" field as the registry key.

  • Passing plain callables – If a function lacks the _needle_tool attribute, build_schema() automatically generates a schema from the function's type hints and docstring.

Why "Unknown Tool" Errors Occur

When the LLM returns a function call, Needle performs a direct dictionary lookup in needle/__init__.py:

fn = self._functions.get(call.get("name"))
if fn is None:
    results.append({"error": "unknown tool: " + str(call.get("name"))})

If the "name" field from the LLM's JSON payload doesn't exist as a key in self._functions, Needle cannot locate the callable and returns the error. This lookup is case-sensitive and requires exact string matching.

Common Causes and Solutions

Tool Not Passed to Agent Constructor

The most common cause is forgetting to include the function in the tools list when creating the Needle instance.

Fix: Explicitly pass all tools to the constructor:

from needle import Needle

def my_function(x: int) -> int:
    return x * 2

# Missing tools=[my_function] causes the error

agent = Needle()  # Wrong

# Correct - function is registered in _functions

agent = Needle(tools=[my_function])

Name Mismatch Between Schema and LLM Call

The schema's "name" field must exactly match the name the LLM uses in its function call, including case and underscores.

Fix: Verify the registered name matches your prompt instructions:

print(agent._functions.keys())

# If the LLM calls "getWeather" but the key is "get_weather", the lookup fails

If you need a custom name that differs from the function name, pass a schema dict explicitly:

custom_schema = {
    "name": "weather_lookup",  # Must match what LLM uses

    "description": "Look up weather for a city.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
}

def get_weather(city: str):
    return f"Sunny in {city}"

agent = Needle(tools=[{"schema": custom_schema, "func": get_weather}])

Missing @tool Decorator

Without the @tool decorator, build_schema() attempts to infer the schema, but complex signatures or missing type hints may cause incomplete registration.

Fix: Always use the decorator for explicit schema control:

from needle import tool

@tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

agent = Needle(tools=[add])

Function Defined After Agent Creation

The _functions dictionary is immutable after instantiation. Adding new functions to your Python scope after creating the agent doesn't update the registry.

Fix: Define all tools before instantiation, or create a new Needle instance when tools change.

Incorrect Import Path or Stale Reference

Passing a module path that hasn't been imported, or using a stale cached object, results in the function reference not resolving correctly during _resolve().

Fix: Import the module explicitly before passing:

from mymodule import my_tool  # Ensure fresh import

agent = Needle(tools=[my_tool])

Unsupported Type Signatures

If build_schema() in needle/agent/tools.py encounters unsupported types (complex custom classes without Pydantic models), it may fail to generate a valid schema, preventing the tool from entering _functions.

Fix: Use supported type hints (int, str, list, dict, typing.Literal, typing.Optional) or wrap complex types in Pydantic models.

Debugging Steps

Inspect the Registry with agent._functions

Immediately after construction, dump the registry keys to verify registration:

agent = Needle(tools=[my_tool])
print("Registered tools:", list(agent._functions.keys()))

# Output should include: ['my_tool']

Verify the Generated Schema

Check that the _needle_tool attribute contains a valid name field:

print(my_tool._needle_tool)

# Verify: {'name': 'my_tool', 'description': '...', ...}

Enable Verbose Logging

Set the environment variable before instantiation to see internal library messages:

import os
os.environ["NEEDLE_LOG"] = "1"

from needle import Needle
agent = Needle(tools=[my_tool])

Validate the LLM Response

Inspect the raw LLM response to ensure the "name" field in the function call exactly matches your registered keys, including casing:


# Example problematic LLM response

{"name": "GetWeather", "arguments": {"city": "NYC"}}  # vs "get_weather"

Code Examples for Fixing Registration Issues

Proper registration with decorator:

from needle import Needle, tool, Field

@tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

agent = Needle(tools=[add])
response = agent.run("What is 3 plus 4?")
print(response["results"])  # → [{'result': 7}]

Registering a plain callable without decorator:

def greet(name: str) -> str:
    """Greet someone."""
    return f"Hello, {name}!"

# build_schema() creates the schema automatically

agent = Needle(tools=[greet])

Handling custom schema names:

custom_schema = {
    "name": "weather_lookup",
    "description": "Look up weather for a city.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name"}
        },
        "required": ["city"]
    }
}

def get_weather(city: str):
    return f"Sunny in {city}"

agent = Needle(tools=[{"schema": custom_schema, "func": get_weather}])

Summary

  • Needle stores tools in self._functions (defined in needle/__init__.py), populated only at agent instantiation via the _resolve() method.
  • The @tool decorator from needle/agent/tools.py attaches the _needle_tool schema attribute; without it, build_schema() attempts automatic inference.
  • "Unknown tool" errors occur when self._functions.get(call.get("name")) returns None, meaning the LLM's requested name isn't in the registry.
  • Common fixes include: ensuring exact name matching, passing tools to the constructor, using supported type hints, and defining tools before creating the Needle instance.
  • Debug by printing agent._functions.keys(), inspecting function._needle_tool, and enabling NEEDLE_LOG=1.

Frequently Asked Questions

Why does Needle say "unknown tool" when my function is clearly defined?

The function exists in your Python code but was never added to the agent's internal _functions dictionary. According to the source code in needle/__init__.py, this registry is built only during __init__ from the tools argument. If you defined the function after creating the agent or forgot to include it in the tools list, Needle cannot find it during the LLM function call lookup.

Can I add tools to a Needle agent after instantiation?

No. The _functions dictionary is populated once during construction and is not dynamic. If you need to add new capabilities, you must create a new Needle instance with the updated tools list. This design ensures the function registry remains immutable during the agent's execution lifecycle.

How do I check what name Needle expects for a tool?

Inspect the _needle_tool attribute attached by the decorator, or print the keys of agent._functions. The string key in that dictionary must exactly match the "name" field the LLM sends in its function call. For undecorated functions, build_schema() uses the function's __name__ attribute as the default.

What types are supported in tool function signatures?

Needle's build_schema() supports int, str, float, bool, list, dict, typing.Literal, typing.Optional, and Pydantic models. Complex custom classes without Pydantic definitions may cause schema generation to fail silently, preventing the tool from being registered in _functions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →