How Signal Routing Works in Jido AgentServer: A Complete Technical Guide
Signal routing in Jido AgentServer uses a unified trie-based router built from strategy, agent, and plugin routes to match incoming signals and dispatch them to specific action modules.
When a signal reaches a running agent in the Jido framework, the Jido.AgentServer must decide which actions to execute. This article explains how signal routing in Jido AgentServer works, from building the router at startup to processing individual signals through the trie-based matching system.
Building the Signal Router
The router is constructed once during AgentServer initialization and stored in the server state. In lib/jido/agent_server.ex, the handle_continue(:post_init, state) callback invokes SignalRouter.build/1 to create the unified routing table.
Route Sources and Priorities
The SignalRouter.build/1 function in lib/jido/agent_server/signal_router.ex aggregates routes from three distinct sources, each with a default priority:
| Source | Default Priority | Contribution |
|---|---|---|
Strategy (strategy.signal_routes/1) |
50 | Strategy-level commands like {:strategy_cmd, :my_cmd} |
Agent (agent_module.signal_routes/1) |
0 | Agent-specific actions with module references |
Plugins (plugin_routes/0 or plugin.signal_routes/1) |
-10 | Plugin-provided routes or pattern matches |
Lower priority values take precedence, meaning plugin routes (-10) are checked before agent routes (0), which are checked before strategy routes (50).
The SignalRouter.build/1 Function
The builder normalizes each route into a canonical {path, target, priority} tuple using normalize_routes/2. If a route tuple lacks an explicit priority, the function injects the appropriate default based on the source.
# From lib/jido/agent_server/signal_router.ex
signal_router = SignalRouter.build(state)
state = %{state | signal_router: signal_router}
After normalization, the route list passes to Jido.Signal.Router.new/1, which constructs the trie-based router used for fast pattern matching against signal types.
Processing Incoming Signals
When a client sends a signal via AgentServer.cast/2 or AgentServer.call/3, the server eventually reaches process_signal/2, which consults the stored router.
The Routing Pipeline
The process_signal/2 function in lib/jido/agent_server.ex delegates to route_to_actions/2:
defp process_signal(%Signal{} = signal, %State{signal_router: router} = state) do
case route_to_actions(router, signal) do
{:ok, actions} ->
# Execute actions via agent.cmd/2
{:error, reason} ->
# Handle routing failure or default action
end
end
The route_to_actions/2 function calls JidoRouter.route/2 to perform the trie lookup:
defp route_to_actions(router, signal) do
case JidoRouter.route(router, signal) do
{:ok, targets} when targets != [] ->
actions = Enum.map(targets, &target_to_action(&1, signal))
{:ok, actions}
{:error, %{details: %{reason: :no_handlers_found}}} ->
default_system_action(signal)
{:error, reason} ->
{:error, reason}
end
end
Converting Targets to Actions
Each matched target transforms into a concrete action tuple via target_to_action/2 in lib/jido/agent_server.ex:
| Target Pattern | Resulting Action | Purpose |
|---|---|---|
{:strategy_cmd, cmd} |
{cmd, signal.data} |
Strategy-level command dispatch |
{:strategy_tick} |
`{:strategy_tick, %{}} | Periodic strategy execution |
{:custom, _} |
{:custom, signal.data} |
Custom action handling |
mod (atom) |
{mod, signal.data} |
Direct module invocation |
{mod, params} |
{mod, params} |
Module with predefined parameters |
The resulting {module, params} tuples pass to state.agent_module.cmd(state.agent, action_arg), executing the agent's pure logic and returning directives for the server to process.
Plugin Signal Hooks and Pre-Routing
Before the router executes, plugins can intercept signals via run_plugin_signal_hooks. In compute_signal_call_result/2 (around line 1034 in lib/jido/agent_server.ex), the server evaluates plugin callbacks:
case run_plugin_signal_hooks(signal, state) do
{:error, error} ->
# Abort processing and return error
{:override, action_spec, modified_signal} ->
# Skip routing entirely, use specified action
{:continue, modified_signal} ->
# Proceed with normal routing using modified signal
end
Plugins match signals using patterns defined in plugin.signal_patterns (supporting wildcards * and .*). The signal_type_matches?/2 helper implements this pattern logic, allowing plugins to selectively handle signal types without modifying the core router.
Default System Actions
When JidoRouter.route/2 returns {:error, :no_handlers_found}, the server falls back to default_system_action/1 in lib/jido/agent_server.ex:
defp default_system_action(%Signal{type: "jido.agent.stop", data: data}) do
params = if is_map(data), do: data, else: %{}
{:ok, [{Jido.Actions.Lifecycle.StopSelf, params}]}
end
defp default_system_action(_), do: {:error, :no_matching_route}
This built-in handler automatically processes lifecycle signals like "jido.agent.stop" by mapping them to Jido.Actions.Lifecycle.StopSelf, while unknown signal types result in a routing error.
End-to-End Signal Flow
The complete signal routing pipeline in Jido AgentServer follows this execution path:
Client → AgentServer.cast/2 or call/3
→ enqueue_signal_call
→ maybe_start_next_signal_call
→ process_signal
├─ run_plugin_signal_hooks
│ ├─ :override → compute_signal_call_dispatch (skip router)
│ ├─ :continue → route_to_actions (normal routing)
│ └─ :error → emit error directive
└─ route_to_actions
├─ JidoRouter.route (trie lookup by signal type)
├─ target_to_action (convert to {module, params})
└─ state.agent_module.cmd/2 (execute pure agent logic)
→ directives queued → drain loop executes
The router remains static after initialization unless explicitly rebuilt via AgentServer.update_routes/2, ensuring fast O(log n) trie lookups for every signal while supporting dynamic route updates when agents modify their behavior at runtime.
Summary
- Signal routing in Jido AgentServer relies on a unified trie-based router built during server initialization via
SignalRouter.build/1. - Routes originate from three sources: Strategy (priority 50), Agent (priority 0), and Plugins (priority -10), with lower values taking precedence.
- The router matches signal types against patterns using
Jido.Signal.Router, then converts targets to action tuples viatarget_to_action/2inlib/jido/agent_server.ex. - Plugin hooks can intercept signals before routing using
run_plugin_signal_hooks, supporting:override,:continue, or:errorresponses. - Default system actions handle unmatched signals, including automatic lifecycle management for
"jido.agent.stop"viaJido.Actions.Lifecycle.StopSelf.
Frequently Asked Questions
How does Jido AgentServer prioritize conflicting signal routes?
Jido AgentServer assigns default priorities based on route source: Plugins receive priority -10, Agent modules receive priority 0, and Strategies receive priority 50. When multiple routes match the same signal type, the router selects the target with the lowest priority value, ensuring plugin routes override agent routes, and agent routes override strategy routes unless explicitly configured otherwise.
What happens when no route matches an incoming signal?
When the trie-based router returns {:error, :no_handlers_found}, the server invokes default_system_action/1 in lib/jido/agent_server.ex. This function checks for built-in system signals like "jido.agent.stop", which automatically maps to Jido.Actions.Lifecycle.StopSelf. For any other unmatched signal type, the function returns {:error, :no_matching_route}, which the server handles according to its error policy.
Can plugins override the default signal routing behavior?
Yes, plugins can intercept signals before they reach the router through the run_plugin_signal_hooks mechanism implemented in compute_signal_call_result/2. When a plugin's signal_patterns match the incoming signal type (supporting wildcards like * and .*), the plugin can return {:override, action_spec, modified_signal} to bypass the router entirely, {:continue, modified_signal} to proceed with normal routing using a transformed signal, or {:error, reason} to abort processing.
How do I define custom signal routes in my agent module?
Define the signal_routes/1 callback in your agent module to return a list of route tuples. Each route specifies a signal pattern and a target module or command. For example, {"user.request.*", MyHandler} routes any signal matching "user.request.*" to the MyHandler module. You can also use tuple targets like {:strategy_cmd, :my_command} or include explicit priorities like {"alert.*", AlertHandler, 5}. The SignalRouter.build/1 function automatically calls your agent's signal_routes/1 callback during server initialization with default priority 0.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →