Skip to content

Agents

An Agent is the fundamental building block of UNaIVERSE. Whether you connect a PyTorch vision model, a language model, a plain Python function, or a human at a browser, UNaIVERSE treats every participant as an Agent with equal standing on the network. Each Agent owns typed input/output channels, carries a behavior engine that decides what to do next, and talks to other Agents through the same P2P protocol, regardless of what runs under the hood.

In one sentence

An Agent = a processor (the brain) + typed streams (its senses and voice) + a behavior engine (HSM) that decides what it does next.

Those three parts are all there is to an Agent. Keep this picture in mind for the rest of the page:

flowchart LR
    subgraph AGENT[An Agent]
        direction TB
        IN[/typed input streams<br/>its senses/]
        P["processor<br/>the brain: a torch.nn.Module<br/>or None"]
        OUT[/typed output streams<br/>its voice/]
        HSM{{"behavior engine (HSM)<br/>decides what to do next"}}
        IN --> P --> OUT
        HSM -.drives.-> P
    end
    PEER([other agents and humans]) <-->|peer to peer| IN
    OUT <-->|peer to peer| PEER

The processor turns input into output, the streams carry typed data in and out, and the behavior engine decides when the processor runs and who it talks to. A human agent has the same shape, the person simply plays the role of the processor.

The processor

The processor is the Agent's brain, the thing that turns inputs into outputs. It must be a torch.nn.Module (PyTorch's standard container, an object with a forward() method), or None for a coordinator with no brain. What that forward() does is entirely up to you:

What forward() does Example
Trivial logic a 3-line module that adds two numbers
Database lookup queries a vector store, no neural net
Web / LLM service calls an external (e.g. vLLM/OpenAI-compatible) API
A neural network torch.nn.Sequential(...) or any model
A human HumanModule, the person is the processor

Prefer a module over a bare function

The processor is any callable, but a bare lambda is a poor choice: UNaIVERSE can't infer its proc_inputs / proc_outputs (you'd have to declare them by hand) and can't checkpoint it. Wrap your logic in a small torch.nn.Module instead. UNaIVERSE ships ready-made ones (the model zoo) for convenience, but the brain is always yours.

This mirrors the perceive, think, act cycle of any real agent:

  1. Perceive, read data arriving on the input DataStream.
  2. Think / process, run forward() on that data.
  3. Act, write the result to the output DataStream.
flowchart LR
    PER[Perceive<br/>read the input stream] --> THINK[Think<br/>run forward on the data]
    THINK --> ACT[Act<br/>write to the output stream]
    ACT -.next tick.-> PER

One turn of this loop is what the process action does, and the behavior engine decides how often it runs. A learning agent adds a fourth beat, a backward pass, which is exactly what learn does instead of process.

Inputs and outputs

You describe what your processor expects and produces with a list of StreamType descriptors. UNaIVERSE uses them to validate data and match compatible streams between Agents at connection time.

from unaiverse.streams.dataprops import StreamType

proc_inputs  = [StreamType(data_type="tensor", tensor_shape=(None, 3, 224, 224))]
proc_outputs = [StreamType(data_type="tensor", tensor_shape=(None, 1000))]

None in a shape dimension means that dimension is dynamic (e.g. a variable batch size). See Data streams for the full type system.

Where proc_input_0 / proc_output_0 come from

Each position in these lists becomes a stream with a fixed, canonical name: proc_inputs[0] becomes proc_input_0, proc_outputs[0] becomes proc_output_0, and so on. Those are the names you'll address from actions (self.stdin, self.stdout) and from send(...). The convention and the std* proxies are explained once, in full, in An agent's own streams, and every world example links back to it.

Creating an Agent

from unaiverse.agent import Agent
from unaiverse.streams.dataprops import StreamType

agent = Agent(
    proc=my_model,
    proc_inputs=[StreamType(data_type="tensor", tensor_shape=(None, 3, None, None))],
    proc_outputs=[StreamType(data_type="text")],
)

The constructor's main parameters:

proc · torch.nn.Module | Callable | None · required
The processing module, any torch.nn.Module implementing forward() or any Callable. Pass None to create a processor-free relay or coordinator agent.
proc_inputs · list[StreamType] | None
What the processor expects as input. If None, UNaIVERSE attempts to infer the input types.
proc_outputs · list[StreamType] | None
What the processor produces. If None, UNaIVERSE attempts to infer it.
proc_opts · dict | None
Options for the processor. Pass an optimizer and losses list here when the agent needs to learn (run backward passes).
world_folder · str | None
Path to the world folder. Set this only when creating a World subclass, regular agents leave it None.
policy_filter · callable | None
Optional function overriding the HSM's action-selection policy while inside a World. Receives the policy's selection plus all candidate actions; returns the final choice.

Processor-free agents

Pass proc=None to create an Agent with no processor. These act as relay nodes or coordinators, they forward messages, manage state, and lead other agents without running any inference of their own. Worlds are built this way.

Lifecycle actions

Once an Agent runs inside a Node, you control it through built-in actions, call them from an HSM transition or directly from Python.

  • send()


    Send an interaction request (an action + optional data) to one or more target agents.

  • process()


    Run one inference step: read from stdin, call forward(), write to stdout.

  • learn()


    Run one learning step: inference followed by a backward pass using the configured optimizer and loss.

  • find_agents()


    Search the local list of known peers for agents matching a given role.

  • connect_by_role()


    Find and open a P2P connection to agents whose profiles match a role.

  • disconnected()


    React to a peer disconnecting, the counterpart of connected().

Beyond these built-ins, you can add your own actions by subclassing Agent and decorating a method with @action, this is how world roles carry role-specific behavior. See Custom actions.

Saving and loading

Checkpoint an Agent's full state to resume a run later or transfer weights between sessions.

agent.save("checkpoint")   # write weights (.pt) + agent state (.pkl)
agent.load("checkpoint")   # restore from the same directory

Under the hood, save() writes two files:

  • <node_name>.pt, PyTorch model weights and optimizer state (only if the processor changed since the last save).
  • <node_name>.pkl, the serialized agent state (streams, stats, HSM state, etc.).

Human agents vs. AI agents

UNaIVERSE is built on the principle that humans and AI models are indistinguishable at the network layer. The browser platform at unaiverse.io runs the same Python agent code right in the browser, giving human participants the exact same capabilities as any Python-based agent.

Feature Human agent AI agent
Processor Human brain (implicit) Callable with forward()
DataStreams Same Same
Hybrid state machine Same Same
P2P network layers Same Same
Role assignment Same Same
Saving / loading Same Same

When a human agent receives input through a DataStream, the data is shown on-screen, the person responds, and that response flows back through the same stream infrastructure used by any model, no special casing. See Human agents.

Roles

Worlds assign roles to Agents when they connect. The built-in role constants are defined on AgentBasics:

Constant Value Meaning
ROLE_PUBLIC 0 Operating on the public network (lone-wolf mode)
ROLE_WORLD_AGENT 1 Standard participant in a World
ROLE_WORLD_MASTER 3 First agent to connect; the World's coordinator

A role name is simply a label that maps to a behavior JSON file in the World's folder, and you can define custom roles beyond these three. See Behaviors.

Where next