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

> Discover how the run() method in Needle 2 creates a complete agentic loop. This think-act-reflect cycle empowers autonomous agents for reasoning, tool execution, and self-refinement without external help.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: internals
- Published: 2026-09-02

---

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

```python

# 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.

```python

# 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:

```python

# 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:

```python

# 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. **Return** – `run()` exits, populates `"results"` with full execution history

## Key Files Powering the Loop

| File | Purpose | Critical Functionality |
|------|---------|------------------------|
| [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) | Core `Needle` class | `run()` method implementing the agentic loop; `_resolve()` for tool registration |
| [`needle/model/run.py`](https://github.com/cactus-compute/needle/blob/main/needle/model/run.py) | Low-level engine interface | `_complete()` wrapper around native C-extension; generation parameter handling |
| [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) | Tool infrastructure | `tool` decorator; `build_schema` for LLM-compatible function schemas |
| [`needle/_telemetry.py`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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`](https://github.com/cactus-compute/needle/blob/main/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.