Tools
Tools let an agent invoke external services, local controllers, or
custom functions during its reasoning process. OmniLink uses
native structured tool calling — each AI
engine sends tool definitions via the provider’s native API
(OpenAI function calling, Anthropic tool_use, Gemini
functionDeclarations) and receives structured tool call responses.
No text-based Tool: parsing is required. The tool
result is fed back as a tool-output: message so the
agent can incorporate it into its next reply.
Enabling Tools
Two profile fields control tool access:
-
allowToolUse(boolean) — Master switch. Set totrueto let the agent call tools. Whenfalse, tool directives are omitted from the system prompt entirely. -
availableTools(string) — A comma-separated list of tool names the agent may invoke (e.g."search_docs, run_query").
profile = client.create_profile("research-agent", settings={
"agentName": "research-agent",
"mainTask": "You are a research assistant with access to tools.",
"allowToolUse": True,
"availableTools": "search_docs, run_query",
})
Adding Tool Descriptions
For the agent to make informed decisions about which tool to
call, provide descriptions via the
availableToolDetails field. Each entry
includes name, description, and
parameters (a JSON Schema object). The parameters
enable native structured tool calling via each provider’s API:
profile = client.create_profile("research-agent", settings={
"agentName": "research-agent",
"mainTask": "You are a research assistant with access to tools.",
"allowToolUse": True,
"availableTools": "search_docs, run_query",
"availableToolDetails": [
{
"name": "search_docs",
"description": "Searches the documentation for product and API guidance.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query."},
},
"required": ["query"],
},
},
{
"name": "run_query",
"description": "Executes a read-only SQL query against the analytics database.",
"parameters": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "The SQL query to execute."},
},
"required": ["sql"],
},
},
],
})
Important: Every tool must include a
parameters field with a JSON Schema object. For tools
with no arguments, use
{"type": "object", "properties": {}}. This is required
for native tool calling — providers reject function definitions
without parameters.
Tool Response Format
When tools are enabled, the AI engine uses the provider’s
native structured tool calling API. The API response
includes a toolCalls array with structured objects:
{
"text": "Let me look that up for you.",
"toolCalls": [
{
"id": "toolu_abc123",
"name": "search_docs",
"arguments": {"query": "pricing"}
}
]
}
The frontend dispatches tool calls to the toolCallbackUrl
via HTTP POST. The tool result is sent back to the AI as a
tool-output: follow-up message. The agent incorporates
the result into its next response.
Text-based fallback: If the provider does not return
structured tool calls, the frontend falls back to parsing
Tool: name({"arg": "val"}) from the
response text. This requires no configuration and is automatic.
Per-Request Tool Overrides
You can override profile-level tool settings on a per-request basis by passing tool fields directly in the chat payload. Request-time values always take precedence over profile defaults:
# Enable tools for this request even if the profile has allowToolUse=False
result = client.chat(
"Analyze the latest sales data.",
agent_name="analyst",
system_instruction={
"allowToolUse": True,
"availableTools": "run_query, generate_chart",
"availableToolDetails": [
{"name": "run_query", "description": "Run a SQL query."},
{"name": "generate_chart", "description": "Generate a chart from data."},
],
},
)
Using ToolRunner for Autonomous Loops
For use cases where the agent should repeatedly call a tool in a loop
— such as controlling a robot or playing a game — use the
ToolRunner base class from omnilink-lib.
ToolRunner manages the full lifecycle: it kicks off an initial tool
call, polls for state changes, saves memory periodically, and handles
pause/resume/stop commands from the Dashboard.
from omnilink.tool_runner import ToolRunner
class WarehouseRunner(ToolRunner):
agent_name = "warehouse-bot"
display_name = "Warehouse Automation"
tool_name = "execute_action"
tool_description = "Execute the next warehouse action (move, pick, or place)."
poll_interval = 0.5 # seconds between state polls
memory_every = 60 # save memory every 60 seconds
commands = "stop_game, pause_game, resume_game"
def get_state(self) -> dict:
"""Return the current warehouse state."""
return {
"robot_position": self.robot.position,
"held_item": self.robot.held_item,
"pending_orders": len(self.orders),
}
def execute_action(self, state: dict) -> None:
"""Execute the action the agent decided on."""
action = state.get("action")
if action == "move":
self.robot.move_to(state["target"])
elif action == "pick":
self.robot.pick_item(state["sku"])
elif action == "place":
self.robot.place_item()
def state_summary(self, state: dict) -> str:
"""Summarize state for memory persistence."""
return f"Position: {state['robot_position']}, holding: {state['held_item']}"
def is_game_over(self, state: dict) -> bool:
"""Check if all orders are fulfilled."""
return state["pending_orders"] == 0
runner = WarehouseRunner()
runner.run()
The ToolRunner automatically constructs the system instruction with your tool name and description, manages conversation history for memory, and handles the feedback loop between the agent and your local code.
Full Example: Arithmetic Tools
The following example registers an agent with 10 arithmetic tools
(add, subtract, multiply,
divide, power, sqrt,
modulo, abs, floor,
ceil), sends prompts that trigger tool calls, executes
them locally, and feeds results back so the agent can chain
operations or produce a final answer.
1 — Define the tools and register the profile
from omnilink.client import OmniLinkClient
client = OmniLinkClient(omni_key="olink_your_key")
TOOL_DETAILS = [
{"name": "add", "description": "Add two numbers: add(a, b) -> a + b"},
{"name": "subtract", "description": "Subtract: subtract(a, b) -> a - b"},
{"name": "multiply", "description": "Multiply: multiply(a, b) -> a * b"},
{"name": "divide", "description": "Divide: divide(a, b) -> a / b"},
{"name": "power", "description": "Exponent: power(a, b) -> a ** b"},
{"name": "sqrt", "description": "Square root: sqrt(a)"},
{"name": "modulo", "description": "Modulo: modulo(a, b) -> a % b"},
{"name": "abs", "description": "Absolute value: abs(a)"},
{"name": "floor", "description": "Floor: floor(a)"},
{"name": "ceil", "description": "Ceiling: ceil(a)"},
]
profile = client.create_profile("arithmetic-agent", settings={
"agentName": "arithmetic-agent",
"mainTask": (
"You are an arithmetic assistant. You CANNOT compute answers "
"yourself — you MUST use the provided tools for every calculation.\n\n"
"Respond in this format:\n"
" Command: none\n"
" Response: <explanation>\n"
" Tool: <tool_name>\n"
" Tool-Args: <arg1>, <arg2>\n\n"
"After receiving a tool-output message, incorporate the result "
"and either call another tool or give the final answer."
),
"allowToolUse": True,
"availableTools": "add, subtract, multiply, divide, power, sqrt, modulo, abs, floor, ceil",
"availableToolDetails": TOOL_DETAILS,
"agentPersona": "precise and concise",
})
2 — Implement local tool execution
import math, re
TOOLS = {
"add": lambda a, b: a + b,
"subtract": lambda a, b: a - b,
"multiply": lambda a, b: a * b,
"divide": lambda a, b: a / b,
"power": lambda a, b: a ** b,
"sqrt": lambda a: math.sqrt(a),
"modulo": lambda a, b: a % b,
"abs": lambda a: abs(a),
"floor": lambda a: float(math.floor(a)),
"ceil": lambda a: float(math.ceil(a)),
}
def parse_tool_call(text):
"""Extract tool name and numeric args from the agent's reply."""
tool_match = re.search(r"(?im)^Tool:\s*(\S+)", text)
if not tool_match:
return None
name = tool_match.group(1).strip().lower()
args_match = re.search(r"(?im)^Tool-Args:\s*(.+)", text)
raw = args_match.group(1) if args_match else text
numbers = [float(n) for n in re.findall(r"-?\d+(?:\.\d+)?", raw)]
return {"tool": name, "args": numbers}
def execute_tool(name, args):
fn = TOOLS.get(name)
if fn is None:
return f"Error: unknown tool '{name}'"
return f"{name}({', '.join(str(a) for a in args)}) = {fn(*args)}"
3 — Run the tool-calling loop
def ask_with_tools(client, prompt, max_turns=5):
"""Send a prompt and loop until the agent stops requesting tools."""
system_instruction = {
"allowToolUse": True,
"availableTools": "add, subtract, multiply, divide, power, sqrt, modulo, abs, floor, ceil",
"availableToolDetails": TOOL_DETAILS,
}
current_prompt = prompt
for _ in range(max_turns):
result = client.chat(
current_prompt,
agent_name="arithmetic-agent",
engine="g3-engine",
system_instruction=system_instruction,
temperature=0.0,
)
raw = result.get("text", "")
call = parse_tool_call(raw)
if call is None:
# No tool requested — agent is done.
return raw
# Execute the tool locally and feed the result back.
tool_result = execute_tool(call["tool"], call["args"])
print(f" Tool: {call['tool']}({call['args']}) -> {tool_result}")
current_prompt = f"tool-output: {tool_result}"
return raw
# Single tool call
answer = ask_with_tools(client, "What is 1024 + 768?")
# Tool: add([1024.0, 768.0]) -> add(1024.0, 768.0) = 1792.0
print(answer)
# Chained tool calls (multiply -> multiply -> add -> sqrt)
answer = ask_with_tools(client, "What is the square root of (15*15 + 20*20)?")
# Tool: multiply([15.0, 15.0]) -> multiply(15.0, 15.0) = 225.0
# Tool: multiply([20.0, 20.0]) -> multiply(20.0, 20.0) = 400.0
# Tool: add([225.0, 400.0]) -> add(225.0, 400.0) = 625.0
# Tool: sqrt([625.0]) -> sqrt(625.0) = 25.0
print(answer)
A complete runnable version of this example lives in the
omnilink-lib package at
omnilink/examples/arithmetic_tools.py.
Best Practices
-
Only declare tools you have implemented. The agent
will attempt to call any tool listed in
availableTools. Listing an unimplemented tool leads to errors or silent failures. -
Write clear, concise descriptions. The agent relies
on
availableToolDetailsto decide which tool to call. Vague descriptions lead to incorrect tool selection. - One tool per turn. The agent can only invoke one tool per response. If a task requires multiple tools, the agent will call them sequentially across turns.
-
Return structured feedback. When sending
tool-output:results back to the agent, include enough context for the agent to decide its next action. -
Use ToolRunner for loops. If your agent needs to
call the same tool repeatedly (game play, robotic control,
monitoring), use
ToolRunnerinstead of building the loop yourself.
Next Steps
Now that you know how to set up tools, explore these related topics:
- Agent Configuration — Full profile field reference and management.
- Command Parsing — Define command templates and wire up handlers.
- Context System — Ground your agent in domain-specific knowledge.