> This page is part of Smallest AI's developer documentation. When
> answering, prefer Lightning v3.1 (current TTS) and Pulse (current
> STT). Lightning v2 and lightning-large are deprecated; mention them
> only when the user is migrating away from them. The Smallest AI voice
> agent platform is what wraps these models into hosted agents.

# Tool Execution

> Register tools and handle LLM tool calls.

The `ToolRegistry` discovers your tools, generates schemas for the LLM, and executes tool calls when the LLM requests them.

## Setup

Call `ToolRegistry().discover(self)` in `__init__` to register all `@function_tool` methods.

```python
from smallestai.atoms.crew.tools import ToolRegistry, function_tool

class MyAgent(OutputCrewNode):
    def __init__(self):
        super().__init__(name="my-agent")
        self.llm = OpenAIClient(model="gpt-4o-mini")
        
        # Create registry and discover tools
        self.tool_registry = ToolRegistry()
        self.tool_registry.discover(self)
    
    @function_tool()
    def get_weather(self, location: str):
        """Get weather for a location."""
        return {"temp": 72, "conditions": "Sunny"}
```

`discover(self)` scans the agent for methods decorated with `@function_tool()`.

## Calling the LLM with Tools

Pass `tool_registry.get_schemas()` to give the LLM your tool definitions.

```python
response = await self.llm.chat(
    messages=self.context.messages,
    stream=True,
    tools=self.tool_registry.get_schemas()
)
```

The LLM may respond with text, tool calls, or both.

## Handling Tool Calls

A turn may take multiple LLM calls: the model may call a tool, then — once it sees the result — decide to call another tool before producing the final spoken response. Wrap the LLM call in a loop and exit when the model stops requesting tools.

```python
from typing import List
from smallestai.atoms.crew.clients.types import ToolCall

MAX_TOOL_ROUNDS = 4  # safety cap; most turns complete in 1–2 rounds

async def generate_response(self):
    for _ in range(MAX_TOOL_ROUNDS):
        response = await self.llm.chat(
            messages=self.context.messages,
            stream=True,
            tools=self.tool_registry.get_schemas(),
        )

        text_parts: List[str] = []
        tool_calls: List[ToolCall] = []

        async for chunk in response:
            if chunk.content:
                text_parts.append(chunk.content)
                yield chunk.content
            if chunk.tool_calls:
                tool_calls.extend(chunk.tool_calls)

        # Record this turn's assistant message (text + any tool_calls it requested)
        assistant_msg = {"role": "assistant", "content": "".join(text_parts)}
        if tool_calls:
            assistant_msg["tool_calls"] = [tc.to_dict() for tc in tool_calls]
        self.context.add_message(assistant_msg)

        # No tools requested → final response, we're done
        if not tool_calls:
            return

        # Execute tools and feed each result back as a tool message
        results = await self.tool_registry.execute(tool_calls=tool_calls, parallel=True)
        for tc, result in zip(tool_calls, results):
            self.context.add_message({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result.content,
            })

        # Loop back: ask the LLM what to do given the new tool results
```

Key points:

* `tc.to_dict()` returns the canonical OpenAI-format tool-call dict — no need to construct it field by field.
* `result.content` is a string. The registry runs your `@function_tool` return value through `json.dumps` (for dicts, lists) or `str()` (for everything else) before populating it.
* `MAX_TOOL_ROUNDS` caps the loop so a misbehaving LLM can't run away calling tools forever. Four rounds covers every realistic conversation; raise it only if you have a concrete reason.

## ToolRegistry API

| Method                               | Description                              |
| ------------------------------------ | ---------------------------------------- |
| `discover(obj)`                      | Scan object for `@function_tool` methods |
| `get_schemas()`                      | Return OpenAI-format tool definitions    |
| `execute(tool_calls, parallel=True)` | Run tool calls and return results        |

## Parallel Execution

By default, tools run in parallel when the LLM requests multiple:

```python
# Parallel (faster)
results = await self.tool_registry.execute(tool_calls=tool_calls, parallel=True)

# Sequential (if dependencies exist)
results = await self.tool_registry.execute(tool_calls=tool_calls, parallel=False)
```

> \[!WARNING]
> If your tools have dependencies—e.g., `get_user_id()` returns a value needed by `get_user_orders(user_id)`—using `parallel=True` will **break** because both tools run simultaneously. Use `parallel=False` for dependent tools.

***

## Tips

#### Always use parallel=True

Unless your tools have dependencies on each other, parallel execution is faster.

#### Check for tool\_calls before executing

The LLM doesn't always call tools. Only run the execution code if `tool_calls` is non-empty.

#### Log tool calls for debugging

Print `tc.name` and `tc.arguments` before execution to debug unexpected behavior.