2. The Agent Core

In this lesson we'll look at the basic type and structure of a Gall agent.

A Gall agent is a door with exactly ten arms. Each arm is responsible for handling certain kinds of events that Gall feeds in to the agent. A door is just a core with a sample - it's made with the barcab rune (|_) instead of the usual barcen rune (|%).

The ten arms

We'll discuss each of the arms in detail later. For now, here's a quick summary. The arms of an agent can be be roughly grouped by purpose:

State management

These arms are primarily for initializing and upgrading an agent.

  • on-init: Handles starting an agent for the first time.
  • on-save: Handles exporting an agent's state - typically as part of the upgrade process but also when suspending, uninstalling and debugging.
  • on-load: Handles loading a previously exported agent state - typically as part of the upgrade process but also when resuming or reinstalling an agent.

Request handlers

These arms handle requests initiated by outside entities, e.g. other agents, HTTP requests from the front-end, etc.

  • on-poke: Handles one-off requests, actions, etc.
  • on-watch: Handles subscription requests from other entities.
  • on-leave: Handles unsubscribe notifications from other, previously subscribed entities.

Response handlers

These two arms handle responses to requests our agent previously initiated.

  • on-agent: Handles request acknowledgements and subscription updates from other agents.
  • on-arvo: Handles responses from vanes.

Scry handler

  • on-peek: Handles local read-only requests.

Failure handler

  • on-fail: Handles certain kinds of crash reports from Gall.

Bowl

The sample of a Gall agent door is always a bowl:gall. Every time an event triggers the agent, Gall populates the bowl with things like the current date-time, fresh entropy, subscription information, which ship the request came from, etc, so that all the arms of the agent have access to that data. For the exact structure and contents of the bowl, have a read through its entry in the Gall vane types documentation.

One important thing to note is that the bowl is only repopulated when there's a new Arvo event. If a local agent or web client were to send multiple messages to your agent at the same time, these would all arrive in the same event. This means if your agent depended on a unique date-time or entropy to process each message, you could run into problems if your agent doesn't account for this possibility.

State

If you've worked through Hoon School, you may recall that a core is a cell of [battery payload]. The battery is the core itself compiled to Nock, and the payload is the subject which it operates on.

For an agent, the payload will at least contain the bowl, the usual Hoon and zuse standard library functions, and the state of the agent. For example, if your agent were for an address book app, it might keep a map of ships to address book entries. It might add entries, delete entries, and modify entries. This address book map would be part of the state stored in the payload.

Transition function

If you recall from the prologue, the whole Arvo operating system works on the basis of a simple transition function (event, oldState) -> (effects, newState). Gall agents also function the same way. Eight of an agent's ten arms produce the same thing, a cell of:

  • Head: A list of effects called cards (which we'll discuss later).
  • Tail: A new agent core, possibly with a modified payload.

It goes something like this:

  1. An event is routed to Gall.
  2. Gall calls the appropriate arm of the agent, depending on the kind of event.
  3. That arm processes the event, returning a list cards to be sent off, and the agent core itself with a modified state in the payload.
  4. Gall sends the cards off and saves the modified agent core.
  5. Rinse and repeat.

Virtualization

When a crash occurs in the kernel, the system usually aborts the computation and discards the event as though it never happened. Gall on the other hand virtualizes all its agents, so this doesn't happen. Instead, when a crash occurs in an agent, Gall intercepts the crash and takes appropriate action depending on the kind of event that caused it. For example, if a poke from another ship caused a crash in the on-poke arm, Gall will respond to the poke with a "nack", a negative acknowledgement, telling the original ship the poke was rejected.

What this means is that you can intentionally design your agent to crash in cases it can't handle. For example, if a poke comes in with an unexpected mark, it crashes. If a permission check fails, it crashes. This is quite different to most programs written in procedural languages, which must handle all exceptions to avoid crashing.

Example

Here's about the simplest valid Gall agent:

|_ =bowl:gall
++ on-init `..on-init
++ on-save !>(~)
++ on-load |=(vase `..on-init)
++ on-poke |=(cage !!)
++ on-watch |=(path !!)
++ on-leave |=(path `..on-init)
++ on-peek |=(path ~)
++ on-agent |=([wire sign:agent:gall] !!)
++ on-arvo |=([wire sign-arvo] !!)
++ on-fail |=([term tang] `..on-init)
--

This is just a dummy agent that does absolutely nothing - it has no state and rejects all messages by crashing. Typically we'd cast this to an agent:gall, but in this instance we won't so it's easier to examine its structure in the dojo. We'll get to what each of the arms do later. For now, we'll just consider a few particular points.

Firstly, note its structure - it's a door (created with |_) with a sample of bowl:gall and the ten arms described earlier. The =bowl:gall syntax simply means bowl=bowl:gall ($= irregular syntax).

Secondly, you'll notice some of the arms return:

`..on-init

A backtick at the beginning is an irregular syntax meaning "prepend with null", so for example, in the dojo:

> `50
[~ 50]

The next part has ..on-init, which means "the subject of the on-init arm". The subject of the on-init arm is our whole agent. In the transition function section we mentioned that most arms return a list of effects called cards and a new agent core. Since an empty list is ~, we've created a cell that fits that description.

Let's examine our agent. In the dojo of a fake ship, mount the %base desk with |mount %base. On the Unix side, navigate to /path/to/fake/ship/base, and save the above agent in the /app directory as skeleton.hoon. Back in the dojo, commit the file to the desk with |commit %base.

For the moment we won't install our %skeleton agent. Instead, we'll use the %build-file thread to build it and save it in the dojo's subject so we can have a look. Run the following in the dojo:

> =skeleton -build-file %/app/skeleton/hoon

Now, let's have a look:

> skeleton
< 10.fxw
[ bowl
[ [our=@p src=@p dap=@tas]
[ wex=nlr([p=[wire=/ ship=@p term=@tas] q=[acked=?(%.y %.n) path=/]])
sup=nlr([p=it(/) q=[p=@p q=/]])
]
act=@ud
eny=@uvJ
now=@da
byk=[p=@p q=@tas r=?([%da p=@da] [%tas p=@tas] [%ud p=@ud])]
]
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]
>

The dojo pretty-prints cores with a format of number-of-arms.hash. You can see the head of skeleton is 10.fxw - that's the battery of the core, our 10-arm agent. If we try printing the head of skeleton we'll see it's a whole lot of compiled Nock:

> -.skeleton
[ [ 11
[ 1.953.460.339
1
[ 7.368.801
7.957.707.045.546.060.659
1.852.796.776
0
]
[7 15]
7
34
]
...(truncated for brevity)...

The battery's not too important, it's not something we'd ever touch in practice. Instead, let's have a look at the core's payload by printing the tail of skeleton. We'll see its head is the bowl:gall sample we specified, and then the tail is just all the usual standard library functions:

> +.skeleton
[ bowl
[ [our=~zod src=~zod dap=%$]
[wex={} sup={}]
act=0
eny=0v0
now=~2000.1.1
byk=[p=~zod q=%$ r=[%ud p=0]]
]
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]

Currently skeleton has no state, but of course in practice you'd want to store some actual data. We'll add foo=42 as our state with the =+ rune at the beginning of our agent:

=+ foo=42
|_ =bowl:gall
++ on-init `..on-init
++ on-save !>(~)
++ on-load |=(vase `..on-init)
++ on-poke |=(cage !!)
++ on-watch |=(path !!)
++ on-leave |=(path `..on-init)
++ on-peek |=(path ~)
++ on-agent |=([wire sign:agent:gall] !!)
++ on-arvo |=([wire sign-arvo] !!)
++ on-fail |=([term tang] `..on-init)
--

Save the modified skeleton.hoon in /app on the %base desk like before, and run |commit %base again in the dojo. Then, rebuild it with the same %build-file command as before:

> =skeleton -build-file %/app/skeleton/hoon

If we again examine our agent core's payload by looking at the tail of skeleton, we'll see foo=42 is now included:

> +.skeleton
[ bowl
[ [our=~zod src=~zod dap=%$]
[wex={} sup={}]
act=0
eny=0v0
now=~2000.1.1
byk=[p=~zod q=%$ r=[%ud p=0]]
]
foo=42
<17.zbp 33.wxp 14.dyd 53.vlb 77.wir 232.wfe 51.qbt 123.zao 46.hgz 1.pnw %140>
]

Summary

  • A Gall agent is a door with exactly ten specific arms and a sample of bowl:gall.
  • Each of the ten arms handle different kinds of events - Gall calls the appropriate arm for the kind event it receives.
  • The ten arms fit roughly into five categories:
    • State management.
    • Request handlers.
    • Response handlers.
    • Scry handler.
    • Failure handler.
  • The state of an agent—the data it's storing—lives in the core's payload.
  • Most arms produce a list of effects called cards, and a new agent core with a modified state in its payload.

Exercises

Edit this page on GitHub

Last modified October 8, 2023