8  Tool Integration

Tools are the primary way to make agents observable and reliable.

This chapter covers:

8.1 Defining a Tool

A Tool has:

  • a description (what it does)
  • an input schema (what arguments it expects)
  • an implementation function (what it returns)
local done = require("tactus.tools.done")

get_weather = Tool {
  description = "Get the forecast for a city",
  input = { city = field.string{required = true} },
  function(args)
    -- Replace with a real API call in production.
    if args.city == "San Francisco" then
      return {temp_f = 58, conditions = "fog"}
    end
    return {temp_f = 72, conditions = "clear"}
  end
}

8.2 Grouping Tools with Toolsets

Toolsets are a convenience for bundling a set of tools into a single handle that can be reused across agents.

8.2.1 Lua toolsets

math = Toolset {
  type = "lua",
  tools = {get_weather}
}

8.2.2 Stdlib toolsets (by name)

Some runtimes expose standard tool bundles (for example: "filesystem"). When supported, you can reference them by string name inside Agent.tools.

8.3 Wiring Tools into an Agent

Agent.tools defines the default capability boundary:

worker = Agent {
  model = "openai/gpt-4o-mini",
  system_prompt = "Use tools when needed, then call done.",
  tools = {math, get_weather, done}
}

Then, per call, you can further restrict capabilities:

worker({message = "Fetch the weather.", tools = {get_weather, done}})
worker({message = "Summarize results only.", tools = {}})

8.4 MCP Toolsets

Tactus can also load toolsets from MCP servers via the Toolset primitive. Conceptually:

  • an MCP server provides a catalog of tools
  • a Toolset selects which tools are available
  • an Agent references that Toolset in tools = {...}

In mock mode, if an MCP server is not configured, Tactus can treat the toolset as an empty placeholder so that examples and specs remain runnable. You still need to mock the tool calls you expect in Mocks { ... }.

8.5 Host-Registered Modules

Embedding hosts can register Python-backed modules that Tactus code imports with require(...). This is different from exposing many individual tools to an agent. A host module is a programmatic API surface: the host chooses the capability boundary, and the Tactus snippet composes calls inside the sandbox.

runtime = TactusRuntime(procedure_id="host-mcp-gateway")
runtime.register_python_module("your_app", app_module)
local app = require("your_app")

local topics = app.docs.list({})
local customer = app.customers.get({ id = "customer-123" })

return {
  docs_available = #topics,
  customer_name = customer.name,
}

Use this pattern when the host application has a broad SDK-like surface and you want the client to call one small gateway instead of loading a large catalog of tool schemas. This keeps the base model context focused: the gateway can expose api.list and docs.get-style discovery so the agent loads detailed docs and examples only for the capability it needs. The model can compose the host APIs into a task-specific tool by writing a short Tactus snippet, while the host still owns authentication, policy, traces, budgets, HITL, and async handles. Plexus is one production example: its MCP server exposes execute_tactus, while the runtime injects plexus as a global for scorecards, scores, feedback, evaluations, reports, procedures, documentation, budgets, and handles.

8.6 Testing Tool Usage

For deterministic CI, run with --mock and use Mocks { ... } to:

  • mock agent behavior (tool calls + messages)
  • mock tool outputs when your procedure logic depends on them

This keeps your tests focused on the orchestration logic: “when tools return X, the procedure does Y.”