UAT: message_router node ignores spec-required rule types (prefix, contains, suffix) — only evaluates field/equals conditions #3658

Open
opened 2026-04-05 21:11:43 +00:00 by freemo · 0 comments
Owner

Metadata

  • Branch: fix/langgraph-message-router-rule-types
  • Commit Message: fix(langgraph): implement prefix/contains/suffix rule types in message_router node
  • Milestone: v3.6.0
  • Parent Epic: #366

Background

Per docs/specification.md (lines 20925–20958), the message_router node type routes messages using a rules-based system with three match types: prefix, contains, and suffix. Each rule has a type field specifying the match strategy.

Current Behavior

The _execute_message_router() method in src/cleveragents/langgraph/nodes.py (lines 311–326) completely ignores the type field of each rule. It only evaluates conditions using _eval_condition() which only supports equals and field comparisons:

async def _execute_message_router(self, state: GraphState) -> dict[str, Any]:
    rules = self.config.metadata.get("rules", [])
    current_msg = state.metadata.get("current_message") if state.metadata else None
    routed_to = None
    for rule in rules:
        cond = rule.get("condition")
        target = rule.get("target")
        if target is None:
            continue
        if cond is None:
            routed_to = target
            break
        if self._eval_condition(current_msg, cond):
            routed_to = target
            break
    return {"metadata": {"routed_to": routed_to, "current_message": current_msg}}

The _eval_condition() method (lines 328–333) only handles equals and field comparisons:

def _eval_condition(self, message: Any, condition: dict[str, Any]) -> bool:
    if "equals" in condition:
        return message == condition["equals"]
    if "field" in condition and isinstance(message, dict):
        return message.get(condition["field"]) == condition.get("value")
    return False

Expected Behavior (per spec)

The spec defines three rule types that MUST be supported:

Rule Type Behavior
prefix Match if message content starts with match string
contains Match if message content contains match string
suffix Match if message content ends with match string (empty string = catch-all)

Additionally, the spec defines strip_match: true to strip the matched prefix/suffix from the message before forwarding.

The spec also defines routing prefixes like GOTO_<NODE>:, SET_<FIELD>:, ROUTE_<TARGET>: that actors use for inter-actor communication — these are all prefix-based and require prefix rule type support.

Code Location

  • src/cleveragents/langgraph/nodes.py lines 311–333: _execute_message_router() and _eval_condition()

Steps to Reproduce

from cleveragents.langgraph.nodes import Node, NodeConfig, NodeType
from cleveragents.langgraph.state import GraphState

# Create a message_router node with prefix rules
config = NodeConfig(
    name="router",
    type=NodeType.MESSAGE_ROUTER,
    metadata={
        "rules": [
            {"type": "prefix", "match": "GOTO_BRAINSTORMING", "target": "brainstorming", "strip_match": True},
            {"type": "contains", "match": "SET_TOPIC:", "target": "discovery"},
            {"type": "suffix", "match": "", "target": "workflow_controller"},  # catch-all
        ]
    }
)
node = Node(config)
state = GraphState(
    messages=[],
    metadata={"current_message": "GOTO_BRAINSTORMING: Start the brainstorm"}
)
import asyncio
result = asyncio.run(node.execute(state))
# Expected: result["metadata"]["routed_to"] == "brainstorming"
# Actual: result["metadata"]["routed_to"] == None (no rule matched because prefix type is ignored)

Impact

All inter-actor communication via routing prefixes (GOTO_<NODE>:, SET_<FIELD>:, ROUTE_<TARGET>:, COMMAND_OUTPUT:, DISCOVERY_RESPONSE:) is broken. Any actor graph that uses message_router nodes with prefix, contains, or suffix rules will silently fail to route messages.

Subtasks

  • Implement prefix rule type: match if current_message.startswith(rule["match"])
  • Implement contains rule type: match if rule["match"] in current_message
  • Implement suffix rule type: match if current_message.endswith(rule["match"]) (empty string = catch-all)
  • Implement strip_match support: strip matched prefix/suffix from message before forwarding
  • Write Behave unit tests for all three rule types and strip_match behavior
  • Verify all nox stages pass; coverage ≥ 97%

Definition of Done

  • All three rule types (prefix, contains, suffix) work correctly
  • strip_match: true strips the matched pattern from the forwarded message
  • Empty suffix match acts as a catch-all
  • Unit tests pass for all rule types
  • All nox stages pass
  • Coverage ≥ 97%

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `fix/langgraph-message-router-rule-types` - **Commit Message**: `fix(langgraph): implement prefix/contains/suffix rule types in message_router node` - **Milestone**: v3.6.0 - **Parent Epic**: #366 ## Background Per `docs/specification.md` (lines 20925–20958), the `message_router` node type routes messages using a rules-based system with three match types: `prefix`, `contains`, and `suffix`. Each rule has a `type` field specifying the match strategy. ## Current Behavior The `_execute_message_router()` method in `src/cleveragents/langgraph/nodes.py` (lines 311–326) completely ignores the `type` field of each rule. It only evaluates conditions using `_eval_condition()` which only supports `equals` and `field` comparisons: ```python async def _execute_message_router(self, state: GraphState) -> dict[str, Any]: rules = self.config.metadata.get("rules", []) current_msg = state.metadata.get("current_message") if state.metadata else None routed_to = None for rule in rules: cond = rule.get("condition") target = rule.get("target") if target is None: continue if cond is None: routed_to = target break if self._eval_condition(current_msg, cond): routed_to = target break return {"metadata": {"routed_to": routed_to, "current_message": current_msg}} ``` The `_eval_condition()` method (lines 328–333) only handles `equals` and `field` comparisons: ```python def _eval_condition(self, message: Any, condition: dict[str, Any]) -> bool: if "equals" in condition: return message == condition["equals"] if "field" in condition and isinstance(message, dict): return message.get(condition["field"]) == condition.get("value") return False ``` ## Expected Behavior (per spec) The spec defines three rule types that MUST be supported: | Rule Type | Behavior | |-----------|----------| | `prefix` | Match if message content starts with `match` string | | `contains` | Match if message content contains `match` string | | `suffix` | Match if message content ends with `match` string (empty string = catch-all) | Additionally, the spec defines `strip_match: true` to strip the matched prefix/suffix from the message before forwarding. The spec also defines routing prefixes like `GOTO_<NODE>:`, `SET_<FIELD>:`, `ROUTE_<TARGET>:` that actors use for inter-actor communication — these are all prefix-based and require `prefix` rule type support. ## Code Location - `src/cleveragents/langgraph/nodes.py` lines 311–333: `_execute_message_router()` and `_eval_condition()` ## Steps to Reproduce ```python from cleveragents.langgraph.nodes import Node, NodeConfig, NodeType from cleveragents.langgraph.state import GraphState # Create a message_router node with prefix rules config = NodeConfig( name="router", type=NodeType.MESSAGE_ROUTER, metadata={ "rules": [ {"type": "prefix", "match": "GOTO_BRAINSTORMING", "target": "brainstorming", "strip_match": True}, {"type": "contains", "match": "SET_TOPIC:", "target": "discovery"}, {"type": "suffix", "match": "", "target": "workflow_controller"}, # catch-all ] } ) node = Node(config) state = GraphState( messages=[], metadata={"current_message": "GOTO_BRAINSTORMING: Start the brainstorm"} ) import asyncio result = asyncio.run(node.execute(state)) # Expected: result["metadata"]["routed_to"] == "brainstorming" # Actual: result["metadata"]["routed_to"] == None (no rule matched because prefix type is ignored) ``` ## Impact All inter-actor communication via routing prefixes (`GOTO_<NODE>:`, `SET_<FIELD>:`, `ROUTE_<TARGET>:`, `COMMAND_OUTPUT:`, `DISCOVERY_RESPONSE:`) is broken. Any actor graph that uses `message_router` nodes with `prefix`, `contains`, or `suffix` rules will silently fail to route messages. ## Subtasks - [ ] Implement `prefix` rule type: match if `current_message.startswith(rule["match"])` - [ ] Implement `contains` rule type: match if `rule["match"] in current_message` - [ ] Implement `suffix` rule type: match if `current_message.endswith(rule["match"])` (empty string = catch-all) - [ ] Implement `strip_match` support: strip matched prefix/suffix from message before forwarding - [ ] Write Behave unit tests for all three rule types and `strip_match` behavior - [ ] Verify all nox stages pass; coverage ≥ 97% ## Definition of Done - [ ] All three rule types (`prefix`, `contains`, `suffix`) work correctly - [ ] `strip_match: true` strips the matched pattern from the forwarded message - [ ] Empty `suffix` match acts as a catch-all - [ ] Unit tests pass for all rule types - [ ] All nox stages pass - [ ] Coverage ≥ 97% --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
freemo added this to the v3.6.0 milestone 2026-04-05 21:11:48 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
#366 Epic: Post-MVP Deferred Work
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#3658
No description provided.