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/gall
- Treaty/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 (sample
s).
An
%iron
corei
has a write-only sample (payload head,+6.i
) and an opaque context (payload tail,+7.i
). A corej
which nests within it must be a%gold
or%iron
core, such that+6.i
nests 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:ut
and++peel:ut
.) - Try to implement an agent missing an arm, like
++on-fail
. - Bonus question: what half-implemented rune produces an
%iron
core?
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 +$sign
s and the Arvo +$sign
s 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:shoe
with+$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
:: :::::::: ++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 sequenceq=(map duct bone) :: by ductr=(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==-- ::gall
Most 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=ductoutstanding=(map [wire duct] (qeu remote-request))contacts=(set ship)yokes=(map term yoke)blocked=(map term (qeu blocked-move))=bug==
system-duct
is the set of outbound moves to other vanes (like Ames for subscriptions) or remote agent contacts.outstanding
is the outstanding request queue.contacts
is the set of other ships with which we are in communication.yokes
is the set of running agents.blocked
is the set of moves to agents that haven't been started yet.bug
is 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 :: hostsrc=ship :: guestdap=term :: agent== ::$: wex=boat :: outgoing subssup=bitt :: incoming subs$= sky :: scry bindings%+ map path ::((mop @ud (pair @da (each page @uvI))) lte) ::== ::$: act=@ud :: change numbereny=@uvJ :: entropynow=@da :: current timebyk=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:
++mo
Arvo move handler++ap
agent-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
Many ++mo
calls resolve into ++ap
calls. It mainly sets things up around particular per-agent calls.
++ap
agent-level core
To 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)]$: %livecontrol-duct=ductrun-nonce=@tsub-nonce=_1=stats=bitt=boat=boarcode=*agent=(each agent vase)=beakmarks=(map duct mark)sky=(map spur path-state)ken=(jug spar:ames wire)== ==
control-duct
is the duct ofrun-nonce
is a unique nonce for each build.sub-nonce
is global%watch
nonce.stats
bitt
is the set of incoming subscriptions (for thebowl
).boat
is the set of outgoing subscriptions (for thebowl
).code
is the most recently loaded code as a noun.agent
is the agent core, possibly as a vase.beak
is the compilation source.marks
is the map of mark conversion requests.sky
is the map of scry bindings.ken
is the map of sets of open%keen
remote 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++scry
arm.++mo
as a door needs a duct and a set of moves.++mo-peek
sets up a call to++ap-peek
along the given path with the appropriate care.++ap-abed
sets up the agent noun for evaluation.++ap-yoke
loads the actual agent state; an agent is a door with a state and bowl sample.
++ap-peek
parses the scry path appropriately.++ap-mule-peek
evaluates the code using[9 2 0 1]
and++mock
(seeca01
for a refresher).++ap-agent-core
sets up the agent core with its current bowl and state; this includes a++on-peek
arm since we know the shape of the+$agent
core.++ap-construct-bowl
produces the agent-ready bowl from Gall-level information.
The lifecycle of a poke looks like this:
- A move is injected targeting Gall's
++call
arm as a%deal
(indicating that the move goes to an agent).++call
dispatches to++mo-handle-use
for an agent.++mo-handle-local
is for running local agents.++mo-apply
and++mo-apply-sure
prepare to call++ap
.++ap-abed
sets up the agent noun for evaluation.++ap-apply
dispatches several kinds of operations, including pokes.++ap-poke
queues a%poke-ack
(since it's first among moves) and calls++ap-ingest
.++ap-ingest
calls the agent arm.++ap-handle-result
and++ap-handle-peers
take care of watches etc.++ap-agent-core
sets up the agent core with its current bowl and state; this includes a++on-poke
arm since we know the shape of the+$agent
core.
++ap-abet
yields the list of cards to resolve back to++mo
, but also the+$yoke
, which is the new agent state for Gall'sstate
.
++mo-abet
finalizes.
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-nuke
is 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
++goad
in/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 +$sign
s and the Arvo +$sign
s 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. thepath
after thebeak
) must be a%$
empty element. For example:
.^(desk %gd /=acme=/$).^((set [=dude:gall live=?]) %ge /=base=/$).^((list path) %gt /=acme=//foo)
%d
: get desk of app%e
: running apps%f
: nonces of apps%n
: get nonce of subscription%t
: remote scry subpaths%u
: check if installed%w
: latest revision of path%x
: remote scry file%z:
hash of value at path
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
|public
and use Clay to directly synchronize desks. - Use the
/app/treaty
agent from%landscape
to discover and install agents.
Landscape (formerly Grid) is formally a Tlon product. It primarily consists of two agents (and associated marks, libraries, etc.):
/app/treaty
handles publishing and advertising application desks./app/docket
handles retrieving, validating, and installing application desks.
Together they query a remote %treaty
instance to ++install
a particular desk.
- Examine
+$alliance
in/sur/treaty
. - Examine
++publish:so
and++watch:tr
in/app/treaty
. - Examine
++install:ch
in/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-load
arm of the new agent with the state extracted from the old agent. If there is a crash in any+on-load
calls 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
++call
arm receives a%load
move with a noun of a core built by++ford:clay
.++mo-core
is a handle to++mo
because no++abet
is needed.++mo-load
installs agents pretty mechanically, by simply++skim
ming over the%live
agents.++mo-receive-core
checks whether the agent is running.- If it is, then
++ap
is invoked to update the agent:++ap-abed
++ap-reinstall
++on-save:ap-agent-core
++ap-install
is the install wrapper.++ap-upgrade-state
++on-init:ap-agent-core
++on-load:ap-agent-core
++ap-abet
++mo-clear-queue
flushes 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-idle
puts the agent to sleep if it's in the kill list (to be retired due todesk/bill
).
++mo-abet
finalizes 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-poke
wrapper arm work? - How does the
+dbug
generator work?
- In particular, how does the
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
$state
block 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.