How Tool Schema Validation Works with the `Field` Class in Needle 2

In Needle 2, tool schema validation is performed by the Field class in needle/agent/tools.py, which captures parameter constraints and applies them to JSON Schema generation through the build_schema function and _needle_tool attribute.

The Field class provides a declarative way to specify validation rules for function parameters that are exposed as LLM-callable tools. When you decorate a function with @tool, Needle 2 automatically inspects type annotations, extracts Field constraints, and produces a JSON Schema that validates incoming arguments before your Python code ever runs.

Where Field Lives and How It's Defined

The Field class is implemented in needle/agent/tools.py at lines 18-31. Its __init__ method accepts standard JSON Schema constraint keywords:

  • ge / le — greater-than-or-equal / less-than-or-equal (numeric bounds)
  • gt / lt — strict inequality bounds
  • min_length / max_length — string length constraints
  • pattern — regex validation
  • enum — allowed value enumeration
  • description — human-readable documentation

These values are stored internally and later copied into the generated schema.

The Schema Building Pipeline

Step 1: Extracting Field from Annotations

The _field_of helper function (lines 89-96 in needle/agent/tools.py) inspects parameter annotations. When it encounters Annotated[T, Field(...)], it returns the Field instance.

Step 2: Applying Constraints via field.apply()

The Field.apply() method (lines 36-49) merges stored constraints into the schema dictionary:

from typing import Annotated
from needle.agent.tools import Field, tool

@tool
def configure_timeout(
    seconds: Annotated[int, Field(ge=1, le=300, description="Timeout in seconds")],
    retries: Annotated[int, Field(ge=0, le=5)] = 3
) -> dict:
    """Configure request timeout and retry policy."""
    return {"timeout": seconds, "retries": retries}

print(configure_timeout._needle_tool)

Output:

{
  "name": "configure_timeout",
  "description": "Configure request timeout and retry policy.",
  "parameters": {
    "type": "object",
    "properties": {
      "seconds": {
        "type": "integer",
        "description": "Timeout in seconds",
        "minimum": 1,
        "maximum": 300
      },
      "retries": {
        "type": "integer",
        "default": 3,
        "minimum": 0,
        "maximum": 5
      }
    },
    "required": ["seconds"]
  }
}

Note that retries has a default value, so it is excluded from "required" — this logic is handled at lines 34-38 of needle/agent/tools.py.

Step 3: Attaching Schema with the @tool Decorator

The @tool decorator (lines 68-70) invokes build_schema and stores the result as _needle_tool on the function object:


# The decorator does this internally:

def tool(fn):
    fn._needle_tool = build_schema(fn)
    return fn

Validation Constraints You Can Specify with Field

Constraint JSON Schema Key Typical Use
ge / le minimum / maximum Numeric ranges
gt / lt exclusiveMinimum / exclusiveMaximum Strict bounds
min_length / max_length minLength / maxLength String validation
pattern pattern Regex matching
enum enum Whitelist values
description description Documentation

Complete Example: String and Enum Validation

from typing import Annotated
from needle.agent.tools import Field, tool

@tool
def search_users(
    query: Annotated[str, Field(min_length=2, max_length=100, description="Search query")],
    role: Annotated[str, Field(enum=["admin", "editor", "viewer"])] = "viewer",
    active_only: bool = True
) -> list:
    """Search users by query with optional role filter."""
    return []  # Implementation omitted

print(search_users._needle_tool["parameters"]["properties"])

Generated properties:

{
  "query": {
    "type": "string",
    "description": "Search query",
    "minLength": 2,
    "maxLength": 100
  },
  "role": {
    "type": "string",
    "enum": ["admin", "editor", "viewer"],
    "default": "viewer"
  },
  "active_only": {
    "type": "boolean",
    "default": true
  }
}

How Validation Is Enforced at Runtime

The generated schema is consumed by the agent runtime in needle/__init__.py (lines 104-108), which looks up _needle_tool on exported symbols when loading tools. Incoming LLM arguments are validated against this schema before the decorated function executes. This ensures type safety and constraint compliance without manual argument checking in your tool implementations.

Summary

  • Field in needle/agent/tools.py captures validation constraints through its __init__ (lines 18-31)
  • _field_of extracts Field instances from Annotated type hints (lines 89-96)
  • Field.apply() copies constraints into JSON Schema (lines 36-49)
  • build_schema determines required parameters by checking field.has_default() (lines 34-38)
  • @tool attaches the final schema as _needle_tool (lines 68-70)
  • The agent validates LLM arguments against _needle_tool before invocation

Frequently Asked Questions

What happens if I don't use Annotated or Field?

Parameters without Annotated wrappers receive basic type inference in build_schema. They get JSON Schema types ("string", "integer", etc.) but no additional constraints. Optional types (e.g., str | None) are correctly marked as non-required.

Can Field be used with custom types or Pydantic models?

The current implementation in needle/agent/tools.py focuses on scalar constraint keywords. Complex types are handled through standard JSON Schema type mapping, but nested validation through Pydantic-style models would require extending build_schema's type resolution logic.

Where is the _needle_tool attribute actually used for validation?

The attribute is inspected in needle/__init__.py at lines 104-108 during tool loading. The actual JSON Schema validation against incoming LLM calls happens in the agent runtime, which uses _needle_tool["parameters"] as the schema for argument checking before dispatching to your function.

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 →