Skip to main content

lib/bb/jido/action/command.ex

# SPDX-FileCopyrightText: 2026 James Harton
#
# SPDX-License-Identifier: Apache-2.0

defmodule BB.Jido.Action.Command do
  @moduledoc """
  Jido action that executes a Beam Bots command.

  Bridges Jido's action system to BB's command infrastructure: starts the
  named command on the robot, awaits its result with `BB.Command.await/2`,
  and maps the outcome into the canonical bb_jido error taxonomy.

  ## Schema

  - `:robot` — the robot module (required).
  - `:command` — the command name as an atom (required).
  - `:goal` — the goal map passed to the command (default `%{}`).
  - `:timeout` — millisecond timeout for `BB.Command.await/2` (default
    `30_000`).

  ## Returns

  - `{:ok, %{command: ..., goal: ..., outcome: ..., robot: ...}}` on success.
  - `{:error, :safety_disarmed}` if the command exited because the robot was
    disarmed.
  - `{:error, {:command_failed, reason}}` for any other command failure or
    process termination.
  """

  use Jido.Action,
    name: "bb_command",
    description: "Execute a Beam Bots command",
    schema: [
      robot: [type: :atom, required: true, doc: "Robot module"],
      command: [type: :atom, required: true, doc: "Command name"],
      goal: [type: :map, default: %{}, doc: "Command goal map"],
      timeout: [
        type: :pos_integer,
        default: 30_000,
        doc: "Command timeout in milliseconds"
      ]
    ]

  alias BB.Jido.Telemetry

  @impl Jido.Action
  def run(%{robot: robot, command: command} = params, _context) do
    goal = Map.get(params, :goal, %{})
    timeout = Map.get(params, :timeout, 30_000)

    Telemetry.span(
      [:bb_jido, :action, :command],
      %{robot: robot, command: command},
      fn ->
        case apply(robot, command, [goal]) do
          {:ok, pid} ->
            await_command(pid, robot, command, goal, timeout)

          {:error, reason} ->
            {:error, {:command_failed, reason}}
        end
      end
    )
  end

  defp await_command(pid, robot, command, goal, timeout) do
    case BB.Command.await(pid, timeout) do
      {:ok, outcome} ->
        {:ok, build_result(robot, command, goal, outcome)}

      {:ok, outcome, _opts} ->
        {:ok, build_result(robot, command, goal, outcome)}

      {:error, :disarmed} ->
        {:error, :safety_disarmed}

      {:error, reason} ->
        {:error, {:command_failed, reason}}
    end
  end

  defp build_result(robot, command, goal, outcome) do
    %{
      robot: robot,
      command: command,
      goal: goal,
      outcome: outcome
    }
  end
end