# What Happens When a Tool Call Fails During Needle's agent.run() Loop: Error Handling Explained

> Discover how Needle handles failed tool calls in agent.run()! Learn how exceptions are converted to tool_error payloads, enabling LLM recovery instead of agent crashes. Optimize your agent's resilience.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: deep-dive
- Published: 2026-08-20

---

**When a tool call fails during `agent.run()`, Needle catches the exception, converts it into a structured `tool_error` payload, injects it back into the LLM context, and continues the loop—allowing the model to recover instead of crashing the agent.**

The `needle` framework implements resilient autonomous agents through its `Agent.run()` method. Understanding how this library handles tool execution failures is critical for building reliable LLM-powered applications that can gracefully degrade and self-correct when external tools malfunction.

## How Needle's Agent Loop Processes Tool Calls

The agent execution cycle in [[`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py)](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) follows a predictable pattern:

1. **Prompt generation** — constructs context for the LLM based on current state and history
2. **Response parsing** — extracts tool-call JSON payloads from model output
3. **Tool invocation** — executes the requested function via [[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)

The critical resilience mechanism lives in the `_run_tool` helper method, which wraps every tool execution in defensive error handling.

## Tool Failure Handling: Step-by-Step Breakdown

### Exception Interception

When a tool raises any exception, the `try/except` block in `_run_tool` immediately intercepts it. This prevents the exception from propagating upward and terminating the entire agent session.

### Structured Error Payload Generation

The caught exception is converted into a standardized `tool_error` object containing:

- `error_type` — the exception class name (e.g., `ValueError`, `HTTPError`)
- `error_message` — the string representation of the exception
- Optional traceback for debugging purposes

### Context Injection for LLM Recovery

The error payload is **appended to the conversation history as a system message**, making the failure visible to the LLM on its next reasoning step. This enables the model to:

- **Retry** the same tool with corrected arguments
- **Pivot** to an alternative tool or approach
- **Terminate** gracefully when recovery is impossible

### Continuation and Logging

The loop proceeds to the next iteration unless `max_steps` is exhausted or the LLM explicitly signals completion. Simultaneously, Python's `logging` module records the failure at `ERROR` level for observability.

## Code Implementation: The _run_tool Helper

Here's how the error handling is implemented in the Needle source:

```python

# Simplified excerpt from needle/agent/__init__.py

def _run_tool(self, tool_name: str, args: dict) -> dict:
    try:
        tool_fn = self.tools[tool_name]
        result = tool_fn(**args)                # <-- actual tool execution

        return {"type": "tool_result", "result": result}
    except Exception as exc:                     # ← comprehensive failure catch

        logger.error("Tool %s failed: %s", tool_name, exc)
        return {
            "type": "tool_error",
            "error_type": type(exc).__name__,
            "error_message": str(exc),
        }

def run(self, query: str, max_steps: int = 8, max_new_tokens: int = 256) -> dict:
    for step in range(max_steps):
        # ... generate LLM prompt, parse response ...

        if payload_is_tool_call:
            tool_out = self._run_tool(payload.name, payload.arguments)
            # inject tool_out into conversation history for next LLM turn

            self.history.append({"role": "system", "content": tool_out})
        # ... handle direct LLM responses, check termination ...

```

The `tool_error` structure returned to the LLM looks like this:

```json
{
  "type": "tool_error",
  "error_type": "ValueError",
  "error_message": "Invalid parameter 'x': expected int, got str"
}

```

## Key Design Principles in Needle's Error Handling

| Principle | Implementation |
|-----------|---------------|
| **Fail-safe execution** | Exceptions never escape `_run_tool`; the agent survives individual tool crashes |
| **Observable failures** | Structured error types and logging enable debugging and monitoring |
| **LLM-driven recovery** | Errors become part of the reasoning context, allowing model-level adaptation |
| **Configurable limits** | `max_steps` prevents infinite loops when recovery fails |

## Source File Reference Map

| File | Responsibility |
|------|---------------|
| [[`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py)](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) | `Agent` class and `run()` loop orchestration |
| [[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py) | `@tool` decorator and schema generation |
| [[`needle/model/run.py`](https://github.com/cactus-compute/needle/blob/main/needle/model/run.py)](https://github.com/cactus-compute/needle/blob/main/needle/model/run.py) | Low-level LLM inference and output processing |
| [`tests/test_fetch.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_fetch.py), [`tests/test_tools.py`](https://github.com/cactus-compute/needle/blob/main/tests/test_tools.py) | Validation of error handling behavior |

## Summary

- **Tool failures are caught, not crashed** — the `_run_tool` helper uses `try/except` to intercept all exceptions
- **Errors become structured context** — `tool_error` payloads with `error_type` and `error_message` feed back to the LLM
- **The loop continues** — execution proceeds until `max_steps` or explicit termination, enabling recovery attempts
- **Observability is built-in** — Python `logging` captures failures at `ERROR` level

## Frequently Asked Questions

### Does a tool failure stop the entire agent.run() execution?

No. According to the needle source code, the `try/except` block in `_run_tool` prevents exceptions from propagating. The agent converts the failure into a `tool_error` message, appends it to the conversation history, and continues to the next iteration. Only `max_steps` exhaustion or LLM-initiated termination ends the loop.

### What information does the LLM receive when a tool fails?

The LLM receives a JSON object with `"type": "tool_error"`, plus `error_type` (the exception class name) and `error_message` (the exception string). This structured format allows the model to understand what went wrong and decide whether to retry, pivot tools, or abort.

### Can I customize how tool errors are handled in Needle?

The current implementation in [`needle/agent/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/__init__.py) hardcodes the `tool_error` structure in `_run_tool`. To customize behavior, you would subclass `Agent` and override `_run_tool`, or modify the error payload generation before the context injection step in `run()`.

### Where are tool functions defined and registered in Needle?

Tool functions are decorated with `@tool` from [[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py), which attaches JSON schemas for LLM consumption. The `Agent` stores these in `self.tools` as a mapping from tool names to decorated functions, invoked dynamically during the run loop.