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.
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,
})
| field | is |
|---|---|
description | what the model is told the tool does. This is the whole of how it decides to call it. |
needs | the parameter schema, as the model is shown it. Empty means it takes none. |
run | what happens. Returns said and shown. |
transport | how 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
| transport | is | for |
|---|---|---|
lua | a body that runs in magi's own VM | anything that is a few lines of logic |
command | a program on $PATH, given the call as argv | wrapping something that already exists |
casper | casper run <tool> | the thirteen casper ships |
builtin | compiled into magi | the 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
- Output is bounded. Beyond the limit, the middle is dropped — head and tail both kept, because which one matters depends on the tool: a file read wants its head, a build that failed wants its tail. The whole of it is spilled to a file the note names.
- Secrets are masked by value. The actual contents of this process's credential variables are replaced by their names before anything enters a transcript. Not by pattern — by value, so a key that does not look like a key is caught too.
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.