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 contenttype: The response classification (typically"call"or"stop")function_calls: A list of tool invocations requested by the model (whentype == "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:
-
Tool Lookup – The method queries the
_functionsregistry (populated by_resolve()duringNeedleconstruction) to locate each requested tool by name. -
Safe Execution – Each Python tool implementation runs with exception handling. Errors serialize to JSON-serializable objects without breaking the loop.
-
Result Collection – All tool outputs from the current turn aggregate into a results list.
-
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:
- First
_complete()call – Model recognizes the math problem, emitsfunction_callforcalculate("144 * 7") - Tool execution – Python function runs, returns
1008.0 - Second
_complete()call – With tool result fed back, model requestsformat_currency(1008.0, "EUR") - Tool execution – Returns
"€1,008.00" - Third
_complete()call – Model sees final formatted value, produces natural language answer with"stop"type - Return –
run()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
_functionsdict 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 inneedle/__init__.pyimplements a complete agentic loop through three coordinated stages: initial completion, iterative tool execution, and final aggregation - Tools resolve via the
_functionsregistry populated duringNeedleinitialization by_resolve() - The loop continues until the LLM signals completion or
max_stepsis exhausted - All tool executions append to the
"results"array, providing full observability - Native C-extension calls via
_complete()inneedle/model/run.pydeliver 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →