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 requestedfunction_calls: array of tool invocations (empty if none)content: string response whentypeis"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
needle/__init__.py— CoreNeedleclass,complete()implementation, andrun()orchestration loopneedle/playground/server.py— HTTP wrapper showing server-side usageneedle/agent/tools.py—@tooldecorator 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_callsand registered tools viaagent._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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →