# SPDX-FileCopyrightText: 2026 James Harton # # SPDX-License-Identifier: Apache-2.0 defmodule BB.Jido.Plugin.Robot do @moduledoc """ Jido v2 plugin that gives an agent the ability to control a Beam Bots robot. Provides: - The standard robot-control actions: `BB.Jido.Action.Command`, `BB.Jido.Action.Reactor`, `BB.Jido.Action.WaitForState`, and `BB.Jido.Action.GetJointState`. - Default signal routes for the canonical `bb.*` signal types. - Plugin-owned state: the configured `:robot` module and a cached `:safety_state` (updated automatically when `bb.state.transition` signals arrive). - A supervised `BB.Jido.PubSubBridge` mounted under the agent process that forwards BB PubSub events to the agent as Jido signals. ## Configuration Plugin config (passed via `{BB.Jido.Plugin.Robot, %{...}}` when attaching to an agent): - `:robot` — robot module (required). - `:topics` — list of `BB.PubSub` paths to bridge (default `[[:state_machine]]`). - `:message_types` — payload modules to filter on at subscribe time (default `[]`, meaning no filter). - `:throttle_ms` — optional per-signal-type throttle in milliseconds. ## Example defmodule MyRobot.Agent do use Jido.Agent, name: "my_robot", plugins: [{BB.Jido.Plugin.Robot, %{robot: MyRobot}}] end """ # Plugin name is `bb` so that Jido v2's plugin-route prefixing yields # the canonical user-facing signal types: a route declared here as # `command.execute` ends up as `bb.command.execute` after prefixing. use Jido.Plugin, name: "bb", state_key: :robot, actions: [ BB.Jido.Action.Command, BB.Jido.Action.GetJointState, BB.Jido.Action.Reactor, BB.Jido.Action.WaitForState ], signal_routes: [ {"command.execute", BB.Jido.Action.Command}, {"reactor.run", BB.Jido.Action.Reactor}, {"state.wait", BB.Jido.Action.WaitForState} ] alias BB.Jido.PubSubBridge @impl Jido.Plugin def mount(_agent, config) do case Map.fetch(config, :robot) do {:ok, robot} when is_atom(robot) -> {:ok, %{ robot: robot, safety_state: :unknown, last_joint_state: %{} }} _ -> {:error, "BB.Jido.Plugin.Robot requires :robot module in config"} end end @impl Jido.Plugin def child_spec(config) do agent_pid = self() robot = Map.fetch!(config, :robot) topics = Map.get(config, :topics, default_topics()) message_types = Map.get(config, :message_types, []) throttle_ms = Map.get(config, :throttle_ms) bridge_opts = [ robot: robot, agent: agent_pid, topics: topics, message_types: message_types ] |> maybe_put(:throttle_ms, throttle_ms) %{ id: {__MODULE__, :pub_sub_bridge, robot}, start: {PubSubBridge, :start_link, [bridge_opts]}, restart: :transient, type: :worker } end @impl Jido.Plugin def handle_signal(%Jido.Signal{type: "bb.state.transition"} = signal, context) do case signal.data do %{message: %BB.Message{payload: %BB.StateMachine.Transition{to: to}}} -> update_safety_state(context, to) {:ok, :continue} _ -> {:ok, :continue} end end def handle_signal(_signal, _context), do: {:ok, :continue} defp update_safety_state(%{agent_ref: ref}, state) when not is_nil(ref) do send(ref, {:bb_jido, :safety_state, state}) :ok end defp update_safety_state(_context, _state), do: :ok defp default_topics, do: [[:state_machine]] defp maybe_put(opts, _key, nil), do: opts defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value) end