Nodes
Multi-agent systems are often distributed: their agents live on different devices, such as robots, servers or phones, that run and fail independently and can only talk over a network. JaKtA makes this structure explicit with nodes. A node is a part of the MAS that lives on its own: it hosts a set of agents, connects them to the external environment, and runs and stops independently of the other nodes. Agents on different nodes interact only through messages, as they would across a network, even when all the nodes run in the same process. Where the nodes actually run, and how messages travel between them, is left to a runner and a network, so the same MAS can run in a single process, in a simulation, or across real devices.
mas(NodeBuilders.baseNode()) {
node {
withAgents(helloWorldAgent) // agents defined elsewhere with agent { }
agent(BaseAgentID("inline")) { /* ... */ }
}
node { /* another node */ }
}.run(CoroutineNodeRunner(SharedMemoryNetwork()))
Nodes can also be defined on their own with node(NodeBuilders.baseNode()) { ... } and combined with
mas(...) { withNodes(nodeA, nodeB) }.
What a node gives its agents
- a body for each agent, its representation inside the node: see Embodiment;
- the perceptions published on the node: see Perception;
- the messages sent to them, from any node: see Communication;
- the skills wrapped around them with
context(...): see Acting and skills.
From a plan body or a skill, the node also manages its agents (see
Add and remove agents at runtime): node.addAgent(agentFactory) adds a new agent,
node.removeAgent(id) removes one, and node.terminateNode() stops the node and all its agents.
How nodes run
A MAS (BaseMasBuilder.kt) runs each of its nodes concurrently with the given NodeRunner, and run returns when
all nodes have terminated. CoroutineNodeRunner(SharedMemoryNetwork()) runs every agent on coroutines, with all the
nodes in the same process; runLocally() is a shorthand for it. The
Alchemist incarnation runs nodes as simulated devices instead, with simulated time.
Runners, networks and system events
CoroutineNodeRunner (CoroutineNodeRunner.kt) runs a node as follows:
- it subscribes the node to a
NodeNetworkand forwards the node's system events to it: agent additions and removals, messages, shutdown requests; - it receives system events from the network, lets the node handle them, and reacts to them: it starts a coroutine for every added agent, running its reasoning cycle, cancels removed agents, and stops the node on shutdown.
SharedMemoryNetwork (SharedMemoryNetwork.kt) is an in-process network that broadcasts every system event to
every subscribed node.
How events reach agents
The two kinds of external events reach agents differently (BaseNode.kt):
- Perceptions published with
node.publishEvent(...)are delivered directly to the node's own agents whose body passes the filter. They never leave the node. - Messages travel as system events through the network, so every node receives them and delivers them to its
local agents that pass the filter. This is how
sendToreaches agents on other nodes.
MessagingSkill.sendTo addresses the receiver through its body: the filter selects the agent whose body
maps back to the receiver's ID (Node.getAgentIDfromBody), which is why each agent needs a
distinct body.
Adding, removing and terminating
node.addAgent(factory)and the agents declared innode { }are posted as agent-addition system events. Each agent starts once its node's runner receives the event, so the change shows up innode.agentsshortly after.node.removeAgent(id)posts a removal event: the runner cancels the agent's reasoning loop. Since intentions only progress through the Step events handled by that loop, none of them runs any further.- An agent whose loop crashes with an unexpected exception is removed from its node.
node.terminateNode()posts a shutdown event for the node. When the runner receives it, it closes the node's subscription and cancels all the node's agents; the node'srunthen completes.