In the last lesson we looked at the most basic aspects of a Gall agent's structure. Before we get into the different agent arms in detail, there's some boilerplate to cover that makes life easier when writing Gall agents.
Useful libraries
There are a couple of libraries that you'll very likely use in every agent you write. These are default-agent
and dbug
. In brief, default-agent
provides simple default behaviours for each agent arm, and dbug
lets you inspect the state and bowl of an agent from the dojo, for debugging purposes. Every example agent we look at from here on out will make use of both libraries.
Let's look at each in more detail:
default-agent
The default-agent
library contains a basic agent with sane default behaviours for each arm. In some cases it just crashes and prints an error message to the terminal, and in others it succeeds but does nothing. It has two primary uses:
- For any agent arms you don't need, you can just have them call the matching function in
default-agent
, rather than having to manually handle events on those arms. - A common pattern in an agent is to switch on the input of an arm with wutlus (
?+
) runes or maybe wutcol (?:
) runes. For any unexpected input, you can just pass it to the relevant arm ofdefault-agent
rather than handling it manually.
The default-agent
library lives in /lib/default-agent/hoon
of the %base
desk, and you would typically include a copy in any new desk you created. It's imported at the beginning of an agent with the faslus (/+
) rune.
The library is a wet gate which takes two arguments: agent
and help
. The first is your agent core itself, and the second is a ?
. If help
is %.y
(equivalently, %&
), it will crash in all cases. If help
is %.n
(equivalently, %|
), it will use its defaults. You would almost always have help
as %.n
.
The wet gate returns an agent:gall
door with a sample of bowl:gall
- a typical agent core. Usually you would define an alias for it in a virtual arm (explained below) so it's simple to call.
dbug
The dbug
library lets you inspect the state and bowl
of your agent from the dojo. It includes an agent:dbug
function which wraps your whole agent:gall
door, adding its extra debugging functionality while transparently passing events to your agent for handling like usual.
To use it, you just import dbug
with a faslus (/+
) rune at the beginning, then add the following line directly before the door of your agent:
%- agent:dbug
With that done, you can poke your agent with the +dbug
generator from the dojo and it will pretty-print its state, like:
> :your-agent +dbug
The generator also has a few useful optional arguments:
%bowl
: Print the agent's bowl.[%state 'some code']
: Evaluate some code with the agent's state as its subject and print the result. The most common case is[%state %some-face]
, which will print the contents of the wing with the given face.[%incoming ...]
: Print details of the matching incoming subscription, one of:[%incoming %ship ~some-ship]
[%incoming %path /part/of/path]
[%incoming %wire /part/of/wire]
[%outgoing ...]
: Print details of the matching outgoing subscription, one of:[%outgoing %ship ~some-ship]
[%outgoing %path /part/of/path]
[%outgoing %wire /part/of/wire]
[outgoing %term %agent-name]
By default it will retrieve your agent's state by using its on-save
arm, but if your app implements a scry endpoint with a path of /x/dbug/state
, it will use that instead.
We haven't yet covered some of the concepts described here, so don't worry if you don't fully understand dbug
's functionality - you can refer back here later.
Virtual arms
An agent core must have exactly ten arms. However, there's a special kind of "virtual arm" that can be added without actually increasing the core's arm count, since it really just adds code to the other arms in the core. A virtual arm is created with the lustar (+*
) rune, and its purpose is to define deferred expressions. It takes a list of pairs of names and Hoon expressions. When compiled, the deferred expressions defined in the virtual arm are implicitly inserted at the beginning of every other arm of the core, so they all have access to them. Each time a name in a +*
is called, the associated Hoon is evaluated in its place, similar to lazy evaluation except it is re-evaluated whenever needed. See the tistar reference for more information on deferred expressions.
A virtual arm in an agent often looks something like this:
+* this .def ~(. (default-agent this %.n) bowl)
this
and def
are the deferred expressions, and next to each one is the Hoon expression it evaluates whenever called. Notice that unlike most things that take n arguments, a virtual arm is not terminated with a ==
. You can define as many aliases as you like. The two in this example are conventional ones you'd use in most agents you write. Their purposes are:
this .
Rather than having to return ..on-init
like we did in the last lesson, instead our arms can just refer to this
whenever modifying or returning the agent core.
def ~(. (default-agent this %.n) bowl)
This sets up the default-agent
library we described above, so you can easily call its arms like on-poke:def
, on-agent:def
, etc.
Additional cores
While Gall expects a single 10-arm agent core, it's possible to include additional cores by composing them into the subject of the agent core itself. The contents of these cores will then be available to arms of the agent core.
Usually to compose cores in this way, you'd have to do something like insert tisgar (=>
) runes in between them. However, Clay's build system implicitly composes everything in a file by wrapping it in a tissig (=~
) expression, which means you can just butt separate cores up against one another and they'll all still get composed.
You can add as many extra cores as you'd like before the agent core, but typically you'd just add one containing type definitions for the agent's state, as well as any other useful structures. We'll look at the state in more detail in the next lesson.
Example
Here's the /app/skeleton.hoon
dummy agent from the previous lesson, modified with the concepts discussed here:
Click to expand
/+ default-agent, dbug|%+$ card card:agent:gall--%- agent:dbug^- agent:gall|_ =bowl:gall+* this .def ~(. (default-agent this %.n) bowl)++ on-init^- (quip card _this)`this++ on-save on-save:def++ on-load on-load:def++ on-poke on-poke:def++ on-watch on-watch:def++ on-leave on-leave:def++ on-peek on-peek:def++ on-agent on-agent:def++ on-arvo on-arvo:def++ on-fail on-fail:def--
The first line uses the faslus (/+
) Ford rune to import /lib/default-agent.hoon
and /lib/dbug.hoon
, building them and loading them into the subject of our agent so they're available for use. You can read more about Ford runes in the Fas section of the rune documentation.
Next, we've added an extra core. Notice how it's not explicitly composed, since the build system will do that for us. In this case we've just added a single card
arm, which makes it simpler to reference the card:agent:gall
type.
After that core, we call agent:dbug
with our whole agent core as its argument. This allows us to use the dbug
features described earlier.
Inside our agent door, we've added an extra virtual arm and defined a couple deferred expressions:
+* this .def ~(. (default-agent this %.n) bowl)
In most of the arms, you see we've been able to replace the dummy code with simple calls to the corresponding arms of default-agent
, which we set up as a deferred expression named def
in the virtual arm. We've also replaced the old ..on-init
with our deferred expression named this
in the on-init
arm as an example - it makes things a bit simpler.
You can save the code above in /app/skeleton.hoon
of your %base
desk like before and |commit %base
in the dojo. Additionally, you can start the agent so we can try out dbug
. To start it, run the following in the dojo:
> |rein %base [& %skeleton]
For details of using the |rein
generator, see the Dojo Tools↗ documentation.
Now our agent should be running, so let's try out dbug
. In the dojo, let's try poking our agent with the +dbug
generator:
> ~> :skeleton +dbug>=
It just printed out ~
. Our dummy skeleton
agent doesn't have any state defined, so it's printing out null as a result. Let's try printing the bowl
instead:
> [ [our=~zod src=~zod dap=%skeleton sap=/gall/dojo][wex=~ sup=~ sky=~]act=5eny0v209.tg795.bc2e8.uja0d.11eq9.qp3b3.mlttd.gmf09.q7ro3.6unfh.16jiu.m9lh9.6jlt8.4f847.f0qfh.up08t.3h4l2.qm39h.r3qdd.k1r11.bja8lnow=~2024.5.9..13.28.24..e20ebyk=[p=~zod q=%base r=[%da p=~2024.5.9..12.02.22..f99b]]]> :skeleton +dbug %bowl>=
We'll use dbug
more throughout the guide, but hopefully you should now have an idea of its basic usage.
Summary
The key takeaways are:
- Libraries are imported with
/+
. default-agent
is a library that provides default behaviors for Gall agent arms.dbug
is a library that lets you inspect the state andbowl
of an agent from the dojo, with the+dbug
generator.- Convenient deferred expressions for Hoon expressions can be defined in a virtual arm with the lustar (
+*
) rune. this
is a conventional deferred expression name for the agent core itself.def
is a conventional deferred expression name for accessing arms in thedefault-agent
library.- Extra cores can be composed into the subject of the agent core. The composition is done implicitly by the build system. Typically we'd include one extra core that defines types for our agent's state and maybe other useful types as well.
Exercises
- Run through the example yourself on a fake ship if you've not done so already.
- Have a read through the Ford rune documentation for details about importing libraries, structures and other things.
- Try the
+dbug
generator out on some other agents, like:settings +dbug
,:contacts +dbug
, etc, and try some of its options described above. - Have a quick look over the source of the
default-agent
library, located at/lib/default-agent.hoon
in the%base
desk. We've not yet covered what the different arms do but it's still useful to get a general idea, and you'll likely want to refer back to it later.