Skip to content

Instantly share code, notes, and snippets.

@zmanian
Last active February 7, 2026 04:37
Show Gist options
  • Select an option

  • Save zmanian/d58c9df4b76dffe3e97c4094bc582f77 to your computer and use it in GitHub Desktop.

Select an option

Save zmanian/d58c9df4b76dffe3e97c4094bc582f77 to your computer and use it in GitHub Desktop.
Endo daemon design: define/endow/form verbs for LLM agent interaction

Endo Daemon: define/endow/form Verbs

Design notes for new guest-to-host interaction verbs in the Endo daemon, motivated by the Llamadrome LLM agent integration.

Problem

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:

  1. The LLM is suggesting which capabilities it should receive -- a social engineering surface.
  2. The guest interaction vocabulary is limited: send (fire-and-forget) and request (ask for a single opaque value). There's no structured input.

Three New Verbs

1. define -- LLM proposes code, user endows

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 CodeDefinition formula: { 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.

2. endow -- Bind capabilities to a definition

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  ----------+

3. form -- Structured input via Endo patterns

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',
})

How Patterns Drive the UI

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 + form = Combined Flow

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
 |                                  |

Trust Models

Mode 1: LLM proposes, user endows (define)

  • Safest model -- the LLM never suggests what powers it should get.
  • Code is inert until user explicitly endows.
  • define doesn't need host approval; only endow does (that's where authority flows).

Mode 2: LLM self-serves with constrained endowments (requestEvaluation)

  • Useful when the LLM's endowments are benign (read-only values, counters, etc.).
  • The existing requestEvaluation stays 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.

Existing Architecture Support

The daemon already has the pieces:

  • EvalFormula stores { source, names, values } separately.
  • formulateEval creates eval formulas with formula identifiers for endowments.
  • Values are instantiated lazily during formula incarnation, not at definition time.
  • A CodeDefinition formula type would just omit the values field.

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.


Relation to PR #3074: Message & Promise Persistence

PR #3074 (feat(daemon): Persist messages across restarts) introduces durable message infrastructure that directly enables the define/endow/form design.

What PR #3074 Changes

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.

How This Enables define/endow/form

  1. Messages-as-formulas is the right foundation. A define message would be a new MessageFormula.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.

  2. Durable promise/resolver pairs decouple request from response. The old Responder was ephemeral. The new PromiseFormula/ResolverFormula system means a define can create a durable promise that only resolves when the user endows and executes. No timeout pressure.

  3. MAIL as a directory means messages are inspectable by number. A form response could be a structured record stored as a formula, addressable via MAIL/42/response.

  4. Reserved message names (FROM, TO, DATE, TYPE, DESCRIPTION, STRINGS, PROMISE, RESOLVER) formalize the message schema. New verbs would extend this set with entries like SOURCE, CODE_NAMES, PATTERN.

  5. eval-request is absent from PR #3074's MessageFormula.messageType -- it only handles 'request' | 'package'. The eval-request type from the llm branch 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.

Merge Coordination

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment