Jido.Agent behaviour (Jido v2.0.0-rc.1)

View Source

An Agent is an immutable data structure that holds state and can be updated via commands. This module provides a minimal, purely functional API:

  • new/1 - Create a new agent
  • set/2 - Update state directly
  • validate/2 - Validate agent state against schema
  • cmd/2 - Execute actions: (agent, action) -> {agent, directives}

Core Pattern

The fundamental operation is cmd/2:

{agent, directives} = MyAgent.cmd(agent, MyAction)
{agent, directives} = MyAgent.cmd(agent, {MyAction, %{value: 42}})
{agent, directives} = MyAgent.cmd(agent, [Action1, Action2])

Key invariants:

  • The returned agent is always complete — no "apply directives" step needed
  • directives are external effects only — they never modify agent state
  • cmd/2 is a pure function — given same inputs, always same outputs

Action Formats

cmd/2 accepts actions in these forms:

  • MyAction - Action module with no params
  • {MyAction, %{param: value}} - Action with params
  • %Instruction{} - Full instruction struct
  • [...] - List of any of the above (processed in sequence)

Directives

Directives are effect descriptions for the runtime to interpret. They are strictly outbound - the agent never receives directives as input.

Directives are bare structs (no tuple wrappers). Built-in directives (see Jido.Agent.Directive):

  • %Directive.Emit{} - Dispatch a signal via Jido.Signal.Dispatch
  • %Directive.Error{} - Signal an error (wraps Jido.Error.t())
  • %Directive.Spawn{} - Spawn a child process
  • %Directive.Schedule{} - Schedule a delayed message
  • %Directive.Stop{} - Stop the agent process

The Emit directive integrates with Jido.Signal for dispatch:

# Emit with default dispatch config
%Directive.Emit{signal: my_signal}

# Emit to PubSub topic
%Directive.Emit{signal: my_signal, dispatch: {:pubsub, topic: "events"}}

# Emit to a specific process
%Directive.Emit{signal: my_signal, dispatch: {:pid, target: pid}}

External packages can define custom directive structs without modifying core.

Directives never modify agent state — that's handled by the returned agent.

Usage

Defining an Agent Module

defmodule MyAgent do
  use Jido.Agent,
    name: "my_agent",
    description: "My custom agent",
    schema: [
      status: [type: :atom, default: :idle],
      counter: [type: :integer, default: 0]
    ]
end

Working with Agents

# Create a new agent (fully initialized including strategy state)
agent = MyAgent.new()
agent = MyAgent.new(id: "custom-id", state: %{counter: 10})

# Execute actions
{agent, directives} = MyAgent.cmd(agent, MyAction)
{agent, directives} = MyAgent.cmd(agent, {MyAction, %{value: 42}})
{agent, directives} = MyAgent.cmd(agent, [Action1, Action2])

# Update state directly
{:ok, agent} = MyAgent.set(agent, %{status: :running})

Strategy Initialization

new/1 automatically calls strategy.init/2 to initialize strategy-specific state. Any directives returned by strategy init are dropped here since they require a runtime to execute. When using AgentServer, it handles strategy init directives separately during startup.

Lifecycle Hooks

Agents support two optional callbacks:

  • on_before_cmd/2 - Called before command processing (pure transformations only)
  • on_after_cmd/3 - Called after command processing (pure transformations only)

State Schema Types

Agent supports two schema formats for state validation:

  1. NimbleOptions schemas (familiar, legacy):

    schema: [
      status: [type: :atom, default: :idle],
      counter: [type: :integer, default: 0]
    ]
  2. Zoi schemas (recommended for new code):

    schema: Zoi.object(%{
      status: Zoi.atom() |> Zoi.default(:idle),
      counter: Zoi.integer() |> Zoi.default(0)
    })

Both are handled transparently by the Agent module.

Pure Functional Design

The Agent struct is immutable. All operations return new agent structs. Server/OTP integration is handled separately by Jido.AgentServer.

Summary

Callbacks

Called after command processing. Can transform the agent or directives. Must be pure - no side effects. Return {:ok, agent, directives} to continue.

Called before command processing. Can transform the agent or action. Must be pure - no side effects. Return {:ok, agent, action} to continue.

Returns signal routes for this agent.

Functions

Creates a new agent from attributes.

Returns the Zoi schema for Agent.

Updates agent state by merging new attributes.

Validates agent state against its schema.

Types

action()

@type action() :: module() | {module(), map()} | Jido.Instruction.t() | [action()]

agent_result()

@type agent_result() :: {:ok, t()} | {:error, Jido.Error.t()}

cmd_result()

@type cmd_result() :: {t(), [directive()]}

directive()

@type directive() :: Jido.Agent.Directive.t()

t()

@type t() :: %Jido.Agent{
  category: binary(),
  description: binary(),
  id: binary(),
  name: binary(),
  schema: any(),
  state: map(),
  tags: [binary()],
  vsn: binary()
}

Callbacks

on_after_cmd(agent, action, directives)

(optional)
@callback on_after_cmd(agent :: t(), action :: term(), directives :: [directive()]) ::
  {:ok, t(), [directive()]}

Called after command processing. Can transform the agent or directives. Must be pure - no side effects. Return {:ok, agent, directives} to continue.

Use cases:

  • Auto-validate state after changes
  • Derive computed fields
  • Add invariant checks

on_before_cmd(agent, action)

(optional)
@callback on_before_cmd(agent :: t(), action :: term()) :: {:ok, t(), term()}

Called before command processing. Can transform the agent or action. Must be pure - no side effects. Return {:ok, agent, action} to continue.

This hook runs once per cmd/2 call, with the action as passed (which may be a list). It is not a per-instruction hook.

Use cases:

  • Mirror action params into agent state (e.g., save last_query before processing)
  • Add default params that depend on current state
  • Enforce invariants or guards before execution

signal_routes()

(optional)
@callback signal_routes() :: [Jido.Signal.Router.route_spec()]

Returns signal routes for this agent.

Routes map signal types to action modules. AgentServer uses these routes to map incoming signals to actions for execution via cmd/2.

Route Formats

  • {path, ActionModule} - Simple mapping (priority 0)
  • {path, ActionModule, priority} - With priority
  • {path, {ActionModule, %{static: params}}} - With static params
  • {path, match_fn, ActionModule} - With pattern matching
  • {path, match_fn, ActionModule, priority} - Full spec

Examples

def signal_routes do
  [
    {"user.created", HandleUserCreatedAction},
    {"counter.increment", IncrementAction},
    {"payment.*", fn s -> s.data.amount > 100 end, LargePaymentAction, 10}
  ]
end

Functions

new(attrs)

@spec new(map() | keyword()) :: {:ok, t()} | {:error, term()}

Creates a new agent from attributes.

For module-based agents, use MyAgent.new/1 instead.

schema()

@spec schema() :: Zoi.schema()

Returns the Zoi schema for Agent.

set(agent, attrs)

@spec set(t(), map() | keyword()) :: agent_result()

Updates agent state by merging new attributes.

validate(agent, opts \\ [])

@spec validate(
  t(),
  keyword()
) :: agent_result()

Validates agent state against its schema.