Defining your own tools¶
Beyond the built-in tools, you can define your own in Lua and
offer them to the model. A
discover_tools handler runs when
your config loads and again on every /reload, with a
ToolRegistry to add tools to, so
editing your config and reloading adds or removes a tool without restarting
the process:
agent.on('discover_tools', function(registry)
registry:add {
name = 'weather',
description = 'Look up the current weather for a city.',
-- A JSON Schema object describing the arguments, shown to the model.
parameters = {
type = 'object',
properties = {
city = { type = 'string', description = 'The city to look up.' },
},
required = { 'city' },
},
run = function(args, ctx)
local report = capture('weather ' .. shquote(args.city))
return 'The weather in ' .. args.city .. ' is ' .. report
end,
}
end)
(capture and shquote above are the same small shell helpers shown in the
rtk example below.)
The run callback receives the model's args and a
RunCtx, and returns the result the
model sees. Return a plain string for a successful result, or a { content =
'...', is_error = true } table to report a failure the model can react to.
The context exposes
ctx.cwd (the working
directory) and ctx.spill_dir
(a scratch directory for output too large to return inline, which the model
can read back).
A long-running tool can drive its own live display while it works. Inside
run, ctx:show(state)
publishes a snapshot -- any plain serializable table -- that the tool's block
renders from, and
ctx:title(text) sets a
short label shown after the tool's name in the block header (like the path an
edit shows).
To control how the block looks, add a render callback. It is handed the
latest snapshot from ctx:show and a
RenderView describing the space
available (ctx.width,
ctx.height, and
ctx.expand for the
user's full-output toggle), and returns the body to show -- a string, or an
array of lines:
registry:add {
name = 'weather',
description = 'Look up the current weather for a city.',
parameters = { type = 'object', properties = {} },
run = function(args, ctx)
ctx:title(args.city)
ctx:show { phase = 'fetching', city = args.city }
local report = capture('weather ' .. shquote(args.city))
ctx:show { phase = 'done', city = args.city, report = report }
return 'The weather in ' .. args.city .. ' is ' .. report
end,
render = function(state, view)
if state.phase == 'fetching' then
return 'looking up ' .. state.city .. '...'
end
return state.city .. ': ' .. state.report
end,
}
The last snapshot is persisted with the result, so on a resumed session the
block re-renders from it without running run again. If your config no longer
defines the tool, its block falls back to the recorded model-facing text.
An optional available callback gates whether the tool is offered at all. It
is called with the same context and returns a boolean, so a tool can withhold
itself when its prerequisites are missing (mirroring how find and grep are
only offered when ripgrep is present):
As with the rtk example, a tool is best kept in a self-contained
module alongside your config (e.g. ~/.config/wallah/weather.lua) that you
pull in with require('weather'), so it reads and reuses independently of the
rest of your config.