Design notes for new guest-to-host interaction verbs in the Endo daemon, motivated by the Llamadrome LLM agent integration.
Today requestEvaluation(source, codeNames, petNamePaths, resultName) bundles
code + endowment bindings in a single message. The LLM proposes both "what to run"
and "what to give it access to" at the same time. The host approves or rejects
the whole thing.
This has two issues:
- The LLM is suggesting which capabilities it should receive -- a social engineering surface.
- The guest interaction vocabulary is limited:
send(fire-and-forget) andrequest(ask for a single opaque value). There's no structured input.
The LLM writes code with named variable slots but makes no claim about what capabilities those variables should bind to. The code is inert until the user explicitly endows it.
E(powers).define(
'E(counter).incr()',
{
counter: {
pattern: M.remotable(),
label: 'A counter to increment',
},
},
)- Produces a
CodeDefinitionformula:{ source, codeNames }-- no values. - Does not require host approval (it's just inert code).
- The host sees the code + slot descriptions and decides what to wire in.
The user takes an existing definition and supplies pet names for each slot. This is where authority flows, so this is where approval happens.
define endow
Guest/LLM ------+ CodeDefinition ------+ EvalFormula
| { source, | { source,
| codeNames } | codeNames,
| | values: [...] } --> execution
| |
| User picks |
+-- pet names ----------+
Like request, but the user is asked to fill out a form where each field is
described by an Endo pattern. Returns a promise that resolves to a record
matching the field names.
E(powers).form('HOST', {
fields: {
apiEndpoint: { pattern: M.string(), label: 'API base URL' },
retryCount: { pattern: M.and(M.number(), M.gte(0)), label: 'Max retries' },
verbose: { pattern: M.boolean(), label: 'Enable verbose logging' },
},
description: 'Configure the sync worker',
})Endo patterns serve triple duty: schema definition, input validation, and UI rendering hint.
| Pattern | UI Element |
|---|---|
M.string() |
Text input |
M.number() |
Number spinner |
M.boolean() |
Toggle / checkbox |
M.or(M.string(), M.undefined()) |
Optional text input |
M.remotable() |
Pet name picker (filtered to remotables) |
M.arrayOf(M.string()) |
Multi-value input |
M.record(M.string(), M.number()) |
Key-value pairs |
The pattern validates each field before submission, so the host UI can give immediate feedback.
define is really a special case of form where each field is an endowment
slot. The host approval UI is the form -- filling it out is the act of
endowing. Approval and endowment collapse into a single user gesture.
LLM Host UI
| |
|-- define(source, slotSchemas) -->| shows code + slot form
| | each slot: pet name picker
| | constrained by pattern
| | displayed as links (not chips)
| | user fills slots from directory
|<-- endowed result ---------------| validates, creates eval formula
| |
- Safest model -- the LLM never suggests what powers it should get.
- Code is inert until user explicitly endows.
definedoesn't need host approval; onlyendowdoes (that's where authority flows).
- Useful when the LLM's endowments are benign (read-only values, counters, etc.).
- The existing
requestEvaluationstays for this case. - UI requirements for this mode:
- Display the user's pet names, not the guest's internal names.
- Don't make endowments editable -- show them as read-only links (clickable to inspect) rather than chips (which imply editability).
- The LLM proposed specific bindings; changing them would change semantics.
The daemon already has the pieces:
EvalFormulastores{ source, names, values }separately.formulateEvalcreates eval formulas with formula identifiers for endowments.- Values are instantiated lazily during formula incarnation, not at definition time.
- A
CodeDefinitionformula type would just omit thevaluesfield.
The form verb would introduce a new message type alongside 'request' and
'eval-request', with a fields record of { pattern, label } entries and a
Responder for the structured response.
PR #3074 (feat(daemon): Persist messages across restarts) introduces durable message infrastructure that
directly enables the define/endow/form design.
New formula types:
MailboxStoreFormula-- a dedicated pet store for mailbox entries.MailHubFormula-- a directory-like view over the mailbox, addressable by message number.MessageFormula-- each message is now a durable formula:{ type: 'message', messageType, from, to, date, promiseId, resolverId, ... }.PromiseFormula/ResolverFormula-- durable promise/resolver pairs so request responses survive daemon restarts.
Message numbers shift to bigint for unbounded sequencing.
Responder renamed: respondId() becomes resolveWithId(). The responder
is no longer an ephemeral in-memory exo -- it's backed by a durable
resolver formula created via formulatePromise().
MAIL special name: Both guests and hosts get a MAIL entry in their
special store, pointing to their mail hub. Messages become addressable:
E(powers).lookup('MAIL', '3') fetches message #3 as a directory.
-
Messages-as-formulas is the right foundation. A
definemessage would be a newMessageFormula.messageType(alongside'request'and'package'). Since messages are now durable formulas, a code definition persists across restarts -- the user can review it later and endow it when ready. -
Durable promise/resolver pairs decouple request from response. The old
Responderwas ephemeral. The newPromiseFormula/ResolverFormulasystem means adefinecan create a durable promise that only resolves when the user endows and executes. No timeout pressure. -
MAILas a directory means messages are inspectable by number. Aformresponse could be a structured record stored as a formula, addressable viaMAIL/42/response. -
Reserved message names (
FROM,TO,DATE,TYPE,DESCRIPTION,STRINGS,PROMISE,RESOLVER) formalize the message schema. New verbs would extend this set with entries likeSOURCE,CODE_NAMES,PATTERN. -
eval-requestis absent from PR #3074'sMessageFormula.messageType-- it only handles'request' | 'package'. Theeval-requesttype from thellmbranch needs to be reconciled. The define/endow split offers a cleaner path: instead of adding'eval-request'as a third durable message type, add'definition'and let endowment happen through the form mechanism.
The llm branch's requestEvaluation uses the old ephemeral Responder
pattern (respondId). PR #3074 replaces that with durable
PromiseFormula/ResolverFormula (resolveWithId). When these branches merge,
requestEvaluation in mail.js must adopt the new formulatePromise() path.
The define/endow design could sidestep this conflict: instead of patching
eval-request to be durable, replace it with durable definition messages +
the form-based endowment flow.