This page traces what happens when you attach BB.Jido.Plugin.Robot to an agent and start the agent. Understanding the order lets you reason about where to put state, why child_spec/1 can call self(), and how the bridge stays scoped to the agent.

Where the agent lives

Application supervisor
 Jido (name: MyApp.Jido)             DynamicSupervisor
     Jido.AgentServer (id: "main")   started by Jido.start_agent/3
         BB.Jido.PubSubBridge        started by plugin child_spec/1

The Jido instance is a DynamicSupervisor. Jido.start_agent/3 calls DynamicSupervisor.start_child/2, which spawns a Jido.AgentServer process. That AgentServer then runs the plugin lifecycle below.

Phase 1: agent compile-time

When you write:

use Jido.Agent,
  name: "my_robot",
  plugins: [{BB.Jido.Plugin.Robot, %{robot: MyRobot}}]

…Jido validates the plugin list at compile time and stores plugin specs on the agent module. The robot module reference is captured in the spec as data; nothing is started yet.

Phase 2: Jido.start_agent/3

The AgentServer is spawned. Its init/1 does two things relevant to plugins:

  1. Calls each plugin's mount/2 to build the agent's initial state. This is pure — no processes, no side effects.
  2. Schedules handle_continue(:post_init, state) for child startup.

BB.Jido.Plugin.Robot.mount/2:

def mount(_agent, %{robot: robot}) do
  {:ok,
   %{robot: robot, safety_state: :unknown, last_joint_state: %{}}}
end

The map returned becomes agent.state.robot (because the plugin's state_key: :robot). If :robot is missing from config, mount/2 returns {:error, ...} and the agent fails to start.

Phase 3: handle_continue(:post_init, ...)

This is where children are started. Crucially, the work happens inside the AgentServer process — so self() here is the agent's pid.

Jido.AgentServer.start_plugin_children/1 walks the plugin specs and calls each plugin's child_spec/1. For BB.Jido.Plugin.Robot:

def child_spec(config) do
  agent_pid = self()        # ← captured at this moment
  # ...build PubSubBridge child spec with agent: agent_pid
end

This is the point: child_spec/1 is called from the agent process, so self() is the agent. We capture it once and pass it to the bridge's start_link/1 opts.

The returned spec is fed into Supervisor.child_spec(...)-style startup. The bridge is now a monitored child of the AgentServer: if either crashes, the supervision tree handles it.

Phase 4: bridge init/1

The bridge subscribes to its configured topics:

for topic <- topics do
  BB.PubSub.subscribe(robot, topic, message_types: message_types)
end

…and stashes the agent pid in its state. From now on:

BB.PubSub  [:bb, path, %BB.Message{}]  Bridge
                                              
                                               Jido.AgentServer.cast/2
                                              
                                          AgentServer

The bridge sees every matching delivery, turns it into a signal, and casts. The agent's router takes it from there.

Phase 5: signal routing

When the bridge casts a signal to the agent:

  1. Each plugin's handle_signal/2 pre-routing hook fires in declaration order. BB.Jido.Plugin.Robot.handle_signal/2 watches for bb.state.transition and updates its cached safety_state — that's why the cache stays current without a separate subscriber.
  2. The signal router matches the type against the plugin's signal_routes: and any other plugin's routes.
  3. The matched action's run/2 is invoked.
  4. Any returned directives (e.g. Emit) are dispatched.

What this means in practice

  • Don't start processes in mount/2. It's pure; failures there crash agent creation, not a restartable child.
  • Don't store the agent pid in plugin state. It's not needed — actions get the agent via context. The bridge needs it only because it lives in a separate process.
  • Don't restart the agent to pick up new bridge config. Stop and restart the agent (Jido.stop_agent/2 then Jido.start_agent/3). The bridge restarts as part of that.

See also