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

> Debug Needle tool calling failures by inspecting fn._needle_tool, fixing optional parameter detection, and improving descriptions with Field constraints and Literal types. Learn how to enhance your tool definitions.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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:

```python
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`](https://github.com/cactus-compute/needle/blob/main/tools.py) |
| `parameters.required` | List of parameters without defaults | `build_schema()` at lines 30–36 in [`tools.py`](https://github.com/cactus-compute/needle/blob/main/tools.py) |
| Property `description` | Extracted from *Args* block or `Field(description=...)` | `_parse_doc()` at lines 94–108 in [`tools.py`](https://github.com/cactus-compute/needle/blob/main/tools.py) |
| Top-level `description` | First line of docstring | Line 38 in [`tools.py`](https://github.com/cactus-compute/needle/blob/main/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:

```python
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

```python
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

```python
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

```python
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

```python
@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

```python
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:

```bash
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](https://github.com/cactus-compute/needle/blob/main/doc/apis.md#tool-retrieval).

### Debug Retrieval Failures

```python
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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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.