How to Use `agent.complete()` for One-Shot Tool Calls in Needle

agent.complete() is the primary entry point for single inference requests in Needle, allowing the native engine to optionally invoke registered tools once and return a structured JSON response with no automatic follow-up loops.

Needle is a lightweight Python SDK from cactus-compute/needle that wraps a native inference engine. The complete() method handles basic text generation, audio processing, and—crucially—one-shot tool calls where the model selects and describes a tool invocation in a single request. This article explains how tool registration, native engine binding, and response parsing work together to enable synchronous function calling.


Tool Registration with @tool and Needle(tools=...)

Before complete() can invoke any function, you must register tools during agent initialization.

The @tool decorator (in needle/agent/tools.py, lines 73–76) attaches a _needle_tool attribute containing a JSON schema generated by build_schema. This metadata is later serialized by _resolve() (lines 96–107 in needle/__init__.py) into the format expected by the native engine.

Tools can be:

  • Decorator-wrapped functions (shown below)
  • Pydantic models with callable semantics
  • Plain callables that pass schema introspection
from needle import Needle, tool

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

# Register at initialization

agent = Needle(tools=[add])

The tools argument accepts a list; multiple tools are supported, though the engine selects at most one per complete() call in one-shot mode.


Understanding the agent.complete() Method Signature

The public complete() method (lines 15–22 of needle/__init__.py) sanitizes inputs and delegates to _complete():

def complete(self, text: str = "", max_new_tokens: int = 256,
             audio=None, audio_format="wav", sample_rate=0,
             channels=1) -> dict:
    _track("complete", self._track_props())
    payload = _prepare_audio(audio, audio_format, sample_rate, channels)
    return self._complete(text, max_new_tokens, payload)
Parameter Purpose
text Primary prompt string; can be empty if audio is provided
max_new_tokens Hard limit on output token count
audio File path, bytes, or buffer for audio inputs (Needle 3 models)
audio_format Container format: "wav", "mp3", etc.
sample_rate, channels Audio metadata for proper decoding

Return value is always a Python dictionary parsed from the native engine's JSON response.


Native Engine Invocation and One-Shot Semantics

Inside _complete() (lines 24–40), the method binds the shared library via ctypes if not already cached, then invokes needle_complete through one of two signatures:

  • Generation 2 models: Direct call to lib.needle_complete
  • Generation 3 models: Audio-aware extended signature

The native function writes a JSON string into a pre-allocated buffer. On any non-zero status code, RuntimeError is raised with the engine's error message.

Critical distinction from run(): complete() performs exactly one inference round. There is no implicit loop that:

  • Executes the selected tool
  • Feeds results back to the model
  • Requests a follow-up completion

This "one-shot" behavior makes complete() ideal for fire-and-forget commands where your application handles tool execution separately.


Parsing Tool Call Responses

When the engine selects a registered tool, the JSON envelope contains a function_calls array:

response = agent.complete("What is 7 plus 5?")
print(response)

Example output:

{
  "type": "call",
  "function_calls": [
    {"name": "add", "arguments": {"a": 7, "b": 5}}
  ]
}

Your application extracts name and arguments, executes the corresponding Python function, and decides whether to:

  • Return results to the user directly
  • Issue a second complete() call with tool results appended

For pure text generation without tools, the envelope contains a text field instead.


Complete Code Examples for One-Shot Tool Calls

Example 1: Arithmetic Tool Invocation

from needle import Needle, tool

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

agent = Needle(tools=[add])
response = agent.complete("What is 7 plus 5?")

# Extract and execute manually

if response.get("type") == "call":
    call = response["function_calls"][0]
    if call["name"] == "add":
        result = add(**call["arguments"])
        print(f"Result: {result}")  # => 12

Example 2: Audio Transcription with Tool Selection

from needle import Needle, tool

@tool
def transcribe(audio: str) -> str:
    """Return textual transcription of the provided audio file."""
    return "placeholder"  # Engine overrides in practice

agent = Needle(tools=[transcribe])

response = agent.complete(
    "Transcribe this recording",
    audio="meeting.wav",
    audio_format="wav",
    max_new_tokens=256,
)

print(response["function_calls"][0]["name"])  # => "transcribe"

Example 3: Plain Text Generation (No Tools)

agent = Needle()
response = agent.complete("Explain quantum computing in one sentence.")
print(response["text"])

Key Source Files Reference

File Purpose Lines of Interest
needle/__init__.py Needle class, complete(), _complete(), _resolve() 15–22 (public API), 24–40 (native binding), 96–107 (schema resolution)
needle/agent/tools.py @tool decorator, build_schema, Pydantic support 73–76 (decorator implementation)
needle/_worker.py Thin wrapper for native needle_complete in fine-tuned deployments Entire module
tests/test_worker.py Validation suite for one-shot complete behavior and tool invocation Test cases demonstrating expected JSON envelopes

Summary

  • agent.complete() in cactus-compute/needle executes single-round inference with optional tool selection, returning a JSON envelope the caller must interpret.
  • Tools are registered via Needle(tools=[...]) after decoration with @tool, which attaches JSON schemas via _needle_tool.
  • The native engine is invoked through _complete() in needle/__init__.py, with generation-aware signatures for text-only or audio-capable models.
  • One-shot semantics mean no automatic tool execution or re-prompting—your application receives the tool call description and handles fulfillment.
  • For multi-turn conversations with automatic tool looping, use agent.run() instead.

Frequently Asked Questions

What is the difference between complete() and run() in Needle?

complete() performs a single inference request and returns immediately, making it suitable for simple queries or when your application manages tool execution externally. run() implements an agentic loop that automatically executes selected tools, appends results to context, and re-prompts the model until completion—useful for complex multi-step tasks.

Can complete() execute the selected tool automatically?

No. As implemented in needle/__init__.py, complete() delegates to the native needle_complete function and returns the raw JSON envelope. Your application must parse function_calls[0]["name"] and function_calls[0]["arguments"], then invoke the corresponding Python callable. This design keeps the core SDK lightweight and deterministic.

How do I pass multiple tools to agent.complete()?

Initialize the agent with a list: Needle(tools=[add, subtract, multiply]). The engine will select zero or one tool per call based on prompt relevance. Multiple simultaneous tool calls in one request are not supported in one-shot mode; use sequential complete() calls or switch to run() for dependent operations.

What audio formats does complete() support?

According to the _prepare_audio helper used in complete(), the SDK accepts audio_format="wav" natively, with other formats (MP3, FLAC, OGG) potentially supported depending on your Needle engine version and shared library compilation. Always provide sample_rate and channels explicitly for non-WAV inputs to ensure proper buffer preparation.

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 →