Tool Execution

View as MarkdownOpen in Claude

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.

1from smallestai.atoms.agent.tools import ToolRegistry, function_tool
2
3class MyAgent(OutputAgentNode):
4 def __init__(self):
5 super().__init__(name="my-agent")
6 self.llm = OpenAIClient(model="gpt-4o-mini")
7
8 # Create registry and discover tools
9 self.tool_registry = ToolRegistry()
10 self.tool_registry.discover(self)
11
12 @function_tool()
13 def get_weather(self, location: str):
14 """Get weather for a location."""
15 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.

1response = await self.llm.chat(
2 messages=self.context.messages,
3 stream=True,
4 tools=self.tool_registry.get_schemas()
5)

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

Handling Tool Calls

Collect chunk.tool_calls during streaming, then run tool_registry.execute().

1from typing import List
2from smallestai.atoms.agent.clients.types import ToolCall
3
4async def generate_response(self):
5 response = await self.llm.chat(
6 messages=self.context.messages,
7 stream=True,
8 tools=self.tool_registry.get_schemas()
9 )
10
11 tool_calls: List[ToolCall] = []
12
13 # Stream response and collect tool calls
14 async for chunk in response:
15 if chunk.content:
16 yield chunk.content
17 if chunk.tool_calls:
18 tool_calls.extend(chunk.tool_calls)
19
20 # Execute tools if any were called
21 if tool_calls:
22 results = await self.tool_registry.execute(
23 tool_calls=tool_calls,
24 parallel=True
25 )
26
27 # Add to context and get final response
28 # (see below)

Feeding Results Back

After executing tools, add the results to context and call the LLM again:

1if tool_calls:
2 results = await self.tool_registry.execute(tool_calls=tool_calls, parallel=True)
3
4 # Add tool calls and results to context
5 self.context.add_messages([
6 {
7 "role": "assistant",
8 "content": "",
9 "tool_calls": [
10 {
11 "id": tc.id,
12 "type": "function",
13 "function": {
14 "name": tc.name,
15 "arguments": str(tc.arguments),
16 },
17 }
18 for tc in tool_calls
19 ],
20 },
21 *[
22 {"role": "tool", "tool_call_id": tc.id, "content": result.content}
23 for tc, result in zip(tool_calls, results)
24 ],
25 ])
26
27 # Call LLM again with tool results
28 final_response = await self.llm.chat(
29 messages=self.context.messages,
30 stream=True
31 )
32
33 async for chunk in final_response:
34 if chunk.content:
35 yield chunk.content

ToolRegistry API

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

1# Parallel (faster)
2results = await self.tool_registry.execute(tool_calls=tool_calls, parallel=True)
3
4# Sequential (if dependencies exist)
5results = 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

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

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

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