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

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 (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:

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) 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:

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 (lines 30–40) demonstrates the same pattern. The Engine.complete() method forwards requests to a Needle instance:


# 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

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 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) 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 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().

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 →