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 to true to let the agent call tools. When false, 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 availableToolDetails to 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 ToolRunner instead of building the loop yourself.

Next Steps

Now that you know how to set up tools, explore these related topics: