Understanding the Needle Agent Loop: How agent.run() Executes Function Calls

The agent.run() method implements a closed-loop interaction that repeatedly invokes the language model, detects function calls, executes registered tools, and feeds results back until the model stops requesting tools or reaches the maximum step limit.

The cactus-compute/needle repository provides a lightweight agent framework where the Needle class orchestrates autonomous tool use through its run() method. Understanding this agent loop mechanism is essential for building reliable applications that delegate computation to external functions. The implementation resides primarily in needle/__init__.py and manage six distinct stages per iteration.

The Six-Stage Execution Cycle

Each iteration of the Needle agent loop follows a strict pipeline from model invocation to result feedback. The outer loop controller resides at lines 84‑96 of needle/__init__.py and manages the lifecycle through the following stages.

Stage 1: Model Invocation via _complete

The loop begins by delegating to the private _complete method at lines 28‑31. This helper transmits the current prompt to the underlying native engine (or fine-tuned worker) and returns a raw JSON envelope containing the model's response.


# Conceptual flow within the loop

response = self._complete(messages, tools=self._functions)

Stage 2: Function Call Detection

The loop inspects the JSON response for a "function_calls" field. If the response type is not "call" or the field is absent, the agent loop terminates immediately. This check occurs in the loop header at lines 84‑92, preventing unnecessary computation when the model requires no external tools.

Stage 3: Grounding Validation (Optional)

When strict=True, the loop validates argument grounding. The _ungrounded_paths helper (lines 37‑44) extracts any ungrounded parameters reported by the engine. If violations exist, the loop converts these into error objects before tool execution, preventing hallucinated arguments from reaching your functions.

Stage 4: Tool Registry Lookup and Execution

For each detected call, the loop performs the following in the inner block at lines 95‑108:

  • Name resolution: Looks up the tool in the agent's _functions registry
  • Error handling: Returns an error object if the tool is unknown
  • Safe execution: Invokes the tool with model-provided arguments inside a try/except block
  • Exception capture: Reports any runtime exceptions as structured errors

# Simplified logic from lines 95-108

for call in calls:
    if call.name not in self._functions:
        result = {"error": f"Unknown tool: {call.name}"}
    else:
        try:
            tool = self._functions[call.name]
            result = tool(**call.arguments)
        except Exception as e:
            result = {"error": str(e)}

Stage 5: Result Feedback

The loop JSON-encodes all tool results (including errors) and passes them back to _complete as the next conversation turn. Crucially, this re-entry step at lines 110‑113 sets ground=False to prevent the engine from attempting additional grounding on synthetic tool outputs.

Stage 6: Iteration Control

The entire sequence repeats for up to max_steps iterations (default 8). The loop accumulates all intermediate outputs in an executed list, which the method finally attaches to the response as "results" before returning to the caller.

Configuration Parameters

Two key parameters control the Needle agent loop behavior:

  • max_steps: Hard limit on loop iterations (default 8) defined in the outer range(max_steps) controller
  • strict: Boolean flag enabling grounding validation via _ungrounded_paths

# Example with explicit configuration

agent = Needle(tools=[add])
response = agent.run(
    query="Calculate 15 + 27",
    max_steps=5,
    strict=True
)

Code Examples

Basic Tool Execution

This example demonstrates the complete loop with a registered tool:

from needle import Needle, tool

@tool
def add(x: int, y: int) -> int:
    """Return the sum of x and y."""
    return x + y

agent = Needle(tools=[add])

# The agent loop will detect the need for addition and execute the tool

response = agent.run(
    query="What is the total of 12 and 7?",
    max_steps=5,
    strict=True
)

print(response)

# Output includes: {'type': 'result', 'results': [19], ...}

User-Defined Functions Without Decoration

The loop supports plain functions through automatic schema inference:

def fetch_weather(city: str) -> str:
    """Mock weather lookup."""
    return f"The weather in {city} is sunny."

agent = Needle(tools=[fetch_weather])

out = agent.run("Give me the weather for Paris.")
print(out["results"])  

# ["The weather in Paris is sunny."]

Summary

  • The Needle agent loop in needle/__init__.py implements a closed-cycle architecture: model → function detection → tool execution → results → model.
  • The private _complete method (lines 28‑31) handles all LLM communication, while the outer loop controller (lines 84‑96) manages up to 8 iterations by default.
  • Strict mode validates argument grounding using _ungrounded_paths (lines 37‑44) before executing tools.
  • Tool dispatch occurs in the inner execution block (lines 95‑108), which handles registry lookups, error capture, and exception safety.
  • Results feed back into the conversation with ground=False (lines 110‑113) to prevent recursive grounding attempts on synthetic data.

Frequently Asked Questions

What is the default maximum number of steps in the Needle agent loop?

The default max_steps is 8 iterations, as defined by the range(max_steps) controller in needle/__init__.py (lines 84‑96). You can override this by passing a different integer to the max_steps parameter of agent.run().

How does Needle handle unknown or invalid tool calls?

When the loop encounters a function name not present in the _functions registry, it immediately generates an error object rather than raising an exception. This allows the model to receive feedback about the invalid call and potentially correct its approach in subsequent iterations.

What happens when strict mode is enabled in agent.run()?

When strict=True, the loop invokes _ungrounded_paths (lines 37‑44) to check for hallucinated arguments before executing any tool. If ungrounded paths are detected, the loop treats these as execution errors, preventing unvalidated parameters from reaching your tool implementations.

Can the agent loop handle exceptions thrown by tool functions?

Yes. The execution block at lines 95‑108 wraps each tool invocation in a try/except construct. Any exception raised during tool execution is caught and converted into a structured error result, which is then fed back to the model as context for the next iteration.

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 →