12. Vanes V: Gall and Userspace
This lesson covers Gall: the $agent type, running agents, and userspace software updates.
Gall
Currently, the end user zone of Urbit, denoted “userspace”, is supplied primarily by the /sys/vane/gall vane. Much as Arvo acts as a dispatcher and state manager for a functional OS in terms of transactions between vanes, Gall acts as a dispatcher and state manager for longrunning daemons. These daemons are referred to as “agents”, and comprise the main way that users actually use Urbit. (Other parts of userspace include generators and threads.)
Gall is responsible to manage userspace applications and their state, including subscribers. To fully understand agent userspace, we want to cover a few related topics:
Agents
/sys/vane/gallTreaty/Docket publication
Agent wrappers
Historic (dynamic) Gall
We will cover threads in the lesson on Khan and Lick. We covered generators previously in the lesson on Dill and Dojo.
Agents
An agent is a piece of software that is primarily focused on maintaining and distributing a piece of state with a defined structure. It exposes an interface that lets programs read, subscribe to, and manipulate the state. Every event happens in an atomic transaction, so the state is never inconsistent. Since the state is permanent, when the agent is upgraded with a change to the structure of the state, the developer provides a migration function from the old state type to the new state type.
What is an agent in practice? In contemporary static Gall, an agent is a core that hews to the definition:
:: ::::
:::: ++gall :: (1g) extensions
:: ::::
++ gall ^?
|%
::
:: +agent: app core
::
++ agent
=< form
|%
+$ step (quip card form)
+$ card (wind note gift)
+$ note
$% [%agent [=ship name=term] =task]
[%arvo note-arvo]
[%pyre =tang]
::
[%grow =spur =page]
[%tomb =case =spur]
[%cull =case =spur]
==
+$ task
$% [%watch =path]
[%watch-as =mark =path]
[%leave ~]
[%poke =cage]
[%poke-as =mark =cage]
==
+$ gift
$% [%fact paths=(list path) =cage]
[%kick paths=(list path) ship=(unit ship)]
[%watch-ack p=(unit tang)]
[%poke-ack p=(unit tang)]
==
+$ sign
$% [%poke-ack p=(unit tang)]
[%watch-ack p=(unit tang)]
[%fact =cage]
[%kick ~]
==
++ form
$_ ^|
|_ bowl
++ on-init
*(quip card _^|(..on-init))
::
++ on-save
*vase
::
++ on-load
|~ old-state=vase
*(quip card _^|(..on-init))
::
++ on-poke
|~ [mark vase]
*(quip card _^|(..on-init))
::
++ on-watch
|~ path
*(quip card _^|(..on-init))
::
++ on-leave
|~ path
*(quip card _^|(..on-init))
::
++ on-peek
|~ path
*(unit (unit cage))
::
++ on-agent
|~ [wire sign]
*(quip card _^|(..on-init))
::
++ on-arvo
|~ [wire sign-arvo]
*(quip card _^|(..on-init))
::
++ on-fail
|~ [term tang]
*(quip card _^|(..on-init))
--
--
-- ::gall
A Gall agent must have ten arms. (There's a fascinating bit of self-reference in the state definition going on, and to be honest I'm a little surprised that it works, but it is an iron core.) The definitions here are for |~ barsig arms in a ^| ketbar core.
So we must at last really grapple with the core variance model in Urbit. This is often notorious to understand because we don't have great metaphors or analogues to type variance in real life. Right now, what we need to understand is that an iron/contravariant core is opaque: That is, we use this to define an interface in which the argument can be less specific than the interface and the result can be more specific. Contravariance is useful for flexibility in input values (samples).
An
%ironcoreihas a write-only sample (payload head,+6.i) and an opaque context (payload tail,+7.i). A corejwhich nests within it must be a%goldor%ironcore, such that+6.inests within+6.j. Hence, contravariant.
The archetypal Gall agents in /sys/lull are composed using iron gates since they will be used as examples for building actual agent cores. Likewise, the +rs and sister gates in /sys/hoon are built using iron doors with specified rounding behavior so when you actually use the core (like +add:rs) the core you are using has been built as an example.
How are the iron gate runes actually implemented in the Hoon type system? (See
+deem:nest:utand+peel:ut.)Try to implement an agent missing an arm, like
+on-fail.Bonus question: what half-implemented rune produces an
%ironcore?
We construct an agent explicitly in an /app file by applying %- agent:gall to a correctly-shaped core.
What does each arm produce?
++ on-init (quip card _agent)
++ on-save (vase)
++ on-load (quip card _agent)
++ on-poke (quip card _agent)
++ on-watch (quip card _agent)
++ on-leave (quip card _agent)
++ on-peek (unit (unit cage))
++ on-agent (quip card _agent)
++ on-arvo (quip card _agent)
++ on-fail (quip card _agent)Finally, we can take a gander at what that ubiquitous +quip is:
++ quip
|$ [item state]
[(list item) state](It's just a wrapper for (list item) state.)
Basically, every arm must produces a list of effects and a state change, if any.
What does each arm expect?
++ on-init :: not a gate, only an arm
++ on-save :: not a gate, only an arm
++ on-load |= =vase
++ on-poke |= =cage
++ on-watch |= =path
++ on-leave |= =path
++ on-peek |= =path
++ on-agent |= [=wire =sign:agent:gall]
++ on-arvo |= [=wire =sign-arvo]
++ on-fail |= [=term =tang]We'll need to differentiate the Gall $signs and the Arvo $signs in a moment.
When +ford:clay reads in a Gall agent file from /app, it automatically composes cores together using => tisgar. (This leads to a slightly disconcerting situation in which the cores are simply present serially in a file.)
Compare the definition of
$agent:shoewith$agent:gall. How does this correctly extend the Gall agent definition for the type system?
Vane
While Gall facilitates very complex userspace apps, the vane itself is rather modest, weighing in at less than half the size of Clay or Ames. Gall knows how to route events to the handler arms in a standard agent core, and it instruments upgrades and subscriptions.
However, we have to consider Gall at two levels: the vane level, which manages top-level state like the set of running agents and queued moves, and the agent level, which manages agents as doors.
Gall is a landlocked vane. It has no runtime counterpart.
/sys/lull Definition
/sys/lull Definition:: ::::
:::: ++gall :: (1g) extensions
:: ::::
++ gall ^?
|%
+$ boar (map [=wire =ship =term] nonce=@) :: and their nonces
+$ dude term :: server identity
+$ gill (pair ship term) :: general contact
+$ load (list [=dude =beak =agent]) :: loadout
+$ scar :: opaque duct
$: p=@ud :: bone sequence
q=(map duct bone) :: by duct
r=(map bone duct) :: by bone
== ::
+$ suss (trel dude @tas @da) :: config report
+$ well (pair desk term) ::
+$ deal
$% [%raw-poke =mark =noun]
task:agent
==
+$ unto
$% [%raw-fact =mark =noun]
sign:agent
==
-- ::gallMost of the important types have been separated and are called out below.
(Variance again: ^? ketwut is for a lead/bivariant core.)
Vane State
+$ state
$: system-duct=duct
outstanding=(map [wire duct] (qeu remote-request))
contacts=(set ship)
yokes=(map term yoke)
blocked=(map term (qeu blocked-move))
=bug
==system-ductis the set of outbound moves to other vanes (like Ames for subscriptions) or remote agent contacts.outstandingis the outstanding request queue.contactsis the set of other ships with which we are in communication.yokesis the set of running agents.blockedis the set of moves to agents that haven't been started yet.bugis the debug print configuration.
Vane Moves
|%
+$ gift :: outgoing result
$% [%boon payload=*] :: ames response
[%done error=(unit error:ames)] :: ames message (n)ack
[%unto p=unto] ::
== ::
+$ task :: incoming request
$~ [%vega ~] ::
$% [%deal p=sock q=term r=deal] :: full transmission
[%sear =ship] :: clear pending queues
[%jolt =desk =dude] :: (re)start agent
[%idle =dude] :: suspend agent
[%load =load] :: load agent
[%nuke =dude] :: delete agent
[%doff dude=(unit dude) ship=(unit ship)] :: kill subscriptions
[%rake dude=(unit dude) all=?] :: reclaim old subs
$>(%init vane-task) :: set owner
$>(%trim vane-task) :: trim state
$>(%vega vane-task) :: report upgrade
$>(%plea vane-task) :: network request
[%spew veb=(list verb)] :: set verbosity
[%sift dudes=(list dude)] :: per agent
== ::
--Agent State
+$ bitt (map duct (pair ship path)) :: incoming subs
+$ boat (map [=wire =ship =term] [acked=? =path]) :: outgoing subs
+$ bowl :: standard app state
$: $: our=ship :: host
src=ship :: guest
dap=term :: agent
== ::
$: wex=boat :: outgoing subs
sup=bitt :: incoming subs
$= sky :: scry bindings
%+ map path ::
((mop @ud (pair @da (each page @uvI))) lte) ::
== ::
$: act=@ud :: change number
eny=@uvJ :: entropy
now=@da :: current time
byk=beak :: load source
== == :: Every agent needs two parts of its state: the $bowl, which is the information outside of the agent that Gall needs to communicate for the vane, and the
Agent Moves
|%
+$ card (wind note gift)
+$ note
$% [%agent [=ship name=term] =task]
[%arvo note-arvo]
[%pyre =tang]
::
[%grow =spur =page]
[%tomb =case =spur]
[%cull =case =spur]
==
+$ task
$% [%watch =path]
[%watch-as =mark =path]
[%leave ~]
[%poke =cage]
[%poke-as =mark =cage]
==
+$ gift
$% [%fact paths=(list path) =cage]
[%kick paths=(list path) ship=(unit ship)]
[%watch-ack p=(unit tang)]
[%poke-ack p=(unit tang)]
==
+$ sign
$% [%poke-ack p=(unit tang)]
[%watch-ack p=(unit tang)]
[%fact =cage]
[%kick ~]
==
--A Gall $card differs from an Arvo card:
:: Arvo
+$ card (cask) :: tagged, untyped event
++ cask |$ [a] (pair mark a) :: marked data builder
::
:: Gall
+$ card (wind note gift)
++ wind
|$ :: a: forward
:: b: reverse
::
[a b]
$% :: %pass: advance
:: %slip: lateral
:: %give: retreat
::
[%pass p=path q=a]
[%slip p=a]
[%give p=b]
==
+$ note
$% [%agent [=ship name=term] =task]
[%arvo note-arvo]
[%pyre =tang]
::
[%grow =spur =page]
[%tomb =case =spur]
[%cull =case =spur]
==
+$ gift
$% [%fact paths=(list path) =cage]
[%kick paths=(list path) ship=(unit ship)]
[%watch-ack p=(unit tang)]
[%poke-ack p=(unit tang)]
==Gall does not permit a %slip, so a card is either:
[%pass path note][%give gift]
Structure
The two main engine cores within /sys/vane/gall are:
+moArvo move handler+apagent-level core
The +abet pattern used in Gall prefixes each arm with the containing door abbreviation so you can remain more easily oriented within /sys/vane/gall.
+mo Arvo move handler
+mo Arvo move handlerMany +mo calls resolve into +ap calls. It mainly sets things up around particular per-agent calls.
+ap agent-level core
+ap agent-level coreTo run an agent, we have to know the state of the agent, which includes its state and relevant bowl information:
:: $yoke: agent runner state
::
+$ yoke
$% [%nuke sky=(map spur @ud)]
$: %live
control-duct=duct
run-nonce=@t
sub-nonce=_1
=stats
=bitt
=boat
=boar
code=*
agent=(each agent vase)
=beak
marks=(map duct mark)
sky=(map spur path-state)
ken=(jug spar:ames wire)
== ==control-ductis the duct ofrun-nonceis a unique nonce for each build.sub-nonceis global%watchnonce.statsbittis the set of incoming subscriptions (for thebowl).boatis the set of outgoing subscriptions (for thebowl).codeis the most recently loaded code as a noun.agentis the agent core, possibly as a vase.beakis the compilation source.marksis the map of mark conversion requests.skyis the map of scry bindings.kenis the map of sets of open%keenremote scry requests.
A typical call from +mo to +ap will be predicated on +ap setting up a Gall agent with its state and processing the incoming move through the appropriate arm.
For instance, this is the lifecycle of a scry call to Gall:
A scry handler (
roof) produces a call to Gall's+scryarm.+moas a door needs a duct and a set of moves.+mo-peeksets up a call to+ap-peekalong the given path with the appropriate care.+ap-abedsets up the agent noun for evaluation.+ap-yokeloads the actual agent state; an agent is a door with a state and bowl sample.
+ap-peekparses the scry path appropriately.+ap-mule-peekevaluates the code using[9 2 0 1]and+mock(seeca01for a refresher).+ap-agent-coresets up the agent core with its current bowl and state; this includes a+on-peekarm since we know the shape of the$agentcore.+ap-construct-bowlproduces the agent-ready bowl from Gall-level information.
The lifecycle of a poke looks like this:
A move is injected targeting Gall's
+callarm as a%deal(indicating that the move goes to an agent).+calldispatches to+mo-handle-usefor an agent.+mo-handle-localis for running local agents.+mo-applyand+mo-apply-sureprepare to call+ap.+ap-abedsets up the agent noun for evaluation.+ap-applydispatches several kinds of operations, including pokes.+ap-pokequeues a%poke-ack(since it's first among moves) and calls+ap-ingest.+ap-ingestcalls the agent arm.+ap-handle-resultand+ap-handle-peerstake care of watches etc.+ap-agent-coresets up the agent core with its current bowl and state; this includes a+on-pokearm since we know the shape of the$agentcore.
+ap-abetyields the list of cards to resolve back to+mo, but also the$yoke, which is the new agent state for Gall'sstate.
+mo-abetfinalizes.
What about an agent modification like |nuke? Let's see the lifecycle of that call.
/gen/hood/nuke→%kiln-nuke/lib/hood/kiln→+poke-nuke[%pass /nuke %arvo %g [%nuke dude]]/sys/vane/gall:+call+mo+mo-nuke+ap+ap-abed+ap-nukeis where the real work is done. Review it.+ap-ingest
+ap-abet
+mo-abet
Clay actually governs which agents can run on a given desk. How does |install instigate this?
See
+goadin/sys/vane/clay.
There is additionally some plumbing around Gall receiving responses in +take for the vane (/sys) versus for an agent (/use).
We'll need to differentiate the Gall $signs and the Arvo $signs in a moment.
Scry interface
Gall brokers two kinds of scries: vane scries and agent scries.
In order to hit the vane-level endpoints, the beginning of the the
spur(e.g. thepathafter thebeak) must be a%$empty element. For example:
.^(desk %gd /=acme=/$)
.^((set [=dude:gall live=?]) %ge /=base=/$)
.^((list path) %gt /=acme=//foo)An agent scry has the form /=agent=/path/to/scry and may accept any care. %x cares must include a terminal mark in the path, however.
Gall also dispatches scries to agents' +on-peek arms. This takes place via +mo-peek→+ap-peek→+on-peek→+ap-mule-peek.
See +scry for details of both.
Treaty and Docket
There are two ways to distribute nouns over Ames today:
Mark a desk
|publicand use Clay to directly synchronize desks.Use the
/app/treatyagent from%landscapeto discover and install agents.
Landscape (formerly Grid) is formally a Tlon product. It primarily consists of two agents (and associated marks, libraries, etc.):
/app/treatyhandles publishing and advertising application desks./app/dockethandles retrieving, validating, and installing application desks.
Together they query a remote %treaty instance to +install a particular desk.
Examine
$alliancein/sur/treaty.Examine
+publish:soand+watch:trin/app/treaty.Examine
+install:chin/app/docket.
/app/treaty in particular has pretty tight construction and I commend its style to you.
Updates
When Gall receives a newly rebuilt agent from Clay, it calls the gate produced by the
+on-loadarm of the new agent with the state extracted from the old agent. If there is a crash in any+on-loadcalls or in the handling of any effects they emit (which can include more agent activations), then the whole event crashes, canceling the commit. This effectively gives any agent the ability to abort a commit by crashing.
Gall's
+callarm receives a%loadmove with a noun of a core built by+ford:clay.+mo-coreis a handle to+mobecause no+abetis needed.+mo-loadinstalls agents pretty mechanically, by simply+skimming over the%liveagents.+mo-receive-corechecks whether the agent is running.If it is, then
+apis invoked to update the agent:+ap-abed+ap-reinstall+on-save:ap-agent-core+ap-installis the install wrapper.+ap-upgrade-state+on-init:ap-agent-core+on-load:ap-agent-core
+ap-abet+mo-clear-queueflushes the blocked tasks pending for a new agent.
If it isn't, then we have to create it in
+ap:+ap-abed+ap-upgrade-state+on-init:ap-agent-core+on-load:ap-agent-core
+mo-idleputs the agent to sleep if it's in the kill list (to be retired due todesk/bill).
+mo-abetfinalizes moves and state changes.
Note that this is independent of Treaty and Docket once the remote desk has been installed.
Agent Wrappers as Core Modifiers
.An agent wrapper (like dbug) is a tool to wrap additional handlers around an agent core. These can wrap the internal Gall agent with new functionality by catching pokes and other standard moves, then re-dispatching to the normal arms if no special behavior is needed.
> :my-agent +dbug [%state 'value']Examine
/lib/dbug.In particular, how does the
+on-pokewrapper arm work?How does the
+dbuggenerator work?
Using agent transformers to extend agents is a very nice conceptual pattern. But in practice, there are three pretty big problems with it:
You need to edit the agent code yourself.
Stateful transformers can break the agent.
The agent's world will also get transformed.
Dynamic Gall
An earlier incarnation of Gall, dynamic Gall, specified its arms in terms of the names of the move coming back or the mark of the poke coming in. (This is what I learned on, way back when.)
For instance, this agent was an earlier version of the egg timer app:
:: |start %egg
:: :egg ~s5
|%
+$ effect (pair bone syscall)
+$ syscall [%wait path @da]
--
|_ [bowl:gall ~]
++ poke-noun
|= t=@dr
^+ [*(list effect) +>.$]
:_ +>.$ :_ ~
[ost %wait /egg-timer (add now t)]
++ wake
|= [=wire error=(unit tang)]
^+ [*(list effect) +>.$]
~& "Timer went off!"
[~ +>.$]
--Exercises
Your assignment is to produce a minimalist Gall-like agent handler: a userspace framework for producing “toy” agent-like applications. Let's call them “scamps”.
The scamp's state is defined in a
$stateblock at the top of its file, e.g.:+$ state $: scores=(list @) hi-score=@ ==Scamps do not support state upgrades, so no version tag is provided.
A scamp requires the following arms for the developer:
|% ++ on-init ++ on-poke ++ on-peek --You should be able to poke and peek into a scamp. It has no subscription model.
A scamp specification file is NOT implicitly chained with a running
=>tisgal. Compose explicitly.
As a final aside, I believe that building an %aqua/%pyro testbed along the lines of Gall should also be feasible for you at this point.
Last updated