Built-in actions¶
Every UNaIVERSE agent comes with a set of built-in actions for free. You reference them by name from a behavior transition (or call them in your own custom actions). This page is the canonical reference: every built-in, with an example, and how it makes agents communicate. Whenever the docs mention a built-in action, they link here.
The action contract (applies to all of them)
An action is an async method that returns True (it succeeded, the
state machine advances) or False (retry on the next tick).
Many actions take an interaction argument the framework injects; you usually
leave it at its default. To write your own, see
Creating actions.
How a request actually works¶
This is the most important idea on the page, and the one that makes UNaIVERSE different from a remote procedure call. When agent A sends an action to agent B, it does not "call a method on B". It makes a request that B's behavior may choose to honor.
Concretely, three things have to line up:
- A sends.
A.send(action_name="X", target=B, streams=[...])creates an interaction and ships it to B. - B must be willing. B runs
Xonly if its behavior, in the state it is currently in, has a transition that acceptsX. That transition is usually a request-driven self-loop, written withready=False(it does not fire on its own; an incoming request fires it). If B is in the wrong state, or has no such transition, the request simply waits until B is ready, or times out. - The result flows back. When B runs
X, it reads the streams A bound (into itsstdin), does the work, and writes the result to itsstdout, which is wired back to A. A then learns the request completed (and can run acallback).
sequenceDiagram
participant A as Agent A (requester)
participant B as Agent B (target)
A->>B: send(action_name="X", streams=[...])
Note over B: in a state whose behavior<br/>accepts "X" (a ready=False transition)?
B->>B: run X (read A's data, act, write result)
B-->>A: completion (+ result on the bound stream)
Note over A: wait_completion / a guard / a callback
The thing to internalize: the effect of a request depends on the target's state
and behavior, not only on the request. Sending an agent a learn request does
nothing unless that agent is in a state ready to learn. An agent only ever does
what its own behavior allows, even when others ask.
Two kinds of send, two kinds of effect
- With an
action_name: the target runs that action (if its behavior accepts it). - Without an
action_name(a pure data push viadata_samples): the target just stores the data; no transition fires on its side.
Communication scenarios¶
The four shapes nearly every world is made of. Switch the tabs to see each, with both sides of the exchange.
A client asks a worker to process some data, and waits for the answer.
The worker sits ready and accepts process requests (the ready=False
self-loop makes it respond rather than act on its own):
# worker behavior
behav.add_state("ready", blocking=True)
behav.add_transit("ready", "ready", action="process", args={}, ready=False)
The client sends one request and blocks until it completes:
# client behavior
behav.add_transit("ask", "got_answer", action="send",
args={"action_name": "process", "target": "<worker>",
"streams": {"stdin": ["<client>:question"]},
"wait_completion": True})
The worker reads the question from stdin, runs its model, writes the answer
to stdout; the answer travels back and the client moves on. Had the worker
been in another state with no process transition, the request would have
waited until it returned to ready.
Before a back-and-forth, agents form a pair, so neither hard-codes the other.
# A: find a partner by role, then offer to engage
behav.add_transit("searching", "engaged", action="connect_by_role", args={"role": "student"})
behav.add_transit("engaged", "ready", action="send_engage", args={})
# B: accept an engagement from an agent playing the "teacher" role
behav.add_transit("waiting", "teacher_engaged", action="engage", args={"acceptable_role": "teacher"})
Now A and B are a stable pair: A can send learn / process and B's
teacher_engaged state has the transitions to honor them. disengage ends it.
A single send addresses a whole group; each follower reacts on its own,
and the leader waits for all.
followers = list(self.world_agents) # the peers you lead
await self.send(action_name="learn", target=followers,
streams={"stdin": ["<lesson>"], "stdtar": ["<lesson>"]}, num_steps=50)
# the leader then waits on a guard: all_sent_completed(action_name="learn")
The effect on each follower is independent. The leader does not loop over
followers; it sends once and waits with a completion guard, with a timeout so
one slow follower cannot stall the rest.
One agent reaches many at once without direct links (a price feed, a sensor reading, a best student's labels). That is pub/sub.
await self.subscribe(stream_owners=["<publisher>"], stream_props=["readings"])
# or a coordinator subscribes others on their behalf:
await self.send_subscribe(agent="<follower>", stream_hashes=["<publisher>:readings"])
graph LR
PUB[Publisher] -->|writes a stream| T[(stream)]
T --> S1[Subscriber 1]
T --> S2[Subscriber 2]
T --> S3[Subscriber N]
Whatever the publisher writes, every subscriber receives, with no per-recipient send. The publisher need not know who is listening.
The action catalogue¶
Each tab is a family of actions. Every action has an example, shown either as a
behavior transition (add_transit(... action="x" ...)) or a direct call
(await self.x(...)).
Find and link to peers by role, not by hard-coded names.
graph LR
A[Agent] -->|connect_by_role| F[matching peers]
A -->|find_agents| K[known peers, no connect]
P[peer leaves] -.disconnected.-> A
connect_by_role(role, filter_fcn=None) finds peers whose role matches and
connects to them. Example: add_transit("searching", "connected", action="connect_by_role", args={"role": "broadcaster"}). filter_fcn names a method that skips unwanted candidates.
find_agents(role, engage=False, handshake_completed=False) searches the
already-known peers, without connecting. Example: await self.find_agents("student") to gather a cohort. engage=True also offers engagement.
connect_to(peer_id) opens a connection to one peer by id. Example:
await self.connect_to(self._broadcaster_peer_id).
connected(agent=None, handshake_completed=False) is a guard that succeeds
once the peer is connected. Example: add_transit("waiting_handshake", "ready", action="connected", args={"handshake_completed": True}).
disconnected(agent=None, handshake_completed=False) succeeds once a peer is
gone. Example (teleport home): add_transit("ready", "init", action="disconnected", args={"delay": 5.0}).
disconnect(agent) drops one peer. Example: await self.disconnect(peer_id).
disconnect_by_role(role, disengage_too=False) drops every peer with a role.
Example: await self.disconnect_by_role("student", disengage_too=True).
Form a working pair before exchanging tasks.
sequenceDiagram
participant A as Agent A
participant B as Agent B
A->>B: send_engage()
B->>B: engage(acceptable_role) then paired
Note over A,B: exchange learn / process / data
A->>B: send_disengage()
B->>B: disengage() then free
send_engage() offers engagement to found agents. Example:
add_transit("connected", "offered", action="send_engage").
engage(acceptable_role=None, sender_role=None, interaction=None) accepts an
incoming engagement. Example: add_transit("waiting", "teacher_engaged", action="engage", args={"acceptable_role": "teacher"}).
set_engaged_partner(agent, clear_found=True) declares who you are working with.
Example: await self.set_engaged_partner(best_student).
send_disengage(send_disconnection_too=False) tells your partner it is over.
Example: await self.send_disengage().
disengage(disconnect_too=False, interaction=None) handles an incoming
disengage. Example: add_transit("teacher_engaged", "init", action="disengage").
disengage_all() clears all engagements locally. Example:
add_state("ready", action="disengage_all").
Ask others to act (send), or run your own processor (process / learn).
graph LR
IN[stdin] --> P["forward()"] --> OUT[stdout]
TAR[stdtar] -.learn: backward pass.-> P
send(action_name=None, target=None, streams=None, data_samples=None, num_steps=-1, timeout=-1., callback=None, copy_sys=False, wait_completion=False, volatile=False, ...)
dispatches an interaction to one or more targets. Named-action
example: await self.send(action_name="broadcast_message", target=[bc], streams=[my_hash], copy_sys=True). Data-push example: await self.send(target=others, data_samples={"proc_output_0": text}, num_steps=1). Full walkthrough: How interactions travel.
process(interaction=None) runs one inference step: read stdin, call
forward(), write stdout. Example (respond to requests): add_transit("ready", "ready", action="process", args={}, ready=False), this is what runs on you when another agent sends a process request.
learn(interaction=None) runs one learning step: process, then a backward
pass against stdtar. Example: add_transit("teacher_engaged", "finished_learning", action="learn"), train on what a teacher bound.
nop(message=None) does nothing (a placeholder to move states). Example:
add_transit("message_sent", "ready", action="nop").
show(interaction=None) logs a completed interaction. Example:
await self.show(interaction) while debugging.
Pub/sub streaming, and snapshotting data into a replayable stream.
subscribe(stream_owners=None, stream_props=None, unsubscribe=False, interaction=None)
receives whatever is published to matching streams. Example: await self.subscribe(stream_owners=["<best>"], stream_props=["labels"]).
send_subscribe(agent=None, stream_hashes=None, unsubscribe=False) asks a
remote agent to subscribe. Example: await self.send_subscribe(agent="<follower>", stream_hashes=["<best>:labels"]).
record(interaction=None, record_uuid=...) snapshots incoming samples into a
new owned stream. Example: add_transit("init", "recorded", action="record", args={"streams": ["<world>:cats"], "num_steps": 998}).
A "playlist" of streams an agent steps through, used by teaching worlds.
graph LR
SP[set_pref_streams] --> L1[lesson 1] --> L2[lesson 2] --> L3[lesson 3]
L3 -.check_pref_stream: last? .-> EXAM[exam]
set_pref_streams(net_hashes, repeat=1) sets the playlist. Example:
action="set_pref_streams", args={"net_hashes": ["<agent>:l1", "<agent>:l2"], "repeat": 4}.
first_pref_stream() · next_pref_stream() move to the first / next stream.
Example: the loop advances with action="next_pref_stream" after each lesson.
check_pref_stream(what="last") branches on playlist position. Example:
add_transit("change_lecture", "exam_time", action="check_pref_stream", args={"what": "last"}).
Score an agent's output, used by a master to grade.
graph LR
EV[evaluate gives an error metric] --> CMP{compare_eval}
CMP -->|metric within threshold| GOOD[pass]
CMP -->|otherwise| BAD[fail]
evaluate(stream_hash, how, steps=100, re_offset=False, agents_to_evaluate=None)
computes an error metric over a stream. Example: action="evaluate", args={"stream_hash": "<agent>:exam", "how": "mse", "steps": 1000}. re_offset=True re-aligns when sample tags are unreliable.
compare_eval(cmp, thres, good_if_true=True) turns the last evaluate into a
pass/fail branch. Example: add_transit("eval_time", "good", action="compare_eval", args={"cmp": "<=", "thres": 0.2}).
Guards that report whether a phase is done, so a requester can wait for one or many targets.
graph LR
SEND[send to N targets] --> W{guard}
W -->|not yet| W
W -->|all done| NEXT[advance]
all_sent_completed(action_name=None) is true once every interaction you sent
has completed. Example: add_transit("lesson_in_progress", "exam", action="all_sent_completed", args={"action_name": "learn"}).
all_asked_finished() is true once every agent you asked has reported done.
Example: action="all_asked_finished" after a mixed fan-out.
all_engagements_completed() is true once every found agent has been engaged.
Example: action="all_engagements_completed" before teaching the cohort.
agents_are_waiting() is true while found agents are still registering.
Example: add_transit("connected", "connected", action="agents_are_waiting").
received_some_asked_data(processing_fcn=None, data_type=None) is true when
asked-for data arrives, and runs processing_fcn on each item. Example: action="received_some_asked_data", args={"processing_fcn": "collect_predictions"}.
How an agent reports up to the world; the world decides what to record.
graph LR
AG[Agent / evaluator] -->|suggest_badges_to_world| W[World]
AG -->|suggest_role_to_world| W
suggest_badges_to_world(agent=None, score=-1.0, badge_type="completed", badge_description=None)
suggests a badge. Example: action="suggest_badges_to_world", args={"badge_description": "Completed the Animal School"}. badge_type is completed / attended / intermediate / pro; score is in [0, 1].
suggest_role_to_world(agent, role) suggests a role change. Example:
action="suggest_role_to_world", args={"agent": "<valid_cmp>", "role": "teacher"}, promote a student who passed.
Want to add your own?
These built-ins cover discovery, communication, learning, and scoring. When you need something specific, a domain rule, a device command, a custom aggregation, you write a custom action. The world-builder chapter Creating actions shows how, with examples well beyond AI (a robot arm, a sensor, a rule).