# How to Manually Control Agent Turns with `agent.complete()` in Needle

> Manually control agent turns in Needle using agent.complete(). Inspect function calls execute tools and feed results back for complete turn-taking loop control.

- Repository: [Cactus Compute, Inc./needle](https://github.com/cactus-compute/needle)
- Tags: how-to-guide
- Published: 2026-09-01

---

**Call `agent.complete()` to perform a single LLM generation, inspect the `function_calls` in the response, execute tools yourself, then feed results back in a new `complete()` call—giving you full control over the turn-taking loop.**

The **Needle** library lets you choose between automatic execution via `agent.run()` and manual orchestration through `agent.complete()`. This article explains how to manually control agent turns with `agent.complete()` in Needle, based on the source code in `cactus-compute/needle`.

## What `agent.complete()` Does Under the Hood

The `complete()` method lives in [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) (lines 19–38) and performs exactly one forward pass through the language model. It invokes the native C‑engine via `needle_complete` and returns a JSON envelope with three key fields:

- **`type`**: `"message"` for direct text or `"call"` when tools are requested
- **`function_calls`**: array of tool invocations (empty if none)
- **`content`**: string response when `type` is `"message"`

No tools execute automatically. You receive raw `function_calls` and decide what happens next.

## The Manual Turn-Taking Pattern

A complete turn consists of three steps you control explicitly:

```python
from needle import Needle, tool, Field
import json

@tool
def add(a: int, b: int) -> int:
    """Return the sum of two integers."""
    return a + b

agent = Needle(tools=[add])

# Step 1: Get model's request

response = agent.complete("Calculate 7 + 5", max_new_tokens=64)

# Step 2: Inspect and execute tools yourself

if response.get("function_calls"):
    call = response["function_calls"][0]
    fn = agent._functions[call["name"]]      # Access registered tool

    args = call.get("arguments", {})
    result = fn(**args)                       # ← You control execution

    print(f"Manual result: {result}")         # 12

    # Step 3: Feed result back for next model response

    next_prompt = json.dumps([result], default=str)
    final = agent.complete(next_prompt, max_new_tokens=64)
    print(final["content"])                   # Model responds with "12"

```

This mirrors the internal `run()` loop (lines 39–60 of [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py)) but gives you intervention points at every step.

## Comparing `complete()` and `run()`

| Capability | `agent.run()` | `agent.complete()` (manual) |
|------------|-------------|----------------------------|
| Tool execution | Automatic | You implement |
| Error handling | Built-in | Custom |
| Retry logic | Fixed | Configurable |
| Multi-step limit | Automatic | You decide when to stop |
| External integration | Limited | Full flexibility |

Use `run()` for standard agent behavior. Use `complete()` when you need to:

- **Intercept tool calls** before execution
- **Rate-limit** or **batch** API operations
- **Route calls to non-Python systems**
- **Log or transform** intermediate state
- **Debug single model steps** without recursion

## Handling Multiple Tool Calls

The `function_calls` array may contain several invocations. Process them independently:

```python
response = agent.complete("Compare weather in Tokyo and Paris", max_new_tokens=128)

calls = response.get("function_calls", [])
results = []

for call in calls:
    fn = agent._functions[call["name"]]
    args = call.get("arguments", {})
    
    # Add your own logic: validation, caching, parallel execution

    try:
        result = fn(**args)
        results.append({"status": "success", "data": result})
    except Exception as e:
        results.append({"status": "error", "message": str(e)})

# Feed aggregated results back

next_prompt = json.dumps(results, default=str)
follow_up = agent.complete(next_prompt, max_new_tokens=128)

```

## Server-Side Usage in the Playground

The HTTP façade in [`needle/playground/server.py`](https://github.com/cactus-compute/needle/blob/main/needle/playground/server.py) (lines 30–40) demonstrates the same pattern. The `Engine.complete()` method forwards requests to a `Needle` instance:

```python

# From needle/playground/server.py

def complete(self, query: str, max_new_tokens: int = 128):
    return self.agent.complete(query, max_new_tokens=max_new_tokens)

```

This confirms that `complete()` is the underlying primitive—both interactive servers and manual scripts use the same interface.

## Key Source Files for Reference

- **[`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py)** — Core `Needle` class, `complete()` implementation, and `run()` orchestration loop
- **[`needle/playground/server.py`](https://github.com/cactus-compute/needle/blob/main/needle/playground/server.py)** — HTTP wrapper showing server-side usage
- **[`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)** — `@tool` decorator and schema definitions

## Summary

- `agent.complete()` performs **one generation only**, returning tool requests without executing them
- **Manual turn control** requires three steps: generate, execute, feed back
- Access pending calls via **`function_calls`** and registered tools via **`agent._functions`**
- Serialize results with **`json.dumps([result], default=str)`** to match internal conventions
- Use this pattern when you need **custom orchestration**, **debugging isolation**, or **external system integration**

## Frequently Asked Questions

### What's the difference between `complete()` and `run()`?

`complete()` does a single LLM forward pass and stops. `run()` (defined in [`needle/__init__.py`](https://github.com/cactus-compute/needle/blob/main/needle/__init__.py) lines 39–60) wraps `complete()` in a loop that automatically executes tools and feeds results back until no more calls remain. Use `complete()` when you want to intervene between steps.

### How does `complete()` know which tools are available?

You register tools by passing them to `Needle(tools=[...])` at initialization. The `@tool` decorator (from [`needle/agent/tools.py`](https://github.com/cactus-compute/needle/blob/main/needle/agent/tools.py)) introspects function signatures to build JSON schemas. Inside `complete()`, the C‑engine receives these schemas and may include `function_calls` in its response.

### Can I use `complete()` from languages other than Python?

Yes. The HTTP server in [`needle/playground/server.py`](https://github.com/cactus-compute/needle/blob/main/needle/playground/server.py) exposes `complete()` via `Engine.complete()`. Any HTTP client can send prompts and receive JSON responses with `function_calls`, then implement tool execution in the host language before sending follow-up requests.

### What format should tool results have when feeding back to `complete()`?

Match the internal convention: a JSON array of results, each typically containing `"status"` and `"data"` or `"message"` keys. Use `json.dumps(results, default=str)` to handle non-serializable types, as shown in the example above and as implemented in `Needle.run()`.