8 Tool Integration
Tools are the primary way to make agents observable and reliable.
This chapter covers:
- Defining tools in Tactus
- Grouping tools into Toolsets
- Wiring tools/toolsets into Agents
- Keeping tool usage testable (mock mode)
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.”