Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

uji.tool

uji ships four tools, read_file, edit_file, write_file and run_command. The functions below add your own, switch tools off and on, and decide which calls need your approval.

uji.tool.add(name, spec)

Registers a tool the model can call, or replaces the tool with the same name.

FieldTypeRequiredMeaning
descriptionstringnoWhat the tool does. The model reads this to decide when to call it.
parameterstablenoA JSON Schema for the arguments, written as a Lua table.
runfunctionyesRuns the call. See below.
subjectstring or functionnoWhat policy rules match against, and what the transcript and approval question show. A function receives the arguments and returns a string. The default is the tool’s name.
policystringno"allow", "ask" or "deny". Used when no policy rule matches.
display.verbstringnoThe transcript line, such as "Read", which uji follows with the subject.
display.questionstringnoThe title of the approval question.

run(args, ctx) receives the decoded arguments and a context table. It returns the result in one of three ways:

  • It returns a string, which becomes the result at once.
  • It returns nothing and calls ctx.done(text) later.
  • It returns a function and calls ctx.done(text) later. uji calls that function to stop the work if you interrupt the turn.

A result may also be a table with text and images, in any of the three ways. images is a list in the format the uji.wire.add request describes. The model gets the images with the text, and after_tool handlers see only the text. An image with no supported media_type, no data or more than 5 MB is left out, and a note at the end of the text says so.

ctx.progress(line) shows a line under the running tool while it works.

Raises an error when run is missing, policy is not one of the three values, or subject is neither a string nor a function.

uji.tool.add("branch", {
  description = "Name of the current git branch.",
  parameters = { type = "object", properties = {} },
  policy = "allow",
  display = { verb = "Checked the branch", question = "Read the current branch?" },
  run = function()
    return io.popen("git branch --show-current"):read("*l")
  end,
})

uji.tool.remove(name)

Removes a tool and returns true if it existed.

uji.tool.remove("write_file")

uji.tool.list()

Returns the names of every registered tool.

for _, name in ipairs(uji.tool.list()) do
  uji.notify(name)
end

uji.tool.disable(names)

Turns tools off. The model still sees them, and uji denies any call to them. Plan mode uses this to take away the editing tools.

uji.tool.disable({ "edit_file", "write_file" })

uji.tool.enable(names)

Turns tools back on after uji.tool.disable.

uji.tool.enable({ "edit_file", "write_file" })

uji.tool.policy(rules)

Sets which tool calls run without asking, which ask first, and which uji refuses. rules maps a tool name to a table with allow, ask and deny lists and an optional default. A top-level default applies to every tool.

uji.tool.policy({
  default = "ask",
  run_command = {
    allow = { "git status", "git diff *", "cargo test*" },
    deny = { "/^rm\\s+-rf/" },
  },
})

Each rule matches the tool’s subject: the path for the file tools, the command line for run_command, and the subject of a tool you add. A rule is an exact string, a glob when it contains *, ? or [, or a regular expression between slashes.

uji checks deny rules first, then allow, then ask, and uses the first match. With no match, it uses the tool’s default, then the policy the tool declares, then the top-level default, then ask. read_file declares allow and the other built-in tools declare ask.

Each call replaces the tools it names and keeps the others. A before_tool hook runs before the policy and overrides it.

uji.tool.confine(enabled)

With true, limits read_file, edit_file and write_file to the working directory and the roots from uji.tool.roots. A ../ path or a symbolic link cannot reach outside them. run_command is not limited. With false, lifts the limit. Returns whether the limit is on, so calling it with no argument reads the setting.

uji.tool.confine(true)
local confined = uji.tool.confine()

uji.tool.roots(paths)

Replaces the directories the file tools may reach besides the working directory, and returns the list. A path may start with ~/. Calling it with no argument returns the list without changing it.

uji.tool.roots({ "~/reference/other-project" })
local roots = uji.tool.roots()