How to: Configure Three-Tier Signal Routing

Goal: Define signal routes across strategy, agent, and plugin tiers so incoming signals produce the correct agent actions.


Core types

#![allow(unused)]
fn main() {
pub struct Signal {
    pub kind: SignalKind,
    pub payload: serde_json::Value,
}

pub enum SignalKind {
    Stop,
    UserMessage,
    ToolResult,
    Timer,
    Custom(String),
}

pub enum Action {
    Continue,
    GracefulStop,
    ForceStop,
    Transition(String),   // FSM state name
    Custom(String),
}
}

SignalRoute

A route binds a SignalKind to an Action, with an optional predicate for finer-grained matching and a numeric priority for tie-breaking within the same tier.

#![allow(unused)]
fn main() {
use synwire_core::agents::signal::{Action, Signal, SignalKind, SignalRoute};

// Match all Stop signals.
let route = SignalRoute::new(SignalKind::Stop, Action::GracefulStop, 10);

// Match only non-empty user messages.
fn non_empty(s: &Signal) -> bool {
    s.payload.as_str().is_some_and(|v| !v.is_empty())
}

let guarded = SignalRoute::with_predicate(
    SignalKind::UserMessage,
    non_empty,
    Action::Continue,
    20,  // higher priority — wins over routes with lower values
);
}

Predicates must be plain function pointers (fn(&Signal) -> bool) so SignalRoute remains Clone + Send + Sync.


ComposedRouter

Combines three tiers of routes. Within each tier the highest-priority matching route wins. The strategy tier always beats the agent tier, which always beats the plugin tier — regardless of priority values within tiers.

#![allow(unused)]
fn main() {
use synwire_core::agents::signal::{Action, ComposedRouter, Signal, SignalKind, SignalRoute, SignalRouter};

let strategy_routes = vec![
    // Unconditionally force-stop on Stop signal from strategy level.
    SignalRoute::new(SignalKind::Stop, Action::ForceStop, 0),
];

let agent_routes = vec![
    // Agent prefers graceful stop with higher intra-tier priority.
    SignalRoute::new(SignalKind::Stop, Action::GracefulStop, 100),
    SignalRoute::new(SignalKind::UserMessage, Action::Continue, 0),
    SignalRoute::new(SignalKind::Timer, Action::Transition("tick".to_string()), 0),
];

let plugin_routes = vec![
    SignalRoute::new(SignalKind::Custom("metrics".to_string()), Action::Continue, 0),
];

let router = ComposedRouter::new(strategy_routes, agent_routes, plugin_routes);
}

Routing a signal:

#![allow(unused)]
fn main() {
use serde_json::json;

let signal = Signal::new(SignalKind::Stop, json!(null));

match router.route(&signal) {
    Some(Action::ForceStop)   => { /* strategy tier matched */ }
    Some(Action::GracefulStop) => { /* agent tier matched */ }
    Some(action)              => { /* other action */ }
    None                      => { /* no route — apply default behaviour */ }
}
}

Inspect all registered routes across tiers:

#![allow(unused)]
fn main() {
let all_routes = router.routes();
println!("{} routes registered", all_routes.len());
}

Custom routers

Implement SignalRouter to replace the composed approach entirely:

#![allow(unused)]
fn main() {
use synwire_core::agents::signal::{Action, Signal, SignalRouter, SignalRoute};

struct AlwaysContinueRouter;

impl SignalRouter for AlwaysContinueRouter {
    fn route(&self, _signal: &Signal) -> Option<Action> {
        Some(Action::Continue)
    }

    fn routes(&self) -> Vec<SignalRoute> {
        Vec::new()
    }
}
}

Priority semantics summary

SituationWinner
Strategy route vs. agent route (same kind)Strategy, regardless of priority values
Agent route vs. plugin route (same kind)Agent, regardless of priority values
Two routes in the same tier, same kindHighest priority value
Predicate fails on higher-priority routeNext matching route in same tier
No route matches in any tierNone — caller decides default

See also