How to Apply Per-Argument Constraints to Tool Parameters Using `needle.Field`

Use needle.Field with typing.Annotated or as a default value to inject JSON-Schema validation rules directly into your tool's parameter schema.

The needle framework provides Field as the core mechanism for adding validation constraints to tool parameters. By enriching function signatures with Field objects, you control the JSON schema that LLMs receive—enforcing limits like string length, numeric ranges, and regex patterns without writing custom validation logic.


Understanding needle.Field Architecture

The constraint system in needle follows a four-stage pipeline defined in needle/agent/tools.py.

Stage 1: Field Definition

A Field instance carries JSON-Schema constraints through its constructor. The source implementation at lines 18–32 accepts parameters like ge, le, pattern, min_length, and max_length:

  • ge / le — numeric greater-than-or-equal / less-than-or-equal
  • gt / lt — strict numeric inequalities
  • min_length / max_length — string and array bounds
  • pattern — regex validation for strings
  • description — human-readable parameter documentation

Stage 2: Schema Population via field.apply

Once the base type is inferred, the schema builder calls field.apply(schema) at lines 36–48. This method mutates the schema dictionary in-place, injecting all specified constraints.

Stage 3: Field Discovery with _field_of

The internal _field_of function (lines 89–101) inspects each function parameter. It checks two locations:

  1. Inside typing.Annotated[type, Field(...)] wrappers
  2. As the default value for the parameter

When found, the Field is returned for schema enrichment.

Stage 4: Final Schema Assembly

The build_schema function (lines 20–47) orchestrates the full process: type inference, constraint application, required/optional flagging, and JSON-Schema output generation.


The Annotated approach attaches constraints without interfering with runtime values. This preserves standard Python type hints while adding schema metadata.

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

@tool
def search(
    query: Annotated[str, Field(min_length=3, max_length=100)],
    top_k: Annotated[int, Field(ge=1, le=10)] = 5,
) -> str:
    """
    Perform a semantic search.

    Args:
        query: The search string (3-100 characters).
        top_k: Number of results to return (1-10).
    """
    return f"Searching for '{query}' with top_k={top_k}"

The generated schema stored at search._needle_tool contains:

{
  "name": "search",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "minLength": 3,
        "maxLength": 100
      },
      "top_k": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10,
        "default": 5
      }
    },
    "required": ["query"]
  }
}

Method 2: Using Field as Default Value

When backward compatibility with older Python versions matters, or when you prefer less verbose signatures, supply Field as the parameter default:

from needle.agent.tools import Field, tool

@tool
def resize_image(
    width: int = Field(ge=1, le=4096),
    height: int = Field(ge=1, le=4096),
    keep_aspect: bool = True,
) -> bytes:
    """
    Resize an image to the given dimensions.

    Args:
        width: Desired width in pixels (1-4096).
        height: Desired height in pixels (1-4096).
        keep_aspect: Preserve the original aspect ratio.
    """
    # Implementation returns resized image bytes

    return b""

Key limitation: This approach consumes the default value slot. You cannot simultaneously provide a runtime default value and attach constraints this way.


Complete Constraint Reference

Constraint JSON-Schema Key Applies To Example
ge minimum int, float Field(ge=0)
le maximum int, float Field(le=100)
gt exclusiveMinimum int, float Field(gt=0)
lt exclusiveMaximum int, float Field(lt=1.0)
min_length minLength str, list Field(min_length=1)
max_length maxLength str, list Field(max_length=10)
pattern pattern str Field(pattern="^[a-z]+$")
description description All types Field(description="User ID")

Validating Your Schema

The @tool decorator attaches the generated schema to the function object. Access it to verify your constraints:

from needle.agent.tools import Field, tool

@tool
def example(x: Annotated[int, Field(ge=0, le=100)]) -> int:
    return x

print(example._needle_tool["parameters"]["properties"]["x"])

# {'type': 'integer', 'minimum': 0, 'maximum': 100}

For comprehensive test coverage of constraint handling, refer to tests/test_tools.py in the repository.


Summary

  • needle.Field is the container for per-argument JSON-Schema constraints in needle.
  • Annotated[type, Field(...)] attaches constraints without affecting runtime behavior.
  • Field as default value provides an alternative syntax with trade-offs.
  • field.apply(schema) at needle/agent/tools.py lines 36–48 performs the actual schema mutation.
  • _field_of discovers Field instances from either location during parameter inspection.

Frequently Asked Questions

What happens if I specify conflicting constraints in Field?

The needle framework passes constraints directly to JSON-Schema without additional validation. Conflicting constraints like Field(ge=10, le=5) generate a schema that will reject all values, which the LLM will observe and respect. Test your tool schemas via fn._needle_tool before deployment.

Can I use needle.Field without the @tool decorator?

No. Field is designed for the needle schema pipeline in needle/agent/tools.py. The _field_of inspection and build_schema functions are only invoked by @tool. For standalone schema generation, use build_schema directly on a function object.

Does Field support custom JSON-Schema keywords?

The current implementation in needle/agent/tools.py lines 18–32 defines a fixed set of parameters. For extension, subclass Field and override apply to inject additional keys into the schema dictionary before build_schema completes.

How do I add descriptions to parameters using Field?

Pass description to the Field constructor:

Annotated[str, Field(min_length=3, description="Search query string")]

This populates the description key in the generated JSON-Schema, improving LLM understanding of the parameter's purpose.

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 →