Understanding the Needle Tool‑Calling Contract: A Complete Technical Guide

Needle's tool‑calling contract is a typed, JSON‑schema‑based protocol that allows language models to safely invoke user‑defined Python functions through a structured "text in, JSON out" cycle.

The open‑source Needle framework implements a precise contract between language models and executable code. This contract ensures type safety, automatic schema generation, and deterministic execution loops—making it ideal for building reliable AI agents. In this guide, you'll learn exactly how Needle's tool‑calling contract works under the hood, with references to the actual source implementation.

What Is the Needle Tool‑Calling Contract?

The Needle tool‑calling contract defines how a language model communicates its intent to execute code and how the framework validates, runs, and returns results. The contract follows a strict "text → JSON → function → JSON → text" pipeline:

  • Text input: Natural language queries from users
  • JSON schema: Structured descriptions of available tools
  • Function execution: Validated Python function calls
  • JSON output: Structured results returned to the model
  • Final response: Synthesized answer after tool execution

This design ensures that models can only emit calls that conform to declared schemas, and the Python runtime safely executes those calls without arbitrary code injection.

How Tool Schemas Are Generated

The foundation of the contract lies in automatic JSON schema generation. When you apply the @needle.tool decorator to a function, Needle inspects its signature and generates a complete JSON schema.

The @tool Decorator Implementation

In needle/agent/tools.py, the tool decorator calls build_schema to analyze your function:

import needle

@needle.tool
def get_weather(city: str, units: str = "celsius") -> dict:
    """Get current weather conditions for a specified city.
    
    Args:
        city: The name of the city to query
        units: Temperature units, either "celsius" or "fahrenheit"
    
    Returns:
        Dictionary containing temperature and conditions
    """
    # Implementation would call a weather API

    return {
        "city": city,
        "temperature": 22 if units == "celsius" else 72,
        "conditions": "partly cloudy"
    }

The build_schema function (lines 15–46 in needle/agent/tools.py) performs three critical operations:

  1. Signature inspection: Extracts parameter names, types, and defaults using Python's inspect module
  2. Docstring parsing: Reads argument descriptions from Google‑style or NumPy‑style docstrings
  3. Schema construction: Outputs a JSON Schema object with name, description, and parameters fields

The resulting schema is stored in the function's __needle_schema__ attribute for runtime access.

The Needle Agent Run Loop

Once tools are defined, the Needle class orchestrates the execution cycle. The run loop is implemented in needle/__init__.py and handles the bidirectional communication between model and code.

Step‑by‑Step Execution Flow

Step 1: Schema registration

When you instantiate an agent, tool schemas populate the internal _functions map:

agent = needle.Needle(tools=[get_weather])

Step 2: Initial model call

Your query is sent via _complete to the underlying inference engine (lines 39–46). The model receives both your question and the available tool schemas.

Step 3: Function call detection

The model returns a structured response. If tool execution is needed, the response includes a "type": "function_call" field and a "function_calls" list containing the tool name and JSON‑encoded arguments.

Step 4: Execution and iteration

The Needle.run method (lines 47–60) implements the core loop:

response = agent.run(
    "What's the weather in Tokyo in fahrenheit?",
    max_steps=5  # Prevent infinite loops

)

For each iteration:

  • Parse the model's JSON response
  • Look up the corresponding Python function in _functions
  • Execute with json.loads()‑validated arguments
  • Append results to the conversation context
  • Re‑invoke _complete with updated context

Step 5: Result aggregation

After completion, the response object includes a "results" field (lines 58–60) containing an ordered list of all tool execution outcomes. This preserves the full execution trace for debugging and auditing.

Response Format and Type Safety

The contract enforces strict output formatting through two mechanisms.

Grammar‑Constrained Generation

As documented in the README (lines 9–11), Needle uses byte‑level grammar enforcement to constrain model outputs. The inference engine validates token sequences against the declared JSON schemas at generation time, preventing malformed tool calls before they occur.

Structured Response Objects

Every agent.run() call returns a dictionary with predictable structure:

response = agent.run("Compare weather in Paris and Rome")

print(response.keys())

# dict_keys(['text', 'results', 'usage', 'finish_reason'])

# Text contains the model's final synthesized answer

print(response["text"])

# "Paris is 18°C and sunny, while Rome is 24°C and clear..."

# Results contains ordered tool execution outputs

print(response["results"])

# [{'city': 'Paris', 'temperature': 18, 'conditions': 'sunny'},

#  {'city': 'Rome', 'temperature': 24, 'conditions': 'clear'}]

Practical Example: Building a Calculator Agent

Here's a complete implementation demonstrating the full contract:

import needle
from typing import Literal

# Define tools with precise type annotations

@needle.tool
def calculate(
    operation: Literal["add", "subtract", "multiply", "divide"],
    a: float,
    b: float
) -> dict:
    """Perform a basic arithmetic operation.
    
    Args:
        operation: The arithmetic operation to perform
        a: First operand
        b: Second operand
    
    Returns:
        Dictionary with result and operation metadata
    """
    ops = {
        "add": lambda x, y: x + y,
        "subtract": lambda x, y: x - y,
        "multiply": lambda x, y: x * y,
        "divide": lambda x, y: x / y if y != 0 else float("inf")
    }
    
    result = ops[operation](a, b)
    return {
        "operation": operation,
        "operands": [a, b],
        "result": result,
        "is_exact": result == int(result)
    }

@needle.tool
def format_currency(amount: float, currency: str = "USD") -> str:
    """Format a number as currency.
    
    Args:
        amount: The numeric amount to format
        currency: ISO 4217 currency code
    
    Returns:
        Formatted currency string
    """
    symbols = {"USD": "$", "EUR": "€", "GBP": "£"}
    symbol = symbols.get(currency, currency + " ")
    return f"{symbol}{amount:,.2f}"

# Instantiate agent with both tools

agent = needle.Needle(tools=[calculate, format_currency])

# Complex query requiring multiple tool calls

response = agent.run(
    "If I have $1,500 and invest it at 8% annual return for 5 years, "
    "what's the final amount in euros? First calculate 1500 * 1.08^5, "
    "then convert the result using a rate of 0.92 USD/EUR."
)

print(f"Final answer: {response['text']}")
print(f"Tool calls executed: {len(response['results'])}")

Structured Data Extraction Mode

Needle extends the same contract for extraction-only workflows via needle.extract(). This specialized mode treats a Pydantic model as a "virtual tool" with single‑shot execution:

from pydantic import BaseModel
import needle

class ResearchPaper(BaseModel):
    title: str
    authors: list[str]
    publication_year: int
    key_contribution: str
    methodology: Literal["experimental", "theoretical", "simulation"]

# The schema is generated internally and used for one-time extraction

paper = needle.extract(
    """
    In their 2023 Nature paper "Quantum Error Correction Below the Surface Code 
    Threshold", Google Quantum AI researchers demonstrated experimental 
    suppression of logical error rates using distance‑5 superconducting qubits.
    """,
    ResearchPaper
)

assert paper.publication_year == 2023
assert paper.methodology == "experimental"

This demonstrates the contract's flexibility: the same "JSON schema → validation → execution" pattern applies whether you're building interactive agents or one‑shot extractors.

Key Source Files

Path Purpose
needle/agent/tools.py @tool decorator and build_schema() implementation
needle/__init__.py Needle class, _complete() method, and run loop
README.md High‑level contract documentation and grammar constraints
needle/environments/*.py Production‑ready tool collections

Summary

  • Needle's tool‑calling contract guarantees type‑safe, schema‑validated execution through automatic JSON schema generation
  • The @needle.tool decorator in needle/agent/tools.py inspects Python signatures to build complete JSON Schema objects
  • The run loop in needle/__init__.py implements a "text in, JSON out" cycle with configurable max_steps iteration
  • Grammar‑level constraints prevent invalid tool calls at the token generation stage
  • Results are returned in a structured response object with full execution traceability via the "results" field

Frequently Asked Questions

How does Needle prevent the model from calling non‑existent tools?

Needle's grammar constraints and schema registration work together. Only tools passed to needle.Needle(tools=[...]) have their schemas included in the system prompt. The byte‑level grammar enforcer restricts the model's output to valid JSON matching only those registered schemas, making structurally invalid or unknown tool calls impossible to generate.

Can I use complex Pydantic models as tool parameters?

Yes. The build_schema function recursively processes type hints, including nested Pydantic models, lists, unions, and Literal types. Complex validation constraints from Pydantic Field() specifications are preserved in the generated JSON schema, though runtime validation currently relies on standard JSON decoding.

What happens if a tool raises an exception during execution?

The run loop catches exceptions and surfaces them to the model as structured error messages within the conversation context. The model can then decide whether to retry with modified parameters, call alternative tools, or report failure to the user. This error‑as‑context pattern enables robust recovery from expected failure modes.

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 →