In HASH, agents interact and change the world using an 'actor model', where each agent is an actor. This means that:
At the beginning of each time step, agents have an inbox of messages sent from other agents: a queue of requests, instructions, responses, and so on. In their behaviors, they check these messages, and in response can do one or both of:
This process happens in private: given the same inbox, starting state and external context, an agent will always end up with the same outbox and finishing state (unless there is randomness in its behaviors).
Predictability: how each agent behaves can easily be reasoned about from the messages it starts each time step with. You do not have to worry about race conditions, side effects, or locking access to resources while others attempt to access them. The agent can process its complete inbox for a time step in private, deciding what to do based on all the messages it has received.
Scalability: the actor model allows us to have millions of agents running on dozens of servers, enabling much bigger simulations than other approaches which need to have agents communicate in the middle of a time step. HASH has been designed from the ground up to enable you to model entire countries without having to worry about the underlying infrastructure.
“The big idea is 'messaging' … The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.”
Alan Kay
Because agents receive all the messages sent to them in the previous time step at the start of the next time step, you must take into account the "travel time" of messages:
Agent A will not know the result of the message it added in Step 1 until it receives the response in Step 3 (or even later if Agent B must communicate with a third agent before responding). Watch out for redundant messaging! You should also consider the order of behaviors when processing inbox messages and queueing outbox messages.
Watch out for redundant messaging. You can run into trouble with a naive message sending pattern where an agent sends messages until it receives a response.
// Potentially Bad
function behavior(state, behavior) {
if (!receivedResponse) {
sendStateChangeMessage();
}
}
This would send a message at timestep 1 (t) and timestep 2 (t+1), and if the message was prompting Agent B to make a change to its state, you might inadvertently apply an update twice.
You can avoid this in several ways, including by modifying Agent A's send behavior to only send every n timesteps, or modifying Agent B such that the effect is the same whether it receives one or many messages (e.g. an idempotent messaging pattern).
In each timestep all agents execute simultaneously. They receive the messages addressed to them, run their behaviors, and update their state. This enables fast, deterministic simulations - but you’ll want to keep this parallel execution model in mind when designing your simulations. Since each agent is executing at the same time, every timestep, you need to include logic within the agent that handles situations where the agent shouldn’t do anything, for example because it should wait on the response of another agent.
There are multiple ways to ensure agents only execute when they should - you could add a check at the beginning of a behavior to see if it hasn’t received a particular message, or use a state flag that is set at the end of the last timestep (e.g. state.waiting = true).
What if agents are in competition for a resource? Agent A and B might both add a message to Agent C in the time step 1 requesting the same item. Now Agent C must decide what to do in time step 2 when it processes those messages, and inform A and B of the result.
A process for handling conflicts must be defined in any modeling approach - in the actor model, the fact that messages must be considered and responded to on successive time steps mean that a complex dispute might take several steps of back-and-forth messages to resolve.
To streamline this, consider:
We are introducing a new feature to eliminate the need for manager agents and multiple back-and-forth messages to resolve conflicts. Contact us if you want to be one of the first to try this feature.
See Managing Resource Access for more.
By default, a 'step' in a simulation is simply the process of agents taking as input the messages received, state and context outputted from the previous step, and adjusting their internal state and producing messages as output for the next step. A step has no intrinsic relation to a particular unit of time.
For many simulations, you will want to have a sense of the passage of clock or calendar time. This might mean an assumption that a step corresponds to a unit of time. This can introduce difficulties when an action that should take a long time only requires one step to process, or when an action that should be near-instantaneous takes several message-passing steps to resolve.
There are strategies for aligning timescales discussed in detail in Designing for Different Timescales.
Another potential pitfall to be aware of is that behaviors run sequentially. So if in Behavior A you send a message and in Behavior B you check if you received a message and set receivedResponse = True, if the agents behavior array is:
["behaviorA", "behaviorB"]
a message would be sent before the agent checks if they've received a response.
Previous
Next