Agent Runtime

How AgentServer runs agents as OTP processes, routes signals, and executes directives.

Agents think, the runtime acts

Jido splits agent work into two halves. Agents “think” by running pure decision logic through cmd/2, which returns an updated agent struct and a list of directives. AgentServer “acts” by executing those directives as side effects.

This separation keeps your agent logic testable without a running process. You can call cmd/2 directly in tests and inspect the returned directives without starting a GenServer, connecting to external services, or managing process lifecycle.

AgentServer owns signal routing, directive execution, and process supervision. Your agent module never needs to know how signals arrive or where directives go.

Starting an agent process

Each AgentServer is a single GenServer registered in Jido.Registry. You start one by passing an :agent option that identifies the agent module or struct.

# Start under Jido.AgentSupervisor (DynamicSupervisor)
{:ok, pid} = Jido.AgentServer.start(agent: MyAgent)

# Start linked to the calling process
{:ok, pid} = Jido.AgentServer.start_link(
  agent: MyAgent,
  id: "order-42",
  initial_state: %{total: 0}
)

The :agent option accepts two forms:

  • Module name – The module must implement new/0 or new/1. When new/1 is available, AgentServer passes [id: id, state: initial_state] as keyword options.
  • Agent struct – Used directly. Provide :agent_module if the behavior module differs from the struct’s module.
# Pre-built struct with explicit module
agent = MyAgent.new(id: "prebuilt", state: %{value: 99})
{:ok, pid} = Jido.AgentServer.start_link(
  agent: agent,
  agent_module: MyAgent
)

Once started, you reference the server by pid or by its string ID. The whereis/1 function resolves an ID to a pid through the registry.

Signal flow

Signals are the sole input channel to a running agent. You send them synchronously with call/3 or asynchronously with cast/2.

signal = Jido.Signal.new!(%{type: "order.placed", data: %{item: "widget"}})

# Synchronous: blocks until cmd/2 completes, returns updated agent
{:ok, agent} = Jido.AgentServer.call(pid, signal)

# Asynchronous: returns immediately
:ok = Jido.AgentServer.cast(pid, signal)

When a signal arrives, AgentServer follows this path:

  1. The signal router maps the signal type to one or more actions. Routes come from strategy-defined signal_routes/1 or fall back to {signal.type, signal.data}.
  2. AgentServer calls Agent.cmd/2 with the resolved action. This runs in a supervised task, keeping the GenServer responsive.
  3. cmd/2 returns {updated_agent, directives}.
  4. The agent struct is updated in server state. For synchronous calls, the updated agent is returned to the caller.
  5. Directives are enqueued for execution.

Signal processing runs one at a time. If a synchronous call is in flight and an async signal arrives, the async signal is deferred until the current call completes.

Directive execution

After cmd/2 produces directives, AgentServer enqueues them and starts a drain loop. The drain loop processes one directive at a time by sending :drain messages to itself, keeping the GenServer free to handle other messages between iterations.

Each directive is executed through the DirectiveExec protocol. Built-in implementations handle the core directive types:

Directive Effect
Emit Dispatches a signal to configured targets
RunInstruction Executes another cmd/2 cycle within the same process
SpawnAgent Starts a child agent process
Schedule Delivers a signal after a delay
StopChild Stops a child agent by tag
Error Routes to the configured error policy

When the queue empties, the drain loop stops and the server returns to :idle status. If the queue exceeds :max_queue_size (default 10,000), new directives are dropped and a :queue_overflow error is returned.

Parent-child hierarchy

AgentServer supports a logical parent-child hierarchy through directive-driven spawning. When a SpawnAgent directive executes, AgentServer starts a new child process under Jido.AgentSupervisor and monitors it.

The parent tracks children by tag in its state. When a child exits, the parent receives a ChildExit signal containing the tag, pid, and exit reason. You can route this signal to handle cleanup or retry logic.

Children receive a :parent reference at startup and monitor the parent process. The :on_parent_death option controls what happens when the parent goes down:

Value Behavior
:stop Child shuts down cleanly
:continue Child keeps running independently
:emit_orphan Child processes an Orphaned signal, letting its strategy decide

This hierarchy is logical, not supervisory. OTP supervision still comes from Jido.AgentSupervisor. The parent-child relationship gives you application-level coordination without coupling agent lifecycles to the supervision tree.

Completion detection

Agents signal completion through state, not process death. Set agent.state.status to :completed or :failed inside your strategy, then observe it externally.

# Poll for completion
{:ok, state} = Jido.AgentServer.state(pid)
case state.agent.state.status do
  :completed -> state.agent.state.last_answer
  :failed -> {:error, state.agent.state.error}
  _ -> :still_running
end

# Or await completion (event-driven, no polling)
{:ok, result} = Jido.AgentServer.await_completion(pid, timeout: 10_000)

The process stays alive after completion until explicitly stopped or cleaned up by supervision. Completion is a state concern, not a lifecycle event.

Configuration

Option Description Default
:agent Agent module or struct (required)
:id Instance ID for registry Auto-generated
:initial_state Initial state map passed to new/1%{}
:agent_module Behavior module when :agent is a struct Struct module
:max_queue_size Maximum directive queue depth 10,000
:default_dispatch Default dispatch config for Emit directives Current process
:parent Parent reference for hierarchy nil
:on_parent_death:stop, :continue, or :emit_orphan:stop
:debug Enable in-memory event buffer false

Next steps

  • Agents – understand the pure decision logic that AgentServer executes
  • Signals – learn about typed envelopes and routing patterns
  • Directives – explore the side-effect payloads that drive the runtime
  • Actions – see how pure functions transform agent state