How to Debug Tool Calling Failures and Improve Tool Descriptions in Needle

TLDR: Debug tool calling failures in Needle by inspecting fn._needle_tool to verify generated schemas, fix optional parameter detection with proper type hints, and strengthen descriptions using needle.Field constraints and Literal types—all implemented in needle/agent/tools.py.

Needle transforms Python functions into tool schemas that LLMs can invoke. When a tool call fails, the root cause typically traces back to schema generation errors or inadequate descriptions. Understanding how build_schema() in needle/agent/tools.py constructs these schemas gives you precise control over debugging and optimization.

Inspect the Generated Schema

Every @tool decorator attaches a JSON schema to your function via the _needle_tool attribute. Start debugging by printing this schema:

import needle
from needle import tool, Field

@tool
def set_thermostat(temp: int, mode: str = "auto"):
    """Set thermostat.

    Args:
        temp: target temperature in Celsius
        mode: heating mode (auto, heat, cool)
    """
    return {"temp": temp, "mode": mode}

# Inspect the generated schema

print(set_thermostat._needle_tool)

Critical Schema Fields to Verify

Field Expected Value Source Location
parameters.properties[name]["type"] Valid JSON Schema type ("integer", "string", etc.) _json_type() at lines 56–81 in tools.py
parameters.required List of parameters without defaults build_schema() at lines 30–36 in tools.py
Property description Extracted from Args block or Field(description=...) _parse_doc() at lines 94–108 in tools.py
Top-level description First line of docstring Line 38 in tools.py

Missing or incorrect values here cause the model to refuse the call (empty function_calls) or generate malformed JSON.

Debug Common Schema Problems

Required Arguments That Should Be Optional

Parameters without defaults become required. The helper _is_optional() (lines 51–53) detects optional types:

import needle.agent.tools
from typing import Optional

def correct_optional(city: str, units: Optional[str] = None):
    pass

schema = needle.agent.tools.build_schema(correct_optional)
print(schema["parameters"]["required"])  # → ['city'] ✅

Common pitfall: On Python < 3.10, typing.Union[int, None] without proper handling may fail detection. Use Optional[T] or Python 3.10+ union syntax (T | None).

Union Types Collapse to First Non-None Type

def multi_type(x: int | str):
    pass

schema = needle.agent.tools.build_schema(multi_type)
print(schema["parameters"]["properties"]["x"])  # → {"type": "integer"}

The schema builder prefers the first non-None type (lines 77–81). For full anyOf support, use Literal to constrain valid values instead.

Enum Constraints with Literal Types

from typing import Literal

@tool
def set_mode(mode: Literal["heat", "cool", "auto"]):
    """Change HVAC mode."""
    return {"mode": mode}

# Verify enum generation

print(set_mode._needle_tool["parameters"]["properties"]["mode"])

# → {"type": "string", "enum": ["heat", "cool", "auto"]}

Literal handling resides at line 70 in _json_type(). If the enum is missing, confirm you're importing from typing (not typing_extensions unless backporting) and running Python ≥ 3.8.

Strengthen Tool Descriptions

Robust descriptions improve both model accuracy and retrieval ranking when using large tool catalogs.

Use Google-Style Args with Field Constraints

from typing import Annotated
from needle import tool, Field

@tool
def send_payment(
    amount: Annotated[float, Field(gt=0, le=10000, description="USD amount")],
    recipient: Annotated[str, Field(pattern=r"^@[a-z0-9_]+$", description="Handle format: @username")],
    memo: Annotated[str, Field(max_length=80)] = ""
):
    """Send payment to a user.

    Args:
        amount: transfer amount in USD
        recipient: destination handle
        memo: optional transaction note
    """
    return {"sent": amount, "to": recipient, "note": memo}

Effects on schema generation:

  • Field constraints (gt, le, pattern, max_length) inject JSON Schema validation keywords via Field.apply() (lines 35–48)
  • Descriptions populate both parameter and top-level schema fields
  • Constraints become part of the constrained decoding grammar

Prefer Literal for Fixed Choices

@tool
def set_priority(level: Literal["low", "medium", "high"] = "medium"):
    """Set task priority level."""
    return {"priority": level}

Literal creates enforceable enums that prevent hallucinated values. The decoder respects these constraints at inference time.

Validate Final Schema Output

import json

print(json.dumps(send_payment._needle_tool, indent=2))

A complete, optimized schema contains:

  • "type": "object" at the parameters level
  • Each property with "type", "description", and constraint keywords
  • "required" listing only non-default, non-optional parameters
  • Concise, informative top-level "description"

Verify Tool Retrieval for Large Catalogs

Needle uses contrastive retrieval when you exceed 5 tools. If tools go unselected:

  1. Add distinctive keywords to descriptions
  2. Keep schemas concise—large schemas dilute embeddings
  3. Pre-compute embeddings for faster iteration:
needle fetch --out ~/.cache/cactus-needle
needle build --tool-index-path my_index.pkl

The retrieval logic is documented in doc/apis.md – Tool retrieval.

Debug Retrieval Failures

import json
from needle import Needle, tool

# Create 7 similar tools

tools = []
for i in range(7):
    @tool
    def make_tool(i=i):
        f"""Execute operation {i}."""
        return {"op": i}
    tools.append(make_tool)

agent = Needle(tools=tools)
response = agent.run("execute operation 6")
print(json.dumps(response.get("function_calls", []), indent=2))

Empty function_calls indicates a retrieval failure. Resolve by making target tool descriptions more distinctive or reducing catalog size.

Complete Debugging Checklist

  • Print fn._needle_tool and verify types, required list, and descriptions
  • Run tests/test_tools.py unit tests to validate schema generation
  • Confirm Field constraints appear as JSON Schema keywords (minimum, pattern, etc.)
  • Check Literal arguments render as "enum" entries
  • Verify _parse_doc() extracts descriptions from Args blocks
  • Test retrieval with agent.run() and inspect response["function_calls"]
  • Add end-to-end test: query → expected tool → validated result

Summary

  • Inspect schemas via fn._needle_tool to catch generation errors early
  • Fix optional detection using Optional[T] or T | None syntax recognized by _is_optional()
  • Strengthen constraints with Field parameters and Literal types for precise validation
  • Optimize descriptions using Google-style docstrings that _parse_doc() extracts correctly
  • Debug retrieval by adding distinctive keywords and keeping schemas compact for the contrastive head

Frequently Asked Questions

Why does my tool argument appear as required when it has a default value?

The build_schema() function at lines 30–36 checks defaults via function introspection. If your parameter uses a mutable default or the inspection fails, verify with inspect.signature(fn).parameters[name].default directly. The _is_optional() helper (lines 51–53) separately checks type annotations for None compatibility.

How do I add regex patterns or numeric ranges to my tool schema?

Use needle.Field with Annotated types: Annotated[str, Field(pattern=r"^...", description="...")] or Annotated[int, Field(gt=0, le=100)]. The Field.apply() method at lines 35–48 maps these to JSON Schema keywords that become part of the constrained decoding grammar.

Why is my tool never selected when I have many tools?

Needle's retrieval head (documented in doc/apis.md) embeds tool descriptions and selects the top-5 most relevant. Improve selection by: using unique, descriptive keywords; keeping schemas compact; or pre-computing embeddings with --tool-index-path for consistent retrieval testing.

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 →