How `run()` Provides a Complete Agentic Loop in Needle 2

The run() method in Needle 2 implements a closed-loop "think-act-reflect" cycle that enables autonomous agents to reason, execute tools, and refine answers without external orchestration.

Needle 2 is a lightweight open-source framework for building LLM-powered agents. At its core, the run() method in needle/__init__.py delivers the complete agentic loop functionality—bridging model generation, tool execution, and iterative reasoning into a single, self-contained workflow.

The Three Stages of the Agentic Loop

The run() method orchestrates the agentic cycle through three distinct stages. Each stage is implemented with minimal overhead, leveraging Needle's native C-extension engine for speed.

Stage 1: Initial LLM Completion

When run() receives a query, it immediately invokes the private _complete() helper located in needle/model/run.py. This helper sends the user query to the native engine and returns a response envelope containing:

  • text: The model's generated content
  • type: The response classification (typically "call" or "stop")
  • function_calls: A list of tool invocations requested by the model (when type == "call")

# From needle/__init__.py lines 39-42

# Initial completion triggers the agentic loop

response = self._complete(
    query=prompt,
    max_new_tokens=max_new_tokens,
)

This initial call sets the loop in motion. If the model determines it needs external tools, the response type signals continuation.

Stage 2: Iterative Tool Execution

While response["type"] == "call" and function_calls exist, run() enters its core execution loop (lines 43-60). For each iteration:

  1. Tool Lookup – The method queries the _functions registry (populated by _resolve() during Needle construction) to locate each requested tool by name.

  2. Safe Execution – Each Python tool implementation runs with exception handling. Errors serialize to JSON-serializable objects without breaking the loop.

  3. Result Collection – All tool outputs from the current turn aggregate into a results list.

  4. Feedback Completion – JSON-encoded results feed back into _complete(), letting the LLM reason on outputs and decide on further tool calls.


# Conceptual loop structure from needle/__init__.py

while response["type"] == "call" and step < max_steps:
    results = []
    for call in response["function_calls"]:
        fn = self._functions[call["name"]]      # Registry lookup

        output = fn(**call["arguments"])        # Execute

        results.append(output)
    
    # Feed results back for next reasoning step

    response = self._complete(
        query=build_tool_prompt(results),
        max_new_tokens=max_new_tokens,
    )
    step += 1

The loop respects the max_steps parameter, preventing runaway execution while allowing genuine multi-step reasoning.

Stage 3: Final Aggregation

When the loop terminates—either because the model returns "stop" or max_steps is reached—run() performs final packaging:


# needle/__init__.py lines 60-61

response["results"] = executed   # Attach accumulated tool history

return response

The "results" key contains the full executed sequence, enabling audit trails, debugging, and downstream processing.

Complete Agentic Loop in Practice

Here's a working example demonstrating the full cycle:


# example_tool.py

from needle import Needle, tool

@tool
def calculate(expression: str) -> float:
    """Safely evaluate a mathematical expression."""
    return eval(expression)  # Simplified—use proper sandboxing in production

@tool
def format_currency(value: float, currency: str = "USD") -> str:
    """Format a number as currency."""
    symbols = {"USD": "$", "EUR": "€", "GBP": "£"}
    return f"{symbols.get(currency, '$')}{value:,.2f}"

# Initialize agent with tools

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

# Run the complete agentic loop

response = agent.run(
    query="What is 144 * 7 in euros?",
    max_steps=4,
    max_new_tokens=256,
)

print(response["results"])

# [{'expression': '144 * 7', 'result': 1008.0}, {'value': 1008.0, 'currency': 'EUR', 'result': '€1,008.00'}]

print(response["text"])

# "The result of 144 * 7 is €1,008.00."

Trace of the agentic loop:

  1. First _complete() call – Model recognizes the math problem, emits function_call for calculate("144 * 7")
  2. Tool execution – Python function runs, returns 1008.0
  3. Second _complete() call – With tool result fed back, model requests format_currency(1008.0, "EUR")
  4. Tool execution – Returns "€1,008.00"
  5. Third _complete() call – Model sees final formatted value, produces natural language answer with "stop" type
  6. Returnrun() exits, populates "results" with full execution history

Key Files Powering the Loop

File Purpose Critical Functionality
needle/__init__.py Core Needle class run() method implementing the agentic loop; _resolve() for tool registration
needle/model/run.py Low-level engine interface _complete() wrapper around native C-extension; generation parameter handling
needle/agent/tools.py Tool infrastructure tool decorator; build_schema for LLM-compatible function schemas
needle/_telemetry.py Instrumentation track wrapper for observability inside run() execution

Loop Characteristics and Design Decisions

The Needle 2 agentic loop differs from heavier orchestration frameworks in several ways:

  • Model-driven termination – The loop exits based on the LLM's own output schema, not hardcoded logic
  • Stateful tool registry – The _functions dict persists across calls, enabling tool reuse without re-registration
  • Minimal serialization overhead – Tool results pass directly as JSON, avoiding complex intermediate formats
  • C-extension performance – Native _complete() calls minimize Python-level latency during tight loops

These characteristics make run() suitable for latency-sensitive applications while maintaining full agentic capabilities.

Summary

  • The run() method in needle/__init__.py implements a complete agentic loop through three coordinated stages: initial completion, iterative tool execution, and final aggregation
  • Tools resolve via the _functions registry populated during Needle initialization by _resolve()
  • The loop continues until the LLM signals completion or max_steps is exhausted
  • All tool executions append to the "results" array, providing full observability
  • Native C-extension calls via _complete() in needle/model/run.py deliver performance without sacrificing Python ergonomics

Frequently Asked Questions

What triggers the agentic loop to continue versus terminate?

The loop continues while the LLM response "type" equals "call" and contains function_calls. The model itself controls termination by switching to "stop" when it has sufficient information to answer. This model-driven termination makes Needle 2 agents self-directed rather than following predetermined execution graphs.

How does max_steps interact with the agentic loop?

max_steps acts as a safety boundary, not a fixed execution count. If the model resolves the query in fewer iterations, run() returns early. If the model keeps requesting tools beyond max_steps, the loop forcibly exits and returns accumulated results. The parameter prevents infinite loops without constraining legitimate multi-step reasoning.

Can tool execution errors break the agentic loop?

No. run() wraps each tool invocation in exception handling that serializes errors to JSON-safe objects. The LLM receives the error description and can attempt recovery, request different tools, or acknowledge failure—all without terminating the loop. This resilience is critical for autonomous operation in production environments.

Where does the tool schema come from when registering functions?

The @tool decorator in needle/agent/tools.py introspects Python function signatures and docstrings to generate LLM-compatible schemas. _resolve() stores both the schema (for the model's context window) and the callable (for execution). This happens once during Needle construction, not per-loop-iteration, keeping run() performant.

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 →