Jido.Plugin behaviour (Jido v2.3.0)

Copy Markdown View Source

A Plugin is a composable capability that can be attached to an agent.

Plugins encapsulate:

  • A set of actions the agent can perform
  • State schema for plugin-specific data (nested under state_key)
  • Configuration schema for per-agent customization
  • Signal routing rules
  • Optional lifecycle hooks and child processes

Lifecycle

  1. Compile-time: Plugin is declared in agent's plugins: option
  2. Agent.new/1: mount/2 is called to initialize plugin state (pure)
  3. AgentServer.init/1: child_spec/1 processes are started and monitored
  4. Signal processing: handle_signal/2 runs before routing, can override or abort
  5. Prepare signal: prepare_signal/2 can verify/rewrite the effective signal and contribute runtime context
  6. Prepare action: prepare_action/3 can authorize the resolved action using runtime context
  7. Before signal emit dispatch: prepare_emit/2 can rewrite outbound emitted signals or dispatch
  8. After cmd/3 (call path): transform_result/3 wraps call results

Example Plugin

defmodule MyApp.ChatPlugin do
  use Jido.Plugin,
    name: "chat",
    state_key: :chat,
    actions: [MyApp.Actions.SendMessage, MyApp.Actions.ListHistory],
    schema: Zoi.object(%{
      messages: Zoi.list(Zoi.any()) |> Zoi.default([]),
      model: Zoi.string() |> Zoi.default("gpt-4")
    }),
    signal_patterns: ["chat.*"],
    signal_routes: [
      {"chat.send", MyApp.Actions.SendMessage},
      {"chat.history", MyApp.Actions.ListHistory}
    ]

  @impl Jido.Plugin
  def mount(agent, config) do
    # Custom initialization beyond schema defaults
    {:ok, %{initialized_at: DateTime.utc_now()}}
  end
end

Using Plugins

defmodule MyAgent do
  use Jido.Agent,
    name: "my_agent",
    plugins: [
      MyApp.ChatPlugin,
      {MyApp.DatabasePlugin, %{pool_size: 5}}
    ]
end

Configuration Options

  • name - Required. The plugin name (letters, numbers, underscores).
  • state_key - Required. Atom key for plugin state in agent.
  • actions - Required. List of action modules.
  • description - Optional description.
  • category - Optional category.
  • vsn - Optional version string.
  • schema - Optional Zoi schema for plugin state.
  • config_schema - Optional Zoi schema for per-agent config.
  • signal_patterns - List of signal pattern strings (default: []).
  • tags - List of tag strings (default: []).
  • capabilities - List of atoms describing what the plugin provides (default: []).
  • requires - List of requirements like {:config, :token}, {:app, :req}, {:plugin, :http} (default: []).
  • signal_routes - List of signal route tuples like {"post", ActionModule} (default: []).
  • subscriptions - List of sensor subscription tuples like {SensorModule, config} or {tag, SensorModule, config} (default: []).
  • schedules - List of schedule tuples like {"*/5 * * * *", ActionModule} (default: []).

For static routes and subscriptions, prefer the compile-time signal_routes: and subscriptions: options in use Jido.Plugin. Use the signal_routes/1 and subscriptions/2 callbacks only for dynamic generation based on runtime config.

Summary

Callbacks

Returns child specification(s) for supervised processes.

Pre-routing hook called before signal routing in AgentServer.

Called when the plugin is mounted to an agent during new/1.

Called during checkpoint to determine how this plugin's state should be persisted.

Called during restore to rehydrate externalized plugin state.

Returns the plugin specification with optional per-agent configuration.

Post-routing hook called after routing and before action execution.

Pre-emit hook called before an emitted signal is dispatched.

Pre-routing hook called after handle_signal/2 rewrites and before routing.

Returns the signal routes for this plugin.

Returns a list of sensors to be started for this plugin.

Caller view transform for the agent returned from AgentServer.call/3.

Types

sensor_subscription()

@type sensor_subscription() ::
  {module(), keyword() | map()} | {term(), module(), keyword() | map()}

Callbacks

child_spec(config)

@callback child_spec(config :: map()) ::
  nil | Supervisor.child_spec() | [Supervisor.child_spec()]

Returns child specification(s) for supervised processes.

Called during AgentServer.init/1. Returned processes are started and monitored. If any crash, AgentServer receives exit signals.

Parameters

  • config - Per-agent configuration for this plugin

Returns

  • nil - No child processes
  • Supervisor.child_spec() - Single child
  • [Supervisor.child_spec()] - Multiple children

Example

def child_spec(config) do
  %{
    id: {__MODULE__, :worker},
    start: {MyWorker, :start_link, [config]}
  }
end

handle_signal(signal, context)

@callback handle_signal(signal :: term(), context :: map()) ::
  {:ok, term()} | {:ok, {:override, term()}} | {:error, term()}

Pre-routing hook called before signal routing in AgentServer.

Can inspect, log, transform, or override which action runs for a signal. Hooks execute in plugin declaration order. The first {:override, ...} short-circuits; the first {:error, ...} aborts. Plugins with non-empty signal_patterns only receive signals matching those patterns; plugins with empty patterns receive all inbound signals for this phase.

This callback remains broad for backwards compatibility and route override. Prefer prepare_signal/2 for identity, encryption, canonicalization, and runtime context extraction.

Parameters

  • signal - The incoming Jido.Signal struct (may be modified by earlier plugins)
  • context - Map with :agent, :agent_module, :plugin, :plugin_spec, :plugin_instance, :config

Returns

  • {:ok, nil} or {:ok, :continue} - Continue to normal routing
  • {:ok, {:continue, %Signal{}}} - Rewrite the signal and continue routing
  • {:ok, {:override, action_spec}} - Bypass router, use this action instead
  • {:ok, {:override, action_spec, %Signal{}}} - Bypass router with rewritten signal
  • {:error, reason} - Abort signal processing with error

Example

def handle_signal(signal, _context) do
  if signal.type == "admin.override" do
    {:ok, {:override, MyApp.AdminAction}}
  else
    {:ok, :continue}
  end
end

mount(agent, config)

@callback mount(agent :: term(), config :: map()) :: {:ok, map() | nil} | {:error, term()}

Called when the plugin is mounted to an agent during new/1.

Use this to initialize plugin-specific state beyond schema defaults. This is a pure function - no side effects allowed.

Parameters

  • agent - The agent struct (with state from previously mounted plugins)
  • config - Per-agent configuration for this plugin

Returns

  • {:ok, plugin_state} - Map to merge into plugin's state slice
  • {:ok, nil} - No additional state (schema defaults only)
  • {:error, reason} - Raises during agent creation

Example

def mount(_agent, config) do
  {:ok, %{initialized_at: DateTime.utc_now(), api_key: config[:api_key]}}
end

on_checkpoint(plugin_state, context)

@callback on_checkpoint(plugin_state :: term(), context :: map()) ::
  {:externalize, key :: atom(), pointer :: term()} | :keep | :drop

Called during checkpoint to determine how this plugin's state should be persisted.

Plugins can declare one of three strategies for their state slice:

  • :keep — Include in checkpoint state as-is (default)
  • :drop — Exclude from checkpoint (transient/ephemeral state)
  • {:externalize, key, pointer} — Strip from checkpoint state and store a pointer separately. The pointer is a lightweight reference (e.g., %{id, rev}) that can be used to rehydrate the full state on restore.

Parameters

  • plugin_state - The plugin's current state slice (may be nil)
  • context - Map with checkpoint context (e.g., :config)

Returns

  • :keep — Include plugin state in checkpoint (default)
  • :drop — Exclude from checkpoint
  • {:externalize, key, pointer} — Store pointer under key in checkpoint

Example

def on_checkpoint(%Thread{} = thread, _ctx) do
  {:externalize, :thread, %{id: thread.id, rev: thread.rev}}
end

def on_checkpoint(nil, _ctx), do: :keep

on_restore(pointer, context)

@callback on_restore(pointer :: term(), context :: map()) ::
  {:ok, term()} | {:error, term()}

Called during restore to rehydrate externalized plugin state.

When a plugin's on_checkpoint/2 returns {:externalize, key, pointer}, the pointer is stored in the checkpoint. During restore, on_restore/2 is called with that pointer to allow the plugin to reconstruct its state.

For plugins that require IO to restore (e.g., loading a thread from storage), returning {:ok, nil} signals that the state will be rehydrated by the persistence layer (e.g., Jido.Persist).

Parameters

  • pointer - The pointer stored during checkpoint (from on_checkpoint/2)
  • context - Map with restore context (e.g., :config)

Returns

  • {:ok, restored_state} — The restored plugin state
  • {:ok, nil} — State will be rehydrated externally (e.g., by Persist)
  • {:error, reason} — Restore failed

plugin_spec(config)

@callback plugin_spec(config :: map()) :: Jido.Plugin.Spec.t()

Returns the plugin specification with optional per-agent configuration.

This is the primary interface for getting plugin metadata and configuration.

prepare_action(signal, action_arg, context)

@callback prepare_action(signal :: term(), action_arg :: term(), context :: map()) ::
  {:ok, map()} | {:error, term()}

Post-routing hook called after routing and before action execution.

Plugins can authorize the resolved action using the prepared signal and accumulated runtime context. This hook cannot rewrite the signal or action; it can only contribute additional runtime context or fail closed.

Parameters

  • signal - The prepared Jido.Signal struct after prepare_signal/2
  • action_arg - The resolved action argument that will be passed to the agent command phase
  • context - Map with :agent, :agent_module, :plugin, :plugin_spec, :plugin_instance, :config, :runtime_context

Returns

  • {:ok, runtime_context_delta} - Continue with additional runtime context.
  • {:error, reason} - Abort signal processing with error.

prepare_emit(signal, context)

@callback prepare_emit(signal :: term(), context :: map()) ::
  {:ok, term()} | {:ok, term(), term()} | {:error, term()}

Pre-emit hook called before an emitted signal is dispatched.

This hook is intended for outbound signal signing, trace enrichment, and other signal-level transformations that must happen after an action returns an emit directive but before runtime dispatch. The context includes :input_signal, :runtime_context, :directive, :dispatch, plugin metadata, agent metadata, :jido_instance, and :partition.

Returns

  • {:ok, signal} - Continue with the prepared signal and existing dispatch.
  • {:ok, signal, dispatch} - Continue with the prepared signal and rewritten dispatch.
  • {:error, reason} - Abort the emit through the configured error policy.

prepare_signal(signal, context)

@callback prepare_signal(signal :: term(), context :: map()) ::
  {:ok, term(), map()} | {:error, term()}

Pre-routing hook called after handle_signal/2 rewrites and before routing.

Plugins can verify, decrypt, canonicalize, or rewrite the final effective signal and contribute runtime context. Returned context is merged into the context given to routed actions and later plugin phases. This is the preferred inbound hook for identity and encrypted communication extensions because it cannot override routing and has an explicit runtime context contract. Plugins may not provide reserved runtime keys such as :state, :signal, :agent, :agent_server_pid, :input_signal, :directive, or :dispatch; duplicate context keys fail closed.

Parameters

  • signal - The effective Jido.Signal struct after handle_signal/2 hooks have run.
  • context - Map with :agent, :agent_module, :plugin, :plugin_spec, :plugin_instance, :config, :runtime_context

Returns

  • {:ok, signal, runtime_context_delta} - Continue with possibly rewritten signal and runtime context.
  • {:error, reason} - Abort signal processing with error.

signal_routes(config)

@callback signal_routes(config :: map()) :: term()

Returns the signal routes for this plugin.

The signal routes determine how signals are routed to handlers. Prefer compile-time signal_routes: in use Jido.Plugin for static routes, and implement this callback only for dynamic route generation.

subscriptions(config, context)

@callback subscriptions(config :: map(), context :: map()) :: [sensor_subscription()]

Returns a list of sensors to be started for this plugin.

Called during AgentServer.init/1 to start and monitor plugin-specific sensors. These sensors are managed and monitored by the agent runtime and can emit signals back to it.

Parameters

  • config - Per-agent configuration for this plugin
  • context - A map containing:
    • :agent_id - The unique identifier of the agent
    • :agent_ref - A reference (PID or via-tuple) to the AgentServer
    • :agent_module - The module of the agent
    • :plugin_spec - The specification of the current plugin
    • :jido_instance - The Jido instance name

Returns

List of {sensor_module, sensor_opts} tuples or {tag, sensor_module, sensor_opts} tuples. Each sensor will be started under a Jido.Sensor.Runtime. Use the tagged form when a plugin starts multiple instances of the same sensor module.

Example

def subscriptions(_config, context) do
  [
    {MyApp.Sensors.MarketData, %{symbol: "AAPL", interval: 1000}},
    {Jido.Sensors.Heartbeat, %{interval: 5000, agent_ref: context.agent_ref}}
  ]
end

transform_result(action, result, context)

@callback transform_result(
  action :: module() | String.t(),
  result :: term(),
  context :: map()
) :: term()

Caller view transform for the agent returned from AgentServer.call/3.

Called after signal processing on the synchronous call path only. Does not affect cast/2, handle_info, routing, authorization, dispatch, or internal server state — only the agent struct returned to the caller. Transforms chain through all plugins in declaration order, so this callback is display/return shaping and is not a security hook.

Parameters

  • action - The resolved action module that was executed, or the signal type string when no single module can be determined
  • result - The agent struct to transform
  • context - Map with :agent, :agent_module, :plugin, :plugin_spec, :plugin_instance, :config, :runtime_context

Returns

The transformed agent struct (or original if no transformation needed).

Example

def transform_result(_action, agent, _context) do
  # Add metadata to returned agent
  new_state = Map.put(agent.state, :last_call_at, DateTime.utc_now())
  %{agent | state: new_state}
end