3 · Roles & the world master¶
Build worlds · Chapter 3 of 12 · Path home
The very first thing that happens when an agent joins your world is that it gets a role. The role decides everything that follows: which behavior the agent runs, what it is allowed to do, and how it fits in. This chapter is about how you hand out roles, and about the one role that can lead the others: the master.
The one method that matters
A world decides roles in assign_role(profile, is_world_master). It runs
once per joiner and returns a role name (a string). That string maps to a
behavior the world ships back. Everything else is detail.
Assigning a role¶
assign_role receives the joiner's profile and a flag, is_world_master,
telling you whether this node is allowed to be a master (more on that below). You
return a role name. You can decide it however you like; here are the three patterns
the examples use.
What a profile is
A profile is everything a node tells the world about itself, its business card. Every node has one, and it comes in two halves:
- Static (
profile.get_static_profile()): identity that rarely changes, thenode_id,node_name,node_type(is it an AI, a human, a sensor?), plus owner details likeorganizationandlocation. - Dynamic (
profile.get_dynamic_profile()): live state, what the node offers and where it sits. The keys you will actually read areproc_inputs/proc_outputs/streams(what it consumes, produces, and publishes) andconnections(its currentroleand who it is connected to), alongside machine stats like CPU and memory.
New to proc_inputs / proc_outputs? They are an agent's fixed input/output
slots, An agent's own streams explains
the whole convention in one place.
assign_role inspects this profile to decide a role, the capability pattern
below reads the dynamic streams. And it is not only for joining: you can read
any connected peer's profile at runtime through self.all_agents[peer_id],
which is how the chat broadcaster looks up a sender's node_name
(get_static_profile()["node_name"]) to prefix the message. The full reference,
every field in both halves, is in The node profile.
By master status (the chat world)¶
The simplest rule: the first master becomes one special role, everyone else becomes the ordinary role.
def assign_role(self, profile: NodeProfile, is_world_master: bool):
if is_world_master:
if len(self.world_masters) <= 1:
return "broadcaster"
else:
return "user"
else:
return "user"
Walk it through as the world sees it. When a node finishes its handshake, the
world calls assign_role once for that node, passing two things:
profile, the joiner'sNodeProfile, its business card. This rule does not even look at it: in a chatroom, who you are does not change what role you get.is_world_master, a boolean the framework computed before calling you, by checking the joiner's node name against theworld_masters_node_nameslist you set when hosting the world (see The world master below).Truemeans "this node is allowed to lead"; it does not mean it already is one.
The body then reads self.world_masters, the world's own live dictionary of the
master peers it has already admitted. The check len(self.world_masters) <= 1
asks "is this the first master in?", at the moment assign_role runs for the
very first eligible node, that node is already counted, so the count is 1. The
first eligible node therefore returns "broadcaster"; a second node that
is also on the master list gets demoted to "user" (a chatroom needs exactly
one broadcaster, not two). Every non-master node returns "user" outright.
The return value is a role name string, and that string is the whole output:
the world looks up the matching behavior file (broadcaster.json or user.json,
built in create_behav_files) and ships it back to the joiner, which starts
running it immediately. That is why this world chose the rule it did: the
broadcaster is the single agent that relays every message to everyone, so it must
be the one trusted, named node; all the talkers, humans and AIs alike, are
interchangeable users.
By capability (the info_extraction world)¶
A more powerful idea: decide the role from what the joiner can do, read from
its declared input/output streams. The
info_extraction world inspects each joiner and assigns:
userif it offers a private image stream to be processed;extractorif its processor takes images in and produces text or labels out;- no role (it returns
None, the joiner is turned away) otherwise.
def assign_role(self, profile: NodeProfile, is_world_master: bool):
dynamic_profile = profile.get_dynamic_profile()
offers_img_stream = False
# If it offers an environmental (image) stream, then it is a "user"
if 'streams' in dynamic_profile and dynamic_profile['streams'] is not None:
for environmental_stream in dynamic_profile['streams']:
props = DataProps.from_dict(environmental_stream)
if (props.get_group() != "processor_in" and
props.is_img() and not props.is_public()):
offers_img_stream = True
break
if offers_img_stream:
return "user"
else:
out_text = False
in_img = False
for proc_input in (dynamic_profile['proc_inputs'] or []):
props = DataProps.from_dict(proc_input)
if (props.is_img() or props.is_all()) and not props.is_public():
in_img = True
break
for proc_output in (dynamic_profile['proc_outputs'] or []):
props = DataProps.from_dict(proc_output)
if not props.is_public() and (props.is_text() or props.is_all() or
(props.is_tensor() and props.has_tensor_labels())):
out_text = True
break
if in_img and out_text:
return "extractor"
return None # No role: not a fit for this world
Now the rule earns its profile argument. Read it as three questions asked in
order:
- Does it offer images? The first block reads
profile.get_dynamic_profile()['streams'], the live list of streams this node publishes, and wraps each one inDataPropsto ask three things at once: it is an image (is_img()), it is not a processor input (get_group() != "processor_in", i.e. a real environmental feed, not the node's own model inlet), and it is private to the world (not is_public()). A node that publishes such a stream is auser, it brings the images the world exists to process. - Does its processor consume images and produce text? If it offers no
image stream, the world looks instead at the joiner's processor: its declared
proc_inputs(what the model takes in) andproc_outputs(what it returns).in_imgis true when an input accepts images (or anything,is_all());out_textis true when an output is text, anything, or a labelled tensor. A node that does both is anextractor, it can turn auser's images into the textual descriptions this world wants. - Neither? The method falls through and returns
None. ANonereturn is the world's way of saying "you don't fit here", the joiner is turned away rather than given a role.
This is the deeper lesson: roles are not hard-coded to agent names. The world
never checks node_name; it checks shape. Any agent whose processor fits, a
classifier, a captioner, a detector, becomes an extractor without the world
knowing its name in advance, and is_world_master is irrelevant here because this
society is organised by capability, not by who leads.
By the joiner's own preference (the social_learning world)¶
Sometimes the joiner simply says what it wants to be, and the world combines
that with master status. social_learning does both at once:
def assign_role(self, profile: NodeProfile, is_world_master: bool):
if is_world_master:
if len(self.world_masters) <= 1:
return "teacher"
else:
return "student"
else:
if 'tmp_role_preference' in profile.get_dynamic_profile():
role_preference = profile.get_dynamic_profile()['tmp_role_preference']
if role_preference == "student":
return "student"
elif role_preference == "student_isolated":
return "student_isolated"
else:
return "student"
else:
return "student"
This rule has two halves, chosen by is_world_master:
- The master half is the same idea as the chat world: the first eligible node
(
len(self.world_masters) <= 1) becomes theteacherthat leads the lessons; any extra eligible node falls back tostudent. There is only one teacher. - The agent half reads the joiner's stated preference. A node declares it
when it joins,
node.run(join_world="...", role_preference="student_isolated"), and the framework parks that string in the dynamic profile undertmp_role_preference.assign_rolereads it back and honours it only for the values it recognises:"student"and"student_isolated". Anything else, or no preference at all, defaults to plainstudent.
So the difference between a regular student and a student_isolated is the
joiner's own choice, surfaced through tmp_role_preference. The world stays in
charge, it whitelists which preferences it will grant and can ignore the rest,
but this is the simplest way to let two otherwise identical nodes pick a lane.
Note how each of the three branches returns a different role string, and each
string has its own behavior file (teacher.json, student.json,
student_isolated.json) built in create_behav_files.
The built-in roles¶
Every world understands three roles out of the box. Internally they are small integer bitmasks, but you work with their names.
| Name | Bits | Meaning |
|---|---|---|
public_agent |
0 |
On the public network, not in a world |
world_agent |
1 |
An ordinary member of a world |
world_master |
3 |
A member that may lead the world |
The bits are simple: bit 0 means "in a world", bit 1 means "is a master",
and bits 2 and up are reserved for your custom roles. The default
assign_role, if you do not override it, makes the first master a world_master
and everyone else a world_agent.
The world master¶
One member can act as the master: the agent that leads the session. Among
everything that joins, who is allowed to be a master is decided when you host the
world, with world_masters_node_names on the Node:
node = Node(world, node_name="ChatRoom", hidden=True, clock_delta=1. / 20.,
world_masters_node_names=["Broadcaster"])
Here, only the node named Broadcaster may become a master. When it joins, your
assign_role is called with is_world_master=True; for everyone else it is
False. That is the whole mechanism.
What a master can do
A master is an ordinary agent with extra authority by convention, not by special enforcement. In practice a master is the one that:
- changes other agents' roles at runtime (
set_role, below); - awards badges (
add_badge, Chapter 10); - leads multi-step activities, teaching rounds, exams, scoring, which is the whole of Chapter 9.
A world with no master is just a shared space. A world with a master becomes a running activity.
Who may join, and who may lead¶
assign_role decides what a joiner becomes, but the host can also decide who is
allowed to connect at all, and who is eligible to lead. Unlike the master's
authority (which is by convention), these are enforced controls, set on the
host Node when you start the world:
world_masters_node_names/world_masters_node_ids- Who may become a master. Names are resolved to node IDs at startup. Only these
nodes ever reach
assign_rolewithis_world_master=True. allowed_node_ids- An explicit allowlist of node IDs permitted to connect. When set, it takes precedence over the allowlist the world would otherwise load from its online profile: nobody outside the list gets in.
only_certified_agents- When
True, accept only agents whose profile carries thecertifiedflag.
node = Node(world, node_name="ChatRoom",
world_masters_node_names=["Broadcaster"], # who may lead
allowed_node_ids=[...], # who may connect at all
only_certified_agents=True) # accept only certified agents
Keep the two layers straight: the Node controls decide who reaches
assign_role in the first place, and assign_role then decides what role
they get (returning None refuses one to a joiner that did get through the gate).
Custom roles¶
You are not limited to the built-ins. Any string you return from assign_role
is a role. broadcaster, user, teacher, student, extractor: these are
all custom roles. Each one needs two things, which you saw in
Chapter 2:
- A behavior built for it in
create_behav_filesand saved as<role>.json(soteachertoteacher.json). - An agent class it is built against, carrying that role's custom actions.
When the world assigns a custom role, it ships the matching <role>.json to the
joiner, exactly as it does for the built-ins.
Changing a role at runtime¶
Roles are not fixed for life. A master can promote or reassign an agent while
the world runs, with World.set_role:
# role is an integer bitmask, e.g. self.ROLE_STR_TO_BITS["teacher"]
await self.set_role(peer_id, role) # ship the agent a new role + its behavior
Read it as the counterpart of assign_role: assign_role picks the role at join
time, set_role changes it afterwards. A few details matter:
- It takes a peer ID and an integer role, not a name. Custom roles are stored
as bitmasks, so you convert the name first with
self.ROLE_STR_TO_BITS["teacher"]. - It keeps the agent's base bits. Internally it does
new_role = (cur_role & 3) | new_role_without_base_int: the bottom two bits (the "in a world" / "is a master" bits from the table above) are preserved, and only the custom part is swapped. A promoted student stays a member of the world; only its job changes. - It pushes the new behavior. On success it sends the agent a
ROLE_SUGGESTIONmessage carrying both the new role and the behavior file registered for it, which the agent starts running immediately. If that message can't be delivered, the world purges (disconnects) the agent rather than leaving it in a half-changed state.
This is how the teaching worlds promote a successful student to teacher (you will see it in Chapter 8).
From the other side, an ordinary agent can suggest a role change with the
built-in suggest_role_to_world(agent, role), here role is the role name
string. The suggestion is sent to the world master, which decides whether to
apply it (typically by calling set_role). The world is always the final
authority. (Both actions are in the
built-in actions reference.)
What just happened¶
You can now answer, for any joiner, "what role does it get, and why?", whether by master status, by capability, or by your own logic. You met the master as the member that may lead, and saw that roles can change while the world runs.
Next: a role is only as good as its behavior. Let's build those.
Where next¶
-
Build the state machine each role runs.
-
set_role,suggest_role_to_world, and every other built-in. -
Roles & the world master (concept)
The reference view: the bitmask,
assign_role,set_role, and the master.