Skip to content

12 · Patterns, pitfalls & a builder's checklist

Build worlds · Chapter 12 of 12 · Path home

You can now build worlds. This closing chapter gives you three things to carry forward: the shapes worlds tend to take (so you can recognize yours), the mistakes that trip people up (so you can avoid them), and a checklist (so you never miss a piece).

Common world patterns

Most worlds are a variation on one of these. The cards below are the quick map; under them, each pattern gets a walked-through illustration, so you can recognize yours, see the one move that makes it work, and know when to reach for it.

  • Hub & relay


    One agent receives and fans out to everyone. Use for chat rooms, broadcasts, shared boards. Key move: a pure-data send to many. Example: chat.

  • Service (request / answer)


    Requesters ask, providers answer, matched by capability not name. Use when interchangeable workers should be discoverable at runtime. Key move: roles from assign_role inspecting profiles; engage. Example: info_extraction.

  • Teach & grade


    A leader streams material, an agent learns over time, the leader scores it. Use for training, certification, curricula. Key move: learn vs process, evaluate / compare_eval. Example: cat_library, animal_school.

  • Sense, decide, act


    Devices read the world, a controller applies rules, actuators respond. Use for automation, IoT, control loops, no AI required. Key move: a master that connects to devices and loops a decide action. Example: your greenhouse (Chapter 11).

  • Lead a group in rounds


    A master runs phased activities for many agents at once. Use for cohorts, tournaments, social learning. Key move: fan-out send, completion guards, callbacks. Example: class_incremental_learning, social_learning.

  • Game & competition


    Many participants, rules, anonymity, scoring. Use for evaluations, studies, multiplayer. Key move: hand-built behaviors and a scoring stage. Example: turing.

Worlds combine these freely: a teaching world is "lead a group" plus "teach & grade"; a marketplace is "service" plus "game". Here is each one walked through.

Hub & relay

The shape. One agent is a fixed point everyone connects to; whatever it receives, it pushes back out to all of them. Nobody talks to anybody directly, they all talk to the hub, and the hub talks to everybody.

Walked through (the chat world). The broadcaster role is a one-state behavior that self-loops on a relay action, kept alive by incoming requests rather than fired proactively (ready=False, the Chapter 4 idiom):

behav.set_role("broadcaster")
behav.add_state("ready", blocking=True)
behav.add_transit("ready", "ready", action="broadcast_message", args={}, ready=False)

Each user connects to it (connect_to_broadcaster), and when one speaks the broadcaster fans the message to everyone with a single send whose target is a list. The whole hub is one state and one self-loop.

Reach for it when there is a shared surface, a room, a board, a feed, that everyone should see the same view of. The key move is a pure-data send to many (Chapter 11's "commanding many devices" is the same fan-out). Start from chat.

Service (request / answer)

The shape. Some agents ask, others answer, and they are matched by what they can do, not by name. A requester does not care which provider serves it, only that the provider has the right capability.

Walked through (the info_extraction world). Matching by capability happens in assign_role, which inspects each joiner's declared streams instead of a hand-set label. An agent that publishes a private image stream becomes a user (it has something to extract from); one whose model takes images in and emits text out becomes an extractor; anything else fits no role. (New to proc_inputs / proc_outputs? They are an agent's fixed input/output slots, An agent's own streams explains the convention in one place.)

def assign_role(self, profile, is_world_master):
    # ...inspect the profile's declared streams / proc_inputs / proc_outputs...
    if offers_img_stream:
        return "user"
    if in_img and out_text:        # image in, text out -> it can extract
        return "extractor"
    return None                    # fits no role -> not admitted

Requesters and providers then find each other at runtime with the service_requester / service_provider behavior templates (Chapter 4) and pair up with engage.

Reach for it when workers are interchangeable and should be discoverable as they arrive, not wired by hand. The key move is roles derived from capability in assign_role plus engage. Start from info_extraction.

Teach & grade

The shape. A leader streams material; an agent on the other side learns over time rather than answering once; the leader then scores how well it learned.

Walked through (the animal_school world). The teacher's behavior is a template filled with wildcards that say how long to train, how long to test, and the pass mark, so one teacher.json teaches any student:

behav.add_wildcards({"<learn_steps>": 40, "<eval_steps>": 30, "<cmp_thres>": 0.65})

The student spends <learn_steps> ticks on learn (which adapts its model, unlike process, which just runs it), then <eval_steps> ticks being scored, and the teacher awards a badge when the comparison clears <cmp_thres>. The distinction that defines this pattern is learn vs process: only learn changes the student.

Reach for it when the point is improvement over time, training, certification, a curriculum, not a one-shot reply. Start from cat_library or animal_school, and see Chapter 8 for the full build.

Sense, decide, act

The shape. Input agents read the world, a controller applies rules, actuators respond. No AI is required, this is a control loop with peers.

Walked through (your greenhouse, Chapter 11). A master controller connects to its devices once, then loops a decide action that pulls the sensor and pushes a command to the valve:

b.add_state("ready", action="decide", args={"dry_below": 0.3}, blocking=True)
b.add_transit("init", "ready", action="connect_to_devices", args={})

The decide action reads the freshest reading (get_stream(...).get(...)) and sends an open/close command, returning to ready to do it again next tick.

Reach for it when the world is automation, IoT, or any rule-driven loop. The key move is a master that connects to devices then loops a decide. You already built this one in Chapter 11.

Lead a group in rounds

The shape. A master runs phased activity for many agents at once: gather them, do phase A for all, wait for all, then phase B. It is "sense, decide, act" scaled to a cohort and run in lockstep.

Walked through (the class_incremental_learning world). The master commands the whole class with one send whose target is the list of students, then waits for the phase to finish before the next one by gating the transition on the all_sent_completed built-in:

b.add_transit("commanding", "ready",
              action="all_sent_completed", args={"action_name": "set_valve"})

To collect replies as they trickle back, the mirror-image built-in received_some_asked_data calls one of your methods per incoming sample. Both idioms are spelled out in Chapter 11.

Reach for it when you run cohorts, tournaments, or social learning, anything that needs everyone in step. The key move is fan-out send + completion guards + per-sample callbacks. Start from class_incremental_learning or social_learning.

Game & competition

The shape. Many participants, explicit rules, often anonymity, and a scoring stage at the end. The leader is a game master enforcing the rules, not a relay.

Walked through (the turing world). It uses several leading roles (hotel_manager, floor_manager) and a guest that is shuffled through stages, hall, floor, room, so it never knows who it is paired with. The managers collect votes and apply consequences in dedicated states:

behav.add_transit("ready_for_news", "votes_processed", action="get_votes")
behav.add_transit("votes_processed", "init", action="send_violations")

The defining move is hand-built behaviors plus a scoring stage, here voting and violations, rather than reusable templates.

Reach for it when you are running an evaluation, a study, or a multiplayer game with rules and a result. Start from turing.

Pitfalls, and how to spot them

Because behaviors are explicit state machines, when something is wrong it usually shows up as an agent stuck in a state. That is good news: there is one obvious place to look. For each pitfall below, read the symptom to recognize it in your logs, the cause to understand why, and the fix to clear it.

The behavior never advances (an agent is stuck)

Symptom. An agent's msg for one state prints (or it goes quiet) and then nothing happens, tick after tick. The next state's msg never appears.

Cause. A transition's action keeps returning False, so the transition never fires. Remember Chapter 5: returning False is the wait. An action that is waiting on something that will never arrive, a stream that has no data yet, a partner that never connected, a human who never clicks, waits forever. A blocking state guarding that transition looks identical from the outside.

Fix. Open that one action and ask: what makes it return True? Then check that the thing actually happens. The most common case is a guarded read, if self.stdin.get(requested_by="x") is None: return False, where no one is writing to that stream, so add a timeout to the transition (Chapter 4) if "give up and move on" is acceptable, or fix the upstream agent so the data flows.

Agents connect, but no data flows

Symptom. Two agents engage successfully, the connection msg fires, but the reader's stdin.get(...) keeps returning None and the consumer is stuck (the pitfall above) even though the producer is clearly publishing.

Cause. Their stream types don't match. Two agents wire together only when one's proc_outputs are compatible with the other's proc_inputs. If the producer emits images and the consumer expects text, there is no compatible pair to connect, so no channel is opened, even though the agents themselves engaged.

Fix. Compare the StreamType on both sides, data type and shape, and make them compatible. The blunt escape hatch is data_type="all", which accepts any value (see Chapter 11's raw-JSON section): it removes the type negotiation entirely while you debug, and you can tighten back to a typed stream once data is moving.

A joiner is silently turned away

Symptom. An agent starts, runs join_world, seems to connect, but never takes part, no role behavior runs, and it does nothing in the world.

Cause. Your assign_role(profile, is_world_master) returned None for it. None means "fits no role", and the agent is not admitted. This is often intended, info_extraction returns None for anything that is neither a user nor an extractor, but it is invisible unless you look.

Fix. Log what assign_role decides for each profile. If a joiner you expected to admit is getting None, the profile it presented did not match your rule, usually a missing role_preference or a stream/proc_inputs shape your if did not anticipate (Chapter 3). Print the dynamic profile and compare it to your conditions.

Nobody leads the world

Symptom. Everyone joins as a plain participant; no master ever appears; the phases that depend on a leader never start.

Cause. You forgot world_masters_node_names on the host Node, so is_world_master is never True for anyone and assign_role's if is_world_master: branch never runs. No agent becomes the master.

Fix. List the node name(s) allowed to lead on the host node, exactly as in Chapter 11: Node(world, ..., world_masters_node_names=["Controller"]). The name must match the joining node's node_name. (Chapter 3.)

Build error: an action does not exist

Symptom. create_behav_files raises at build time, before anything runs, complaining that an action named in a transition is not found.

Cause. The behavior references an action that is not on the agent class it was built against. The state machine checks every action= name against the class you handed it, and a typo or the wrong class fails fast.

Fix. Build each behavior with the class that plays that role, so its custom actions are visible: HybridStateMachine(UserAgent(proc=None)), as in Chapter 4. Then check the action name in add_transit(action=...) matches a @action method on that class exactly (this catch-at-build-time is the whole point, Chapter 2).

A wildcard is empty at runtime

Symptom. A behavior runs until it hits a transition whose args contain a <placeholder>, then it fails or reads nothing, because the literal text <agent> is not a real peer or stream.

Cause. A <placeholder> from a behavior template (Chapter 4) was never filled in, so it reached the action as raw angle-bracket text instead of a resolved value.

Fix. Set every wildcard before the behavior needs it. On the HSM use set_wildcards({...}), add_wildcards({...}), or update_wildcard(key, value); on the agent use add_behav_wildcard("<stream_name>", "animal_stream"). The teaching worlds fill <agent> at runtime so one teacher.json serves any student (Chapter 4).

A builder's checklist

To go from an idea to a running world, you need exactly these. Nothing more.

The world itself

  • A World subclass with assign_role and create_behav_files, why: assign_role decides who becomes what, create_behav_files builds and saves each role's behavior; without both, no one is admitted and no one has anything to do.
  • One Agent subclass per role, each carrying that role's @action methods, why: the behavior is built against this class, so its actions must live here or the build fails (the "action does not exist" pitfall above).
  • A behavior per role, built against its class and saved as <role>.json, why: this is the state machine the world ships to whoever takes the role; it is the role's life (Chapter 4).
  • Any streams the world owns (shared data agents read), why: agents have no shared memory; data only exists on streams someone publishes.
  • (Optional) a stats.py for a dashboard and custom metrics, why: turns a running world into something you can watch and score (Chapter 10).

To run it

  • A run_w.py hosting the world, with world_masters_node_names if a role should lead, why: without it is_world_master is never True and nobody leads (the "nobody leads the world" pitfall above).
  • Join scripts for the agents (node.run(join_world="...")), why: a world is empty until peers join it; each agent needs its own entry point, with a role_preference if assign_role reads one.
  • Try it locally with run_synch.py, then go live with the same scripts, why: the synchronizer runs every peer on one machine so you can debug the whole society before anything is distributed (Chapter 2).

The design, before any of it

The five questions from Chapter 11: who takes part, what does each do, what data flows, who leads, what is each one's behavior. Answer those and the files almost write themselves.

You can build worlds

You went from "what even is a world" to designing, building, and running one yourself, with agents of any kind, learning and acting as peers, under rules you set. No pipeline, no glue code, no central script. That is the level UNaIVERSE works at, and it is now yours.