nervネルフ

Writing a tool

A declaration — a name, a description, a parameter schema, and how to reach it. Written in Lua and read at start-up; none of it is compiled in.

plate 04three sources, one registry, four ways to reach a tool

Four fields

magi.tool("branch", {
  description = "the git branch this directory is on",
  needs = { },
  run = function()
    local out = magi.exec("git", { "branch", "--show-current" })
    return {
      said  = out,                       -- the model reads this. costs context.
      shown = "on " .. out,              -- the person sees this. costs nothing.
    }
  end,
})
fieldis
descriptionwhat the model is told the tool does. This is the whole of how it decides to call it.
needsthe parameter schema, as the model is shown it. Empty means it takes none.
runwhat happens. Returns said and shown.
transporthow it is reached, when it is not this Lua body — see below.

said and shown

The split is the whole idea. said enters the transcript, is replayed on every subsequent request, and costs context every turn until the conversation is compacted. shown is drawn once and costs nothing.

A tool that returns a hundred lines of diff as said has spent that budget for the rest of the session. The same diff as shown, with said = "patched 3 files", costs four words.

Four ways to be reached

transportisfor
luaa body that runs in magi's own VManything that is a few lines of logic
commanda program on $PATH, given the call as argvwrapping something that already exists
caspercasper run <tool>the thirteen casper ships
builtincompiled into magithe floor: read, write, edit

The registry does not care which. One name, one entry, and the model sees the same declaration whichever way it is reached.

Two caps you do not opt out of

Here rather than in the tools, because a peer is another program and cannot be trusted to cap itself, a Lua tool has no way to write a spill file, and a shipped declaration has no knob to set. Every result of every transport passes through one place, which is the only place that is true of.

A tool that asks a question

A declaration reads args.answered to know it is resuming. The answer travels with the arguments rather than beside them — merged, so a declaration writes args.answered and not args.call.answered. The answer is one more thing known about this call, which is what an argument is.