How Needle's `agent.run()` Loop Manages Multi‑Step Tool Execution
The Needle.run() method implements an iterative agent loop that lets language models invoke Python tools repeatedly, feeding results back into the context until the model decides to stop or reaches the step limit.
The needle package provides a lightweight Python binding around a native C engine for language model inference. Its Needle.run() method in needle/__init__.py orchestrates the core pattern modern agents use: planning, tool invocation, observation, and replanning. This article breaks down exactly how that loop works, using source-accurate details from the cactus-compute/needle repository.
The Agent Loop Architecture
Entry Point: self.complete() and Initial Tool Schema
When you call run(), the method first invokes self.complete() with your original query. This wrapper around needle_complete (defined in needle/model/run.py) sends both the prompt and a JSON schema describing all registered tools to the C engine.
The C engine returns a JSON envelope. When the model wants to use tools, that envelope contains "type": "call" and a "function_calls" array with the requested invocations.
The Iterative Execution Loop
The core loop in Needle.run() runs for a configurable number of steps (default 8). Each iteration follows four distinct phases:
-
Extract calls – Parse
response.get("function_calls"); break if empty or non‑call type -
Dispatch to Python – Look up each function name in
self._functions, invoke with extracted arguments, catch and serialize any exceptions -
Collect results – Append outcomes to per‑step
resultsand globalexecutedlist -
Re‑prompt the model – JSON‑encode results (via
_jsonablefor Pydantic compatibility) and callself.complete()again
This cycle enables multi‑step tool execution where later calls can depend on earlier results.
Tool Dispatch and Error Handling
Function resolution happens against self._functions, populated during Needle.__init__ from the tools parameter. The initialization logic (in needle/__init__.py, with schema building from needle/agent/tools.py) handles:
- Plain Python functions decorated with
@tool - Pydantic models for structured inputs/outputs
- Raw JSON schemas for advanced use cases
If a requested function name is missing, run() injects an error dict into results rather than crashing. Tool exceptions are similarly caught and converted to error payloads, keeping the agent loop robust.
Result Serialization with _jsonable
Before feeding tool outputs back to the C engine, run() uses the helper _jsonable to normalize objects. This ensures Pydantic models, dataclasses, and other Python objects become JSON‑serializable without manual conversion.
Multi‑Step Reasoning in Practice
The loop's power lies in context accumulation. Each complete() call includes the conversation history plus the most recent tool results. The model can:
- Request a lookup
- Receive structured data
- Formulate a follow‑up query based on that data
- Issue additional tool calls
This matches the ReAct pattern (reasoning + acting) without explicit prompting templates.
Complete Working Example
from needle import Needle, tool, Field
@tool
def search_web(query: str):
"""Fake web search – returns a canned response."""
return {"answer": f"Result for '{query}'"}
# Initialize agent with tool registry
agent = Needle(tools=[search_web], system="You are a helpful assistant.")
# Execute multi‑step query
response = agent.run(
"Find the capital of France, then look up its population.",
max_steps=5,
max_new_tokens=128,
)
print("Final response:", response)
print("Executed tool calls:", response["results"])
Expected execution flow:
- First
complete()→ model callssearch_webwith"capital of France" - Loop processes call, returns
{"answer": "Result for 'capital of France'"} - Second
complete()with result → model callssearch_webwith"Paris population" - Loop ends (no more calls) → final response includes both results in
"results"key
The response["results"] array preserves the full execution trace for debugging or logging.
Key Source Files
| File | Responsibility |
|---|---|
needle/__init__.py |
Needle class, run() loop, complete() wrapper, tool initialization |
needle/agent/tools.py |
@tool decorator, schema extraction, Pydantic integration |
needle/model/run.py |
Low‑level needle_complete C bindings, response parsing |
Configuration Parameters
max_steps– Hard limit on tool invocation rounds (default 8)max_new_tokens– Generation budget for eachcomplete()callsystem– System prompt set at initialization
These parameters balance latency, cost, and completion quality for your specific use case.
Summary
Needle.run()implements a generic agent loop inneedle/__init__.pywith configurable iteration limits- Each cycle invokes
self.complete()to get tool requests, executes Python functions fromself._functions, and feeds JSON results back via_jsonableserialization - The loop enables multi‑step tool execution where model reasoning can build on prior observations
- Error handling is defensive: unknown functions and tool exceptions become structured error payloads rather than loop‑terminating failures
Frequently Asked Questions
What happens if a tool raises an exception?
The run() loop catches the exception, converts it to an error dict with the exception message, and includes that in the results fed back to the model. The loop continues, allowing the model to recover or report the failure.
How does Needle know which Python functions to call?
During __init__, the agent resolves the tools parameter—functions, Pydantic models, or raw schemas—and populates self._functions with name-to-callable mappings. Tool names in model requests must match these registered keys exactly.
Can the model make multiple tool calls in a single step?
Yes. The "function_calls" array can contain multiple requests. run() iterates through all of them, executes each, and aggregates results before the next complete() call.
Why is there a default limit of 8 steps?
The max_steps default prevents infinite loops from runaway agents. You can increase it for complex workflows requiring extended reasoning chains, or decrease it for latency‑sensitive applications.
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 →