Skip to content

fix(engine,condition): stop starving a downstream node after a single-branch condition#5

Merged
graycyrus merged 1 commit into
mainfrom
fix/condition-single-edge-filter
Jul 8, 2026
Merged

fix(engine,condition): stop starving a downstream node after a single-branch condition#5
graycyrus merged 1 commit into
mainfrom
fix/condition-single-edge-filter

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Bug: a single-branch condition silently starves its downstream node

Two coupled bugs made a downstream node (e.g. an agent with input_context:"=item") receive null when its predecessor was a condition:

A1 — engine drops items on a single-branch condition (engine.rs, HandlerRouting::Plain single-edge branch). A condition with only a true edge was lowered as an unconditional add_edge (ignoring from_port), so the successor ran on every execution — but collect_input port-matches true vs the emitted false and drops the items, so the successor ran with empty input. Fix: when the single outgoing edge's from_port != "main", lower it as a conditional edge with an END fallback — a true-only condition now acts as a filter (runs with items on true, terminates to END on false).

A2 — condition node didn't resolve =-expressions (nodes/control_flow/condition.rs). field was read as a raw literal key, so field:"=item.assignee" did get("=item.assignee") → always None → always false. Fix: if field is_expression(), resolve it and check the resolved value's truthiness; non-expression fields keep the literal-key path (backward compatible).

Tests

condition_single_true_edge_filters_on_false, condition_single_true_edge_item_flows_through, expression_in_field_resolves_and_checks_truthiness. Full suite green (cargo test 304 lib + 12 doctests, --all-features e2e all pass).

Summary by CodeRabbit

  • New Features

    • Condition steps now accept either a plain field name or an expression-based field value, making routing decisions more flexible.
    • Downstream steps now only run when a condition actually takes the matching branch, preventing unintended execution on skipped paths.
  • Bug Fixes

    • Fixed cases where a single outgoing “true” or similar branch could incorrectly continue as if it were unconditional.
    • Prevented empty or null inputs from appearing in downstream branches when a condition is not met.
  • Tests

    • Added regression coverage for conditional routing and branch behavior with per-item flows.

…ition

B15: a condition node wired with only a `true` edge (no `false` edge — the
common gate/filter shape) was lowered as an UNCONDITIONAL plain edge in
build_graph, so the successor ran on every execution regardless of which
port the condition actually emitted. collect_input then port-matched the
edge's `true` against the emitted `false` and silently dropped the items,
so the successor ran with an EMPTY input and downstream `=item`/`=item.<field>`
expressions resolved to null instead of the run simply ending.

Fix: when a node's single outgoing edge has from_port != "main", lower it as
a conditional edge (mirroring the existing multi-edge branch) whose router
returns from_port when the emitted port matches, and END otherwise. A
`true`-only condition now behaves as a FILTER: it runs the successor with
items on `true`, and terminates the run to END on `false`.

Also fixes condition.rs: `field` was read as a raw literal key, so an
`=`-expression field (e.g. `field: "=item.assignee"`) was looked up as a
literal key named "=item.assignee" and always missed, always routing
`false`. Now an expression field is resolved against the node's expression
scope first and the resolved value's truthiness is checked directly;
non-expression fields keep the existing literal-key-lookup behavior.

Tests: condition_single_true_edge_filters_on_false,
condition_single_true_edge_item_flows_through (engine.rs),
expression_in_field_resolves_and_checks_truthiness (condition.rs). All
existing condition/engine tests remain green.
@graycyrus graycyrus merged commit 56d81df into main Jul 8, 2026
0 of 2 checks passed
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 196494bc-12a0-49d8-8edb-614069c1d8d0

📥 Commits

Reviewing files that changed from the base of the PR and between eb4865d and 48e51f4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • src/engine.rs
  • src/nodes/control_flow/condition.rs

📝 Walkthrough

Walkthrough

Graph lowering in engine.rs now emits conditional edges (instead of unconditional edges) for nodes with a single outgoing edge on a non-"main" port, routing based on the emitted port or terminating at END. ConditionNode's field evaluation now supports =-prefixed expressions resolved via expression scope, alongside existing key lookup. New tests cover both changes.

Changes

Condition Node Filtering Fix

Layer / File(s) Summary
Expression-based field evaluation
src/nodes/control_flow/condition.rs
ConditionNode.execute detects =-prefixed expression fields, resolves them via expression scope, and truthiness-checks the result; plain string fields still use direct key lookup with missing keys treated as null. New test verifies routing for non-empty, empty, and missing resolved values.
Conditional lowering for single non-main edges
src/engine.rs
Graph lowering now emits conditional edges (based on the node's emitted port) instead of unconditional edges when a node has exactly one outgoing edge on a non-"main" port, terminating at END on mismatch. New regression tests verify filter-like condition nodes correctly gate downstream execution, including with upstream fan-out.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant ConditionNode
  participant Successor

  Engine->>ConditionNode: execute(item)
  ConditionNode->>ConditionNode: resolve field (expression or key lookup)
  ConditionNode-->>Engine: emitted port ("true"/"false")
  Engine->>Engine: compare emitted port to edge.from_port
  alt port matches edge
    Engine->>Successor: run with item
  else port does not match
    Engine->>Engine: terminate to END
  end
Loading

Poem

A hop, a check, a truthy field,
No more phantom runs revealed,
Ports now matched with careful care,
Filters filter, fair and square,
This rabbit thumps with pure delight! 🐰✅

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant