Jido.AgentServer (Jido v2.0.0-rc.1)

View Source

GenServer runtime for Jido agents.

AgentServer is the "Act" side of the Jido framework: while Agents "think" (pure decision logic via cmd/2), AgentServer "acts" by executing the directives they emit. Signal routing happens in AgentServer, keeping Agents purely action-oriented.

Architecture

  • Single GenServer per agent under Jido.AgentSupervisor
  • Internal directive queue with drain loop for non-blocking processing
  • Registry-based naming via Jido.Registry
  • Logical parent-child hierarchy via state tracking + monitors

Public API

  • start/1 - Start under DynamicSupervisor
  • start_link/1 - Start linked to caller
  • call/3 - Synchronous signal processing
  • cast/2 - Asynchronous signal processing
  • state/1 - Get full State struct
  • whereis/1 - Registry lookup by ID (default registry)
  • whereis/2 - Registry lookup by ID (specific registry)

Signal Flow

Signal  AgentServer.call/cast
       route_signal_to_action (via strategy.signal_routes or default)
       Agent.cmd/2
       {agent, directives}
       Directives queued
       Drain loop executes via DirectiveExec protocol

Signal routing is owned by AgentServer, not the Agent. Strategies can define signal_routes/1 to map signal types to strategy commands. Unmatched signals fall back to {signal.type, signal.data} as the action.

Options

  • :agent - Agent module or struct (required)
  • :id - Instance ID (auto-generated if not provided)
  • :initial_state - Initial state map for agent
  • :registry - Registry module (default: Jido.Registry)
  • :default_dispatch - Default dispatch config for Emit directives
  • :error_policy - Error handling policy
  • :max_queue_size - Max directive queue size (default: 10_000)
  • :parent - Parent reference for hierarchy
  • :on_parent_death - Behavior when parent dies (:stop, :continue, :emit_orphan)
  • :spawn_fun - Custom function for spawning children

Agent Resolution

The :agent option accepts:

  • Module name - Must implement new/0 or new/1
    • new/1 receives [id: id, state: initial_state] as keyword options
    • new/0 creates agent with defaults; :id and :initial_state options are ignored
  • Agent struct - Used directly
    • Provide :agent_module option to specify the module if it differs from agent.__struct__
    • The struct's ID takes precedence over the :id option

The :agent_module option is only used when :agent is a struct. It tells AgentServer which module implements the agent behavior (for calling cmd/2, lifecycle hooks, etc.).

Examples

# Module with new/1 - receives id and state
{:ok, pid} = AgentServer.start_link(
  agent: MyAgent,
  id: "my-id",
  initial_state: %{counter: 42}
)

# Module with new/0 - id and state ignored
{:ok, pid} = AgentServer.start_link(agent: SimpleAgent)

# Pre-built struct - requires agent_module
agent = MyAgent.new(id: "prebuilt", state: %{value: 99})
{:ok, pid} = AgentServer.start_link(
  agent: agent,
  agent_module: MyAgent
)

Completion Detection

Agents signal completion via state, not process death:

# In your strategy/agent, set terminal status:
agent = put_in(agent.state.status, :completed)
agent = put_in(agent.state.last_answer, answer)

# External code polls for completion:
{:ok, state} = AgentServer.state(server)
case state.agent.state.status do
  :completed -> state.agent.state.last_answer
  :failed -> {:error, state.agent.state.error}
  _ -> :still_running
end

This follows Elm/Redux semantics where completion is a state concern. The process stays alive until explicitly stopped or supervised.

Do NOT use {:stop, ...} from DirectiveExec for normal completion—this causes race conditions with async work and skips lifecycle hooks. See Jido.AgentServer.DirectiveExec for details.

Summary

Functions

Check if the agent server process is alive.

Attaches a process to this agent, tracking it as an active consumer.

Wait for an agent to reach a terminal status (:completed or :failed).

Synchronously sends a signal and waits for processing.

Asynchronously sends a signal for processing.

Returns a child_spec for supervision.

Detaches a process from this agent.

Starts an AgentServer under Jido.AgentSupervisor.

Starts an AgentServer linked to the calling process.

Gets the full State struct for an agent.

Gets runtime status for an agent process.

Streams status updates by polling at regular intervals.

Touches the agent to reset the idle timer.

Returns a via tuple for Registry-based naming.

Looks up an agent by ID in a specific registry.

Types

server()

@type server() :: pid() | atom() | {:via, module(), term()} | String.t()

Functions

alive?(server)

@spec alive?(server()) :: boolean()

Check if the agent server process is alive.

attach(server, owner_pid \\ self())

@spec attach(server(), pid()) :: :ok | {:error, term()}

Attaches a process to this agent, tracking it as an active consumer.

When attached, the agent will not idle-timeout. The agent monitors the attached process and automatically detaches it on exit.

Used by Jido.Agent.InstanceManager to track LiveView sockets, WebSocket handlers, or any process that needs the agent to stay alive.

Examples

{:ok, pid} = Jido.Agent.InstanceManager.get(:sessions, key)
:ok = Jido.AgentServer.attach(pid)

# With explicit owner
:ok = Jido.AgentServer.attach(pid, socket_pid)

await_completion(server, opts \\ [])

@spec await_completion(
  server(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Wait for an agent to reach a terminal status (:completed or :failed).

This is an event-driven wait - the caller blocks until the agent's state transitions to a terminal status, then receives the result immediately. No polling is involved.

Options

  • :status_path - Path to status field in agent.state (default: [:status])
  • :result_path - Path to result field (default: [:last_answer])
  • :error_path - Path to error field (default: [:error])

Returns

  • {:ok, %{status: :completed | :failed, result: any()}} - Agent reached terminal status

  • {:error, :not_found} - Server not found
  • Exits with {:timeout, ...} if GenServer.call times out

Examples

{:ok, result} = AgentServer.await_completion(pid, timeout: 10_000)

call(server, signal, timeout \\ 5000)

@spec call(server(), Jido.Signal.t(), timeout()) :: {:ok, struct()} | {:error, term()}

Synchronously sends a signal and waits for processing.

Returns the updated agent struct after signal processing. Directives are still executed asynchronously via the drain loop.

Returns

  • {:ok, agent} - Signal processed successfully
  • {:error, :not_found} - Server not found via registry
  • {:error, :invalid_server} - Unsupported server reference
  • Exits with {:noproc, ...} if process dies during call

Examples

{:ok, agent} = Jido.AgentServer.call(pid, signal)
{:ok, agent} = Jido.AgentServer.call("agent-id", signal, 10_000)

cast(server, signal)

@spec cast(server(), Jido.Signal.t()) :: :ok | {:error, term()}

Asynchronously sends a signal for processing.

Returns immediately. The signal is processed in the background.

Returns

  • :ok - Signal queued successfully
  • {:error, :not_found} - Server not found via registry
  • {:error, :invalid_server} - Unsupported server reference

Examples

:ok = Jido.AgentServer.cast(pid, signal)
:ok = Jido.AgentServer.cast("agent-id", signal)

child_spec(init_arg)

@spec child_spec(keyword() | map()) :: Supervisor.child_spec()

Returns a child_spec for supervision.

detach(server, owner_pid \\ self())

@spec detach(server(), pid()) :: :ok | {:error, term()}

Detaches a process from this agent.

If this was the last attachment and idle_timeout is configured, the idle timer starts.

Note: You don't need to call this explicitly if the attached process exits normally — the monitor will handle cleanup automatically.

Examples

:ok = Jido.AgentServer.detach(pid)

start(opts)

@spec start(keyword() | map()) :: DynamicSupervisor.on_start_child()

Starts an AgentServer under Jido.AgentSupervisor.

Examples

{:ok, pid} = Jido.AgentServer.start(agent: MyAgent)
{:ok, pid} = Jido.AgentServer.start(agent: MyAgent, id: "my-agent")

start_link(opts)

@spec start_link(keyword() | map()) :: GenServer.on_start()

Starts an AgentServer linked to the calling process.

Options

See module documentation for full list of options.

Examples

{:ok, pid} = Jido.AgentServer.start_link(agent: MyAgent)
{:ok, pid} = Jido.AgentServer.start_link(agent: MyAgent, id: "custom-123")

state(server)

@spec state(server()) :: {:ok, Jido.AgentServer.State.t()} | {:error, term()}

Gets the full State struct for an agent.

Returns

  • {:ok, state} - Full State struct retrieved
  • {:error, :not_found} - Server not found via registry
  • {:error, :invalid_server} - Unsupported server reference

Examples

{:ok, state} = Jido.AgentServer.state(pid)
{:ok, state} = Jido.AgentServer.state("agent-id")

status(server)

@spec status(server()) :: {:ok, Jido.AgentServer.Status.t()} | {:error, term()}

Gets runtime status for an agent process.

Returns a Status struct combining the strategy snapshot with process metadata. This provides a stable API for querying agent status without depending on internal __strategy__ state structure.

Returns

  • {:ok, status} - Status struct with snapshot and metadata
  • {:error, :not_found} - Server not found via registry
  • {:error, :invalid_server} - Unsupported server reference

Examples

{:ok, agent_status} = Jido.AgentServer.status(pid)

# Check completion
if agent_status.snapshot.done? do
  IO.puts("Result: " <> inspect(agent_status.snapshot.result))
end

# Use delegate helpers
case Status.status(agent_status) do
  :success -> {:done, Status.result(agent_status)}
  :failure -> {:error, Status.details(agent_status)}
  _ -> :continue
end

stream_status(server, opts \\ [])

@spec stream_status(
  server(),
  keyword()
) :: Enumerable.t()

Streams status updates by polling at regular intervals.

Returns a Stream that yields status snapshots. Useful for monitoring agent execution without manual polling loops.

Options

  • :interval_ms - Polling interval in milliseconds (default: 100)

Examples

# Poll until completion
AgentServer.stream_status(pid, interval_ms: 50)
|> Enum.reduce_while(nil, fn status, _acc ->
  case Status.status(status) do
    :success -> {:halt, {:ok, Status.result(status)}}
    :failure -> {:halt, {:error, Status.details(status)}}
    _ -> {:cont, nil}
  end
end)

# Take first 10 snapshots
AgentServer.stream_status(pid)
|> Enum.take(10)

touch(server)

@spec touch(server()) :: :ok | {:error, term()}

Touches the agent to reset the idle timer.

Use this for request-based activity tracking (e.g., HTTP requests) where you don't want to maintain a persistent attachment.

Examples

# In a controller
{:ok, pid} = Jido.Agent.InstanceManager.get(:sessions, key)
:ok = Jido.AgentServer.touch(pid)

via_tuple(id, registry)

@spec via_tuple(String.t(), module()) :: {:via, Registry, {module(), String.t()}}

Returns a via tuple for Registry-based naming.

Examples

name = Jido.AgentServer.via_tuple("agent-id", MyApp.Jido.Registry)
GenServer.call(name, :get_state)

whereis(registry, id)

@spec whereis(module(), String.t()) :: pid() | nil

Looks up an agent by ID in a specific registry.

Returns the pid if found, nil otherwise.

Examples

pid = Jido.AgentServer.whereis(MyApp.Jido.Registry, "agent-123")
# => #PID<0.123.0>