tiphys.routing

Tiphys multi-agent routing system.

Provides agent registry, bindings, and route resolution for multi-agent support.

 1"""
 2Tiphys multi-agent routing system.
 3
 4Provides agent registry, bindings, and route resolution for multi-agent support.
 5"""
 6
 7from tiphys.routing.bindings import (
 8    AgentBinding,
 9    BindingPriority,
10    BindingType,
11    RoutingContext,
12    create_channel_binding,
13    create_default_binding,
14    create_group_binding,
15    create_peer_binding,
16)
17from tiphys.routing.registry import (
18    AgentDefinition,
19    AgentRegistry,
20    create_default_agent,
21    get_agent_registry,
22)
23from tiphys.routing.resolver import (
24    RouteResolver,
25    get_route_resolver,
26    resolve_agent,
27)
28
29__all__ = [
30    # Bindings
31    "AgentBinding",
32    "BindingType",
33    "BindingPriority",
34    "RoutingContext",
35    "create_peer_binding",
36    "create_group_binding",
37    "create_channel_binding",
38    "create_default_binding",
39    # Registry
40    "AgentDefinition",
41    "AgentRegistry",
42    "get_agent_registry",
43    "create_default_agent",
44    # Resolver
45    "RouteResolver",
46    "get_route_resolver",
47    "resolve_agent",
48]
class AgentBinding(pydantic.main.BaseModel):
 34class AgentBinding(BaseModel):
 35    """
 36    A binding that maps a source to an agent.
 37
 38    Bindings are evaluated in priority order until a match is found.
 39    """
 40
 41    agent_id: str = Field(..., description="Target agent identifier")
 42    binding_type: BindingType = Field(..., description="Type of binding")
 43    priority: int = Field(
 44        default=0,
 45        description="Custom priority (higher = evaluated first). Defaults to type priority.",
 46    )
 47
 48    # Matching criteria (depending on binding type)
 49    peer_id: str | None = Field(default=None, description="Peer ID for PEER bindings")
 50    group_id: str | None = Field(default=None, description="Group ID for GROUP bindings")
 51    channel_name: str | None = Field(default=None, description="Channel name for CHANNEL bindings")
 52    pattern: str | None = Field(
 53        default=None, description="Pattern for PATTERN bindings (supports glob)"
 54    )
 55
 56    # Metadata
 57    description: str | None = Field(default=None, description="Human-readable description")
 58    enabled: bool = Field(default=True, description="Whether this binding is active")
 59    metadata: dict[str, Any] = Field(default_factory=dict, description="Extra metadata")
 60
 61    def model_post_init(self, __context: Any) -> None:
 62        """Set default priority based on binding type if not specified."""
 63        if self.priority == 0:
 64            self.priority = BindingPriority[self.binding_type.name].value
 65
 66    def matches(
 67        self,
 68        peer_id: str | None = None,
 69        group_id: str | None = None,
 70        channel_name: str | None = None,
 71    ) -> bool:
 72        """
 73        Check if this binding matches the given context.
 74
 75        Args:
 76            peer_id: The peer ID to match.
 77            group_id: The group ID to match.
 78            channel_name: The channel name to match.
 79
 80        Returns:
 81            True if this binding matches the context.
 82        """
 83        if not self.enabled:
 84            return False
 85
 86        match self.binding_type:
 87            case BindingType.PEER:
 88                return self.peer_id is not None and self.peer_id == peer_id
 89
 90            case BindingType.GROUP:
 91                return self.group_id is not None and self.group_id == group_id
 92
 93            case BindingType.CHANNEL:
 94                return self.channel_name is not None and self.channel_name == channel_name
 95
 96            case BindingType.PATTERN:
 97                if self.pattern is None:
 98                    return False
 99                # Support glob-style patterns
100                return self._matches_pattern(peer_id, group_id, channel_name)
101
102            case BindingType.DEFAULT:
103                return True
104
105        return False
106
107    def _matches_pattern(
108        self,
109        peer_id: str | None,
110        group_id: str | None,
111        channel_name: str | None,
112    ) -> bool:
113        """Check if any of the identifiers match the pattern."""
114        import fnmatch
115
116        if self.pattern is None:
117            return False
118
119        # Try to match against any provided identifier
120        for identifier in [peer_id, group_id, channel_name]:
121            if identifier is not None and fnmatch.fnmatch(identifier, self.pattern):
122                return True
123
124        return False

A binding that maps a source to an agent.

Bindings are evaluated in priority order until a match is found.

agent_id: str = PydanticUndefined

Target agent identifier

binding_type: BindingType = PydanticUndefined

Type of binding

priority: int = 0

Custom priority (higher = evaluated first). Defaults to type priority.

peer_id: str | None = None

Peer ID for PEER bindings

group_id: str | None = None

Group ID for GROUP bindings

channel_name: str | None = None

Channel name for CHANNEL bindings

pattern: str | None = None

Pattern for PATTERN bindings (supports glob)

description: str | None = None

Human-readable description

enabled: bool = True

Whether this binding is active

metadata: dict[str, typing.Any] = PydanticUndefined

Extra metadata

def matches( self, peer_id: str | None = None, group_id: str | None = None, channel_name: str | None = None) -> bool:
 66    def matches(
 67        self,
 68        peer_id: str | None = None,
 69        group_id: str | None = None,
 70        channel_name: str | None = None,
 71    ) -> bool:
 72        """
 73        Check if this binding matches the given context.
 74
 75        Args:
 76            peer_id: The peer ID to match.
 77            group_id: The group ID to match.
 78            channel_name: The channel name to match.
 79
 80        Returns:
 81            True if this binding matches the context.
 82        """
 83        if not self.enabled:
 84            return False
 85
 86        match self.binding_type:
 87            case BindingType.PEER:
 88                return self.peer_id is not None and self.peer_id == peer_id
 89
 90            case BindingType.GROUP:
 91                return self.group_id is not None and self.group_id == group_id
 92
 93            case BindingType.CHANNEL:
 94                return self.channel_name is not None and self.channel_name == channel_name
 95
 96            case BindingType.PATTERN:
 97                if self.pattern is None:
 98                    return False
 99                # Support glob-style patterns
100                return self._matches_pattern(peer_id, group_id, channel_name)
101
102            case BindingType.DEFAULT:
103                return True
104
105        return False

Check if this binding matches the given context.

Args: peer_id: The peer ID to match. group_id: The group ID to match. channel_name: The channel name to match.

Returns: True if this binding matches the context.

class BindingType(builtins.str, enum.Enum):
14class BindingType(str, Enum):
15    """Types of routing bindings."""
16
17    PEER = "peer"  # Direct peer binding
18    GROUP = "group"  # Group/channel binding
19    CHANNEL = "channel"  # Messaging platform channel
20    PATTERN = "pattern"  # Pattern-based binding (regex/glob)
21    DEFAULT = "default"  # Fallback binding

Types of routing bindings.

PEER = <BindingType.PEER: 'peer'>
GROUP = <BindingType.GROUP: 'group'>
CHANNEL = <BindingType.CHANNEL: 'channel'>
PATTERN = <BindingType.PATTERN: 'pattern'>
DEFAULT = <BindingType.DEFAULT: 'default'>
class BindingPriority(builtins.int, enum.Enum):
24class BindingPriority(int, Enum):
25    """Priority levels for binding resolution."""
26
27    PEER = 100  # Highest - direct peer bindings
28    GROUP = 80  # Group-specific bindings
29    CHANNEL = 60  # Channel-specific bindings
30    PATTERN = 40  # Pattern-based bindings
31    DEFAULT = 0  # Lowest - fallback

Priority levels for binding resolution.

PEER = <BindingPriority.PEER: 100>
GROUP = <BindingPriority.GROUP: 80>
CHANNEL = <BindingPriority.CHANNEL: 60>
PATTERN = <BindingPriority.PATTERN: 40>
DEFAULT = <BindingPriority.DEFAULT: 0>
class RoutingContext(pydantic.main.BaseModel):
127class RoutingContext(BaseModel):
128    """Context for routing decisions."""
129
130    peer_id: str | None = Field(default=None, description="The peer making the request")
131    group_id: str | None = Field(default=None, description="The group context")
132    channel_name: str | None = Field(default=None, description="The channel name")
133    account_id: str | None = Field(default=None, description="The account ID")
134    metadata: dict[str, Any] = Field(default_factory=dict, description="Extra context")

Context for routing decisions.

peer_id: str | None = None

The peer making the request

group_id: str | None = None

The group context

channel_name: str | None = None

The channel name

account_id: str | None = None

The account ID

metadata: dict[str, typing.Any] = PydanticUndefined

Extra context

def create_peer_binding( peer_id: str, agent_id: str, **kwargs: Any) -> AgentBinding:
137def create_peer_binding(peer_id: str, agent_id: str, **kwargs: Any) -> AgentBinding:
138    """Create a direct peer-to-agent binding."""
139    return AgentBinding(
140        agent_id=agent_id,
141        binding_type=BindingType.PEER,
142        peer_id=peer_id,
143        **kwargs,
144    )

Create a direct peer-to-agent binding.

def create_group_binding( group_id: str, agent_id: str, **kwargs: Any) -> AgentBinding:
147def create_group_binding(group_id: str, agent_id: str, **kwargs: Any) -> AgentBinding:
148    """Create a group-to-agent binding."""
149    return AgentBinding(
150        agent_id=agent_id,
151        binding_type=BindingType.GROUP,
152        group_id=group_id,
153        **kwargs,
154    )

Create a group-to-agent binding.

def create_channel_binding( channel_name: str, agent_id: str, **kwargs: Any) -> AgentBinding:
157def create_channel_binding(channel_name: str, agent_id: str, **kwargs: Any) -> AgentBinding:
158    """Create a channel-to-agent binding."""
159    return AgentBinding(
160        agent_id=agent_id,
161        binding_type=BindingType.CHANNEL,
162        channel_name=channel_name,
163        **kwargs,
164    )

Create a channel-to-agent binding.

def create_default_binding(agent_id: str, **kwargs: Any) -> AgentBinding:
167def create_default_binding(agent_id: str, **kwargs: Any) -> AgentBinding:
168    """Create a default fallback binding."""
169    return AgentBinding(
170        agent_id=agent_id,
171        binding_type=BindingType.DEFAULT,
172        **kwargs,
173    )

Create a default fallback binding.

class AgentDefinition(pydantic.main.BaseModel):
 16class AgentDefinition(BaseModel):
 17    """
 18    Definition for an agent.
 19
 20    Each agent has its own identity, configuration, and behavior settings.
 21    """
 22
 23    id: str = Field(..., description="Unique agent identifier")
 24    name: str = Field(..., description="Display name for the agent")
 25    description: str | None = Field(default=None, description="Agent description")
 26
 27    # LLM Configuration
 28    model: str | None = Field(
 29        default=None,
 30        description="LLM model (defaults to global config if not set)",
 31    )
 32    api_base: str | None = Field(
 33        default=None,
 34        description="Custom API base URL for this agent",
 35    )
 36    api_key: str | None = Field(
 37        default=None,
 38        description="API key for this agent's LLM provider",
 39    )
 40    temperature: float = Field(
 41        default=0.7,
 42        ge=0.0,
 43        le=2.0,
 44        description="LLM temperature",
 45    )
 46    max_tokens: int | None = Field(
 47        default=None,
 48        description="Maximum response tokens",
 49    )
 50
 51    # Session settings
 52    dm_scope: DmScope = Field(
 53        default=DmScope.MAIN,
 54        description="Session scoping mode for this agent",
 55    )
 56
 57    # Prompt customization
 58    system_prompt: str | None = Field(
 59        default=None,
 60        description="Custom system prompt (replaces default)",
 61    )
 62    system_prompt_prefix: str | None = Field(
 63        default=None,
 64        description="Prepended to system prompt",
 65    )
 66    system_prompt_suffix: str | None = Field(
 67        default=None,
 68        description="Appended to system prompt",
 69    )
 70    custom_instructions: str | None = Field(
 71        default=None,
 72        description="Additional instructions injected into prompt",
 73    )
 74
 75    # Tool configuration
 76    tools_allow: list[str] = Field(
 77        default_factory=list,
 78        description="Allowed tools (empty = all tools)",
 79    )
 80    tools_deny: list[str] = Field(
 81        default_factory=list,
 82        description="Denied tools",
 83    )
 84    tools_require_permission: list[str] = Field(
 85        default_factory=list,
 86        description="Tools requiring explicit permission",
 87    )
 88
 89    # Skills configuration
 90    skills_dir: Path | None = Field(
 91        default=None,
 92        description="Agent-specific skills directory",
 93    )
 94    preload_skills: list[str] = Field(
 95        default_factory=list,
 96        description="Skills to preload into context",
 97    )
 98
 99    # Behavioral settings
100    max_steps: int = Field(
101        default=10,
102        ge=1,
103        le=100,
104        description="Maximum tool execution steps",
105    )
106    streaming: bool = Field(
107        default=True,
108        description="Whether to stream responses",
109    )
110
111    # Metadata
112    metadata: dict[str, Any] = Field(
113        default_factory=dict,
114        description="Extra metadata",
115    )
116    enabled: bool = Field(
117        default=True,
118        description="Whether this agent is active",
119    )

Definition for an agent.

Each agent has its own identity, configuration, and behavior settings.

id: str = PydanticUndefined

Unique agent identifier

name: str = PydanticUndefined

Display name for the agent

description: str | None = None

Agent description

model: str | None = None

LLM model (defaults to global config if not set)

api_base: str | None = None

Custom API base URL for this agent

api_key: str | None = None

API key for this agent's LLM provider

temperature: float = 0.7

LLM temperature

max_tokens: int | None = None

Maximum response tokens

dm_scope: tiphys.DmScope = <DmScope.MAIN: 'main'>

Session scoping mode for this agent

system_prompt: str | None = None

Custom system prompt (replaces default)

system_prompt_prefix: str | None = None

Prepended to system prompt

system_prompt_suffix: str | None = None

Appended to system prompt

custom_instructions: str | None = None

Additional instructions injected into prompt

tools_allow: list[str] = PydanticUndefined

Allowed tools (empty = all tools)

tools_deny: list[str] = PydanticUndefined

Denied tools

tools_require_permission: list[str] = PydanticUndefined

Tools requiring explicit permission

skills_dir: pathlib.Path | None = None

Agent-specific skills directory

preload_skills: list[str] = PydanticUndefined

Skills to preload into context

max_steps: int = 10

Maximum tool execution steps

streaming: bool = True

Whether to stream responses

metadata: dict[str, typing.Any] = PydanticUndefined

Extra metadata

enabled: bool = True

Whether this agent is active

class AgentRegistry:
122class AgentRegistry:
123    """
124    Registry for managing multiple agent definitions.
125
126    Provides lookup, validation, and lifecycle management for agents.
127    """
128
129    def __init__(self) -> None:
130        """Initialize an empty agent registry."""
131        self._agents: dict[str, AgentDefinition] = {}
132        self._default_agent_id: str | None = None
133
134    def register(self, agent: AgentDefinition) -> None:
135        """
136        Register an agent definition.
137
138        Args:
139            agent: The agent definition to register.
140
141        Raises:
142            ValueError: If an agent with the same ID already exists.
143        """
144        if agent.id in self._agents:
145            raise ValueError(f"Agent '{agent.id}' already registered")
146        self._agents[agent.id] = agent
147
148        # Set as default if it's the first agent
149        if self._default_agent_id is None:
150            self._default_agent_id = agent.id
151
152    def unregister(self, agent_id: str) -> bool:
153        """
154        Unregister an agent.
155
156        Args:
157            agent_id: The agent ID to unregister.
158
159        Returns:
160            True if the agent was unregistered, False if not found.
161        """
162        if agent_id not in self._agents:
163            return False
164
165        del self._agents[agent_id]
166
167        # Clear default if we removed it
168        if self._default_agent_id == agent_id:
169            self._default_agent_id = next(iter(self._agents), None)
170
171        return True
172
173    def get(self, agent_id: str) -> AgentDefinition | None:
174        """
175        Get an agent definition by ID.
176
177        Args:
178            agent_id: The agent ID.
179
180        Returns:
181            The agent definition, or None if not found.
182        """
183        return self._agents.get(agent_id)
184
185    def get_or_default(self, agent_id: str | None = None) -> AgentDefinition | None:
186        """
187        Get an agent by ID, or return the default agent.
188
189        Args:
190            agent_id: Optional agent ID. Uses default if None.
191
192        Returns:
193            The agent definition, or None if no agents registered.
194        """
195        if agent_id:
196            agent = self._agents.get(agent_id)
197            if agent:
198                return agent
199
200        if self._default_agent_id:
201            return self._agents.get(self._default_agent_id)
202
203        return None
204
205    def list_agents(self) -> list[AgentDefinition]:
206        """
207        List all registered agents.
208
209        Returns:
210            List of all agent definitions.
211        """
212        return list(self._agents.values())
213
214    def list_ids(self) -> list[str]:
215        """
216        List all registered agent IDs.
217
218        Returns:
219            List of agent IDs.
220        """
221        return list(self._agents.keys())
222
223    def set_default(self, agent_id: str) -> None:
224        """
225        Set the default agent.
226
227        Args:
228            agent_id: The agent ID to set as default.
229
230        Raises:
231            ValueError: If the agent is not registered.
232        """
233        if agent_id not in self._agents:
234            raise ValueError(f"Agent '{agent_id}' not registered")
235        self._default_agent_id = agent_id
236
237    @property
238    def default_agent_id(self) -> str | None:
239        """Get the default agent ID."""
240        return self._default_agent_id
241
242    def clear(self) -> None:
243        """Remove all registered agents."""
244        self._agents.clear()
245        self._default_agent_id = None
246
247    def __len__(self) -> int:
248        """Return the number of registered agents."""
249        return len(self._agents)
250
251    def __contains__(self, agent_id: str) -> bool:
252        """Check if an agent is registered."""
253        return agent_id in self._agents

Registry for managing multiple agent definitions.

Provides lookup, validation, and lifecycle management for agents.

AgentRegistry()
129    def __init__(self) -> None:
130        """Initialize an empty agent registry."""
131        self._agents: dict[str, AgentDefinition] = {}
132        self._default_agent_id: str | None = None

Initialize an empty agent registry.

def register(self, agent: AgentDefinition) -> None:
134    def register(self, agent: AgentDefinition) -> None:
135        """
136        Register an agent definition.
137
138        Args:
139            agent: The agent definition to register.
140
141        Raises:
142            ValueError: If an agent with the same ID already exists.
143        """
144        if agent.id in self._agents:
145            raise ValueError(f"Agent '{agent.id}' already registered")
146        self._agents[agent.id] = agent
147
148        # Set as default if it's the first agent
149        if self._default_agent_id is None:
150            self._default_agent_id = agent.id

Register an agent definition.

Args: agent: The agent definition to register.

Raises: ValueError: If an agent with the same ID already exists.

def unregister(self, agent_id: str) -> bool:
152    def unregister(self, agent_id: str) -> bool:
153        """
154        Unregister an agent.
155
156        Args:
157            agent_id: The agent ID to unregister.
158
159        Returns:
160            True if the agent was unregistered, False if not found.
161        """
162        if agent_id not in self._agents:
163            return False
164
165        del self._agents[agent_id]
166
167        # Clear default if we removed it
168        if self._default_agent_id == agent_id:
169            self._default_agent_id = next(iter(self._agents), None)
170
171        return True

Unregister an agent.

Args: agent_id: The agent ID to unregister.

Returns: True if the agent was unregistered, False if not found.

def get(self, agent_id: str) -> AgentDefinition | None:
173    def get(self, agent_id: str) -> AgentDefinition | None:
174        """
175        Get an agent definition by ID.
176
177        Args:
178            agent_id: The agent ID.
179
180        Returns:
181            The agent definition, or None if not found.
182        """
183        return self._agents.get(agent_id)

Get an agent definition by ID.

Args: agent_id: The agent ID.

Returns: The agent definition, or None if not found.

def get_or_default( self, agent_id: str | None = None) -> AgentDefinition | None:
185    def get_or_default(self, agent_id: str | None = None) -> AgentDefinition | None:
186        """
187        Get an agent by ID, or return the default agent.
188
189        Args:
190            agent_id: Optional agent ID. Uses default if None.
191
192        Returns:
193            The agent definition, or None if no agents registered.
194        """
195        if agent_id:
196            agent = self._agents.get(agent_id)
197            if agent:
198                return agent
199
200        if self._default_agent_id:
201            return self._agents.get(self._default_agent_id)
202
203        return None

Get an agent by ID, or return the default agent.

Args: agent_id: Optional agent ID. Uses default if None.

Returns: The agent definition, or None if no agents registered.

def list_agents(self) -> list[AgentDefinition]:
205    def list_agents(self) -> list[AgentDefinition]:
206        """
207        List all registered agents.
208
209        Returns:
210            List of all agent definitions.
211        """
212        return list(self._agents.values())

List all registered agents.

Returns: List of all agent definitions.

def list_ids(self) -> list[str]:
214    def list_ids(self) -> list[str]:
215        """
216        List all registered agent IDs.
217
218        Returns:
219            List of agent IDs.
220        """
221        return list(self._agents.keys())

List all registered agent IDs.

Returns: List of agent IDs.

def set_default(self, agent_id: str) -> None:
223    def set_default(self, agent_id: str) -> None:
224        """
225        Set the default agent.
226
227        Args:
228            agent_id: The agent ID to set as default.
229
230        Raises:
231            ValueError: If the agent is not registered.
232        """
233        if agent_id not in self._agents:
234            raise ValueError(f"Agent '{agent_id}' not registered")
235        self._default_agent_id = agent_id

Set the default agent.

Args: agent_id: The agent ID to set as default.

Raises: ValueError: If the agent is not registered.

default_agent_id: str | None
237    @property
238    def default_agent_id(self) -> str | None:
239        """Get the default agent ID."""
240        return self._default_agent_id

Get the default agent ID.

def clear(self) -> None:
242    def clear(self) -> None:
243        """Remove all registered agents."""
244        self._agents.clear()
245        self._default_agent_id = None

Remove all registered agents.

def get_agent_registry() -> AgentRegistry:
260def get_agent_registry() -> AgentRegistry:
261    """
262    Get or create the global agent registry.
263
264    .. deprecated::
265        Use AppContext.agent_registry instead. This function will be removed
266        in a future version.
267
268    Returns:
269        The global AgentRegistry instance.
270    """
271    import warnings
272
273    from tiphys.context import get_context, has_context
274
275    # If AppContext is available, use it (preferred path)
276    if has_context():
277        warnings.warn(
278            "get_agent_registry() is deprecated. Use AppContext.agent_registry instead.",
279            DeprecationWarning,
280            stacklevel=2,
281        )
282        return get_context().agent_registry
283
284    # Fallback to global singleton for backwards compatibility
285    global _agent_registry
286    if _agent_registry is None:
287        _agent_registry = AgentRegistry()
288    return _agent_registry

Get or create the global agent registry.

Deprecated since version : Use AppContext.agent_registry instead. This function will be removed in a future version.

Returns: The global AgentRegistry instance.

def create_default_agent( agent_id: str = 'default', name: str = 'Tiphys', **kwargs: Any) -> AgentDefinition:
291def create_default_agent(
292    agent_id: str = "default",
293    name: str = "Tiphys",
294    **kwargs: Any,
295) -> AgentDefinition:
296    """
297    Create a default agent definition.
298
299    Args:
300        agent_id: Agent identifier.
301        name: Display name.
302        **kwargs: Additional configuration.
303
304    Returns:
305        A new AgentDefinition.
306    """
307    return AgentDefinition(
308        id=agent_id,
309        name=name,
310        **kwargs,
311    )

Create a default agent definition.

Args: agent_id: Agent identifier. name: Display name. **kwargs: Additional configuration.

Returns: A new AgentDefinition.

class RouteResolver:
 18class RouteResolver:
 19    """
 20    Resolves routing contexts to agents.
 21
 22    Evaluates bindings in priority order and returns the matching agent.
 23    """
 24
 25    def __init__(
 26        self,
 27        agent_registry: AgentRegistry | None = None,
 28        default_agent_id: str | None = None,
 29    ) -> None:
 30        """
 31        Initialize the route resolver.
 32
 33        Args:
 34            agent_registry: Agent registry. Uses global registry if not provided.
 35            default_agent_id: Default agent ID for fallback.
 36        """
 37        self.agent_registry = agent_registry or get_agent_registry()
 38        self._bindings: list[AgentBinding] = []
 39        self._default_agent_id = default_agent_id
 40
 41    def add_binding(self, binding: AgentBinding) -> None:
 42        """
 43        Add a routing binding.
 44
 45        Args:
 46            binding: The binding to add.
 47        """
 48        self._bindings.append(binding)
 49        # Keep sorted by priority (highest first)
 50        self._bindings.sort(key=lambda b: b.priority, reverse=True)
 51
 52    def remove_binding(self, binding: AgentBinding) -> bool:
 53        """
 54        Remove a routing binding.
 55
 56        Args:
 57            binding: The binding to remove.
 58
 59        Returns:
 60            True if removed, False if not found.
 61        """
 62        try:
 63            self._bindings.remove(binding)
 64            return True
 65        except ValueError:
 66            return False
 67
 68    def clear_bindings(self) -> None:
 69        """Remove all bindings."""
 70        self._bindings.clear()
 71
 72    def list_bindings(self) -> list[AgentBinding]:
 73        """
 74        Get all bindings sorted by priority.
 75
 76        Returns:
 77            List of bindings.
 78        """
 79        return list(self._bindings)
 80
 81    def resolve(self, context: RoutingContext) -> AgentDefinition | None:
 82        """
 83        Resolve a routing context to an agent.
 84
 85        Evaluates bindings in priority order until a match is found.
 86        Falls back to default agent if no binding matches.
 87
 88        Args:
 89            context: The routing context.
 90
 91        Returns:
 92            The resolved agent, or None if no agent found.
 93        """
 94        # Try to find a matching binding
 95        for binding in self._bindings:
 96            if binding.matches(
 97                peer_id=context.peer_id,
 98                group_id=context.group_id,
 99                channel_name=context.channel_name,
100            ):
101                agent = self.agent_registry.get(binding.agent_id)
102                if agent and agent.enabled:
103                    logger.debug(
104                        "Route resolved via binding",
105                        binding_type=binding.binding_type.value,
106                        agent_id=agent.id,
107                        peer_id=context.peer_id,
108                        group_id=context.group_id,
109                    )
110                    return agent
111
112        # Fall back to default
113        default_id = self._default_agent_id or self.agent_registry.default_agent_id
114        if default_id:
115            agent = self.agent_registry.get(default_id)
116            if agent and agent.enabled:
117                logger.debug(
118                    "Route resolved to default agent",
119                    agent_id=agent.id,
120                    peer_id=context.peer_id,
121                )
122                return agent
123
124        # Try any enabled agent
125        for agent in self.agent_registry.list_agents():
126            if agent.enabled:
127                logger.debug(
128                    "Route resolved to first available agent",
129                    agent_id=agent.id,
130                )
131                return agent
132
133        return None
134
135    def resolve_agent_id(self, context: RoutingContext) -> str | None:
136        """
137        Resolve a routing context to an agent ID.
138
139        Args:
140            context: The routing context.
141
142        Returns:
143            The resolved agent ID, or None.
144        """
145        agent = self.resolve(context)
146        return agent.id if agent else None
147
148    def set_default_agent(self, agent_id: str) -> None:
149        """
150        Set the default agent for this resolver.
151
152        Args:
153            agent_id: The agent ID.
154        """
155        self._default_agent_id = agent_id

Resolves routing contexts to agents.

Evaluates bindings in priority order and returns the matching agent.

RouteResolver( agent_registry: AgentRegistry | None = None, default_agent_id: str | None = None)
25    def __init__(
26        self,
27        agent_registry: AgentRegistry | None = None,
28        default_agent_id: str | None = None,
29    ) -> None:
30        """
31        Initialize the route resolver.
32
33        Args:
34            agent_registry: Agent registry. Uses global registry if not provided.
35            default_agent_id: Default agent ID for fallback.
36        """
37        self.agent_registry = agent_registry or get_agent_registry()
38        self._bindings: list[AgentBinding] = []
39        self._default_agent_id = default_agent_id

Initialize the route resolver.

Args: agent_registry: Agent registry. Uses global registry if not provided. default_agent_id: Default agent ID for fallback.

agent_registry
def add_binding(self, binding: AgentBinding) -> None:
41    def add_binding(self, binding: AgentBinding) -> None:
42        """
43        Add a routing binding.
44
45        Args:
46            binding: The binding to add.
47        """
48        self._bindings.append(binding)
49        # Keep sorted by priority (highest first)
50        self._bindings.sort(key=lambda b: b.priority, reverse=True)

Add a routing binding.

Args: binding: The binding to add.

def remove_binding(self, binding: AgentBinding) -> bool:
52    def remove_binding(self, binding: AgentBinding) -> bool:
53        """
54        Remove a routing binding.
55
56        Args:
57            binding: The binding to remove.
58
59        Returns:
60            True if removed, False if not found.
61        """
62        try:
63            self._bindings.remove(binding)
64            return True
65        except ValueError:
66            return False

Remove a routing binding.

Args: binding: The binding to remove.

Returns: True if removed, False if not found.

def clear_bindings(self) -> None:
68    def clear_bindings(self) -> None:
69        """Remove all bindings."""
70        self._bindings.clear()

Remove all bindings.

def list_bindings(self) -> list[AgentBinding]:
72    def list_bindings(self) -> list[AgentBinding]:
73        """
74        Get all bindings sorted by priority.
75
76        Returns:
77            List of bindings.
78        """
79        return list(self._bindings)

Get all bindings sorted by priority.

Returns: List of bindings.

def resolve( self, context: RoutingContext) -> AgentDefinition | None:
 81    def resolve(self, context: RoutingContext) -> AgentDefinition | None:
 82        """
 83        Resolve a routing context to an agent.
 84
 85        Evaluates bindings in priority order until a match is found.
 86        Falls back to default agent if no binding matches.
 87
 88        Args:
 89            context: The routing context.
 90
 91        Returns:
 92            The resolved agent, or None if no agent found.
 93        """
 94        # Try to find a matching binding
 95        for binding in self._bindings:
 96            if binding.matches(
 97                peer_id=context.peer_id,
 98                group_id=context.group_id,
 99                channel_name=context.channel_name,
100            ):
101                agent = self.agent_registry.get(binding.agent_id)
102                if agent and agent.enabled:
103                    logger.debug(
104                        "Route resolved via binding",
105                        binding_type=binding.binding_type.value,
106                        agent_id=agent.id,
107                        peer_id=context.peer_id,
108                        group_id=context.group_id,
109                    )
110                    return agent
111
112        # Fall back to default
113        default_id = self._default_agent_id or self.agent_registry.default_agent_id
114        if default_id:
115            agent = self.agent_registry.get(default_id)
116            if agent and agent.enabled:
117                logger.debug(
118                    "Route resolved to default agent",
119                    agent_id=agent.id,
120                    peer_id=context.peer_id,
121                )
122                return agent
123
124        # Try any enabled agent
125        for agent in self.agent_registry.list_agents():
126            if agent.enabled:
127                logger.debug(
128                    "Route resolved to first available agent",
129                    agent_id=agent.id,
130                )
131                return agent
132
133        return None

Resolve a routing context to an agent.

Evaluates bindings in priority order until a match is found. Falls back to default agent if no binding matches.

Args: context: The routing context.

Returns: The resolved agent, or None if no agent found.

def resolve_agent_id(self, context: RoutingContext) -> str | None:
135    def resolve_agent_id(self, context: RoutingContext) -> str | None:
136        """
137        Resolve a routing context to an agent ID.
138
139        Args:
140            context: The routing context.
141
142        Returns:
143            The resolved agent ID, or None.
144        """
145        agent = self.resolve(context)
146        return agent.id if agent else None

Resolve a routing context to an agent ID.

Args: context: The routing context.

Returns: The resolved agent ID, or None.

def set_default_agent(self, agent_id: str) -> None:
148    def set_default_agent(self, agent_id: str) -> None:
149        """
150        Set the default agent for this resolver.
151
152        Args:
153            agent_id: The agent ID.
154        """
155        self._default_agent_id = agent_id

Set the default agent for this resolver.

Args: agent_id: The agent ID.

def get_route_resolver() -> RouteResolver:
162def get_route_resolver() -> RouteResolver:
163    """
164    Get or create the global route resolver.
165
166    .. deprecated::
167        Use AppContext.route_resolver instead. This function will be removed
168        in a future version.
169
170    Returns:
171        The global RouteResolver instance.
172    """
173    import warnings
174
175    from tiphys.context import get_context, has_context
176
177    # If AppContext is available, use it (preferred path)
178    if has_context():
179        warnings.warn(
180            "get_route_resolver() is deprecated. Use AppContext.route_resolver instead.",
181            DeprecationWarning,
182            stacklevel=2,
183        )
184        return get_context().route_resolver  # type: ignore[return-value]
185
186    # Fallback to global singleton for backwards compatibility
187    global _route_resolver
188    if _route_resolver is None:
189        _route_resolver = RouteResolver()
190    return _route_resolver

Get or create the global route resolver.

Deprecated since version : Use AppContext.route_resolver instead. This function will be removed in a future version.

Returns: The global RouteResolver instance.

def resolve_agent( peer_id: str | None = None, group_id: str | None = None, channel_name: str | None = None, **metadata: Any) -> AgentDefinition | None:
193def resolve_agent(
194    peer_id: str | None = None,
195    group_id: str | None = None,
196    channel_name: str | None = None,
197    **metadata: Any,
198) -> AgentDefinition | None:
199    """
200    Convenience function to resolve an agent from context.
201
202    Args:
203        peer_id: The peer ID.
204        group_id: The group ID.
205        channel_name: The channel name.
206        **metadata: Extra context metadata.
207
208    Returns:
209        The resolved agent, or None.
210    """
211    context = RoutingContext(
212        peer_id=peer_id,
213        group_id=group_id,
214        channel_name=channel_name,
215        metadata=metadata,
216    )
217    return get_route_resolver().resolve(context)

Convenience function to resolve an agent from context.

Args: peer_id: The peer ID. group_id: The group ID. channel_name: The channel name. **metadata: Extra context metadata.

Returns: The resolved agent, or None.