Jido.Plugin behaviour (Jido v2.0.0-rc.4)

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. After cmd/2 (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.*"]

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

  @impl Jido.Plugin
  def signal_routes(_ctx) do
    [
      {"chat.send", MyApp.Actions.SendMessage},
      {"chat.history", MyApp.Actions.ListHistory}
    ]
  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: []).
  • schedules - List of schedule tuples like {"*/5 * * * *", ActionModule} (default: []).

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.

Returns the signal routes for this plugin.

Returns bus subscriptions for this plugin.

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

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 act as global middleware.

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.

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.

subscriptions(config, context)

@callback subscriptions(config :: map(), context :: map()) :: [
  {module(), keyword() | map()}
]

Returns bus subscriptions for this plugin.

Called during AgentServer.init/1 to determine which bus adapters to subscribe to and with what options.

Parameters

  • config - Per-agent configuration for this plugin
  • context - Map with :agent_id, :agent_module

Returns

List of {adapter_module, opts} tuples. Each adapter's subscribe/2 will be called with the AgentServer pid.

Example

def subscriptions(_config, context) do
  [
    {Jido.Bus.Adapters.Local, topic: "events.*"},
    {Jido.Bus.Adapters.PubSub, pubsub: MyApp.PubSub, topic: context.agent_id}
  ]
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, or internal server state — only the agent struct returned to the caller. Transforms chain through all plugins in declaration order.

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

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