Swapping a program
A role is what a program is for; which program fills it is one line of configuration. balthasar, casper and melchior are the defaults, not the only choices.
Three roles
magi does not know that its memory is called balthasar. It knows the memory
role is filled by whatever magi.memory names, and that whatever fills it answers
the role’s verbs. Change the name and the program changes — nothing else does.
| role | named by | default | core verbs | unfilled |
|---|---|---|---|---|
| memory | magi.memory | balthasar | observe replay sessions | magi refuses to start: this is the store |
| tools | magi.tools | casper | tools run | an ordinary session with the three builtin tools |
| model | magi.melchior | melchior | models ask | no model to ask |
magi.memory = "remembrance" -- a program on $PATH
magi.tools = "workbench"
magi.model is the model, not the program. It named the model before roles existed, so the model role is named by magi.melchior, which has always meant the program. A table there is still that program’s settings; only a string names it.Core, and everything else
| if a program refuses it | |
|---|---|
| core | it cannot fill the role. The gate fails, and magi doctor says so. |
| extension | magi carries on with less — a model tool not declared, no compaction, an outcome loop that records nothing. |
An extension is refused, not omitted: a program that does not do plan
answers plan with a refusal in the reply shape, like any other. Silence is the one
thing that is not allowed. The line was drawn from what magi actually calls, not from what any
implementation happens to offer — see the memory
verbs.
sessions is core because it was measured to be. It was an extension in the first draft. A memory layer written to that draft was pointed at magi with sessions refused, and --resume came back empty at exit 0, saying nothing — magi asks it first, to find the newest run, before it can replay one. Running a second program against the contract is how the line got drawn in the right place.Before you name one
Ask the binary, not its source. The role gate lists the core verbs a program advertises and fails on any it does not:
$ scripts/gate-role.sh tools ./workbench gate-role: workbench as tools tools core answered run core answered extensions 2 of 2 gate-role: workbench fills tools.
And magi doctor asks the same of whatever your configuration names, before a
session depends on it. With magi.memory = "casper", which cannot:
roles memory casper — ~/.local/bin/casper — cannot fill memory: it answers no observe, replay, sessions tools casper — ~/.local/bin/casper — 13 tools model melchior — ~/.local/bin/melchior — 449 models
Two that exist to prove it
magi’s repository carries a second implementation of two of the roles, each one file of Rust with no dependencies, written from the contract rather than from magi’s source. Both run in magi’s own test suite against real sessions: one records a conversation and resumes it, the other is offered to the model, called, and answers.
| fills | answers | |
|---|---|---|
examples/remembrance | memory | the family floor and the three core verbs, on a socket; refuses every extension |
examples/workbench | tools | tools and run, one exec per call, and one tool that reverses a string |
rustc examples/workbench/workbench.rs -O -o ~/.local/bin/workbench
What a tools program is told
Its settings come from the table named after the program itself, and ride on every spawn — there is no process alive between calls to send them to once.
magi.tools = "workbench"
magi.workbench = { quiet = true } -- arrives as JSON in MAGI_TOOLS_CONFIGURE
magi.workbench_sha256 = "…" -- the bytes it must hash to, from magi doctor
For casper that is magi.casper and magi.casper_sha256, which is
why a configuration written before roles existed still works: the default program’s table has
the name it always had. The same value is also set as CASPER_CONFIGURE, which is
what casper reads.
Only your own configuration names one
A role’s program is started every turn with the session’s authority, which is more than a
declared tool — and a project file is already refused a tool. So a .magi.lua that
names a program for any role stops the session with a reason, rather than running what the
checkout shipped beside itself. A project may still tune a sibling through its settings table;
what is privileged is the name.
Writing one
Answer the family floor — verbs and client, in the reply shape —
and the role’s core. Refuse the rest by name. There is no registration and no library to link:
a library shared between these programs is the dependency the whole arrangement exists to
prevent. The contract is ROLES.md, beside FAMILY.md in each of the
four repositories.