Skip to content

feat(libsy): stage router (signals only)#135

Open
messiaen wants to merge 1 commit into
mainfrom
grclark/libsy-stage-router
Open

feat(libsy): stage router (signals only)#135
messiaen wants to merge 1 commit into
mainfrom
grclark/libsy-stage-router

Conversation

@messiaen

@messiaen messiaen commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

  • Implements stage_router tier_picker piece as a Classifier in libsy.
  • No llm fallback yet, once we have llm_classifier processor / classifier, we can join them with fall_through
  • bridge for switchyard-components

How tested

  • uv run ruff check . clean
  • uv run mypy switchyard clean
  • uv run pytest tests/ green
  • Manual smoke (describe what was run)

Checklist

  • One class per file; filename = snake_case of the primary class.
  • New public symbols exported from switchyard/__init__.py.__all__ if intended for downstream use.
  • Unit tests added for new components / bug fixes.
  • README / --help updated if customer-facing surface changed.
  • Commits signed off (Signed-off-by: Your Name <email>) per the DCO.

Notes for reviewers

Anything reviewers should pay extra attention to — risky paths, follow-up tickets, intentional trade-offs.

Summary by CodeRabbit

  • New Features
    • Added automatic stage routing that selects efficient or capable processing based on request signals.
    • Added escalation for critical conditions and de-escalation when work is settled.
    • Added confidence-based fallback classification when routing signals are inconclusive.
    • Added processing to capture recent tool activity for routing decisions.
  • API Updates
    • Exposed stage-routing scores, dimensions, outcomes, and tier-selection utilities through the library interface.

@messiaen messiaen self-assigned this Jul 24, 2026
@messiaen messiaen changed the title Grclark/libsy stage router feat(libsy): stage router (signals only) Jul 24, 2026
@sabhatinas
sabhatinas force-pushed the sabhatinas/stage-router-scorer-corroborative-tanh branch from 7cb8a7b to 96fc5e3 Compare July 24, 2026 18:22
Base automatically changed from sabhatinas/stage-router-scorer-corroborative-tanh to main July 24, 2026 18:27
@messiaen
messiaen force-pushed the grclark/libsy-stage-router branch from c8d9195 to ac31ad5 Compare July 24, 2026 18:43
@sabhatinas

Copy link
Copy Markdown
Contributor

Will you also be updating docs?

@messiaen

Copy link
Copy Markdown
Contributor Author

Will you also be updating docs?

My plan was to update the user docs when we have the full router working.

@messiaen
messiaen marked this pull request as ready for review July 24, 2026 20:47
@messiaen
messiaen requested a review from a team as a code owner July 24, 2026 20:47
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Stage Router

Layer / File(s) Summary
Tool signal state plumbing
crates/libsy/src/algorithms/util.rs, crates/libsy/src/algorithms/util/tool_signals.rs, crates/libsy/src/core/state.rs
Registers the new modules, stores optional ToolSignals in State, and processes request events into signal state.
Signal scoring model
crates/libsy/src/algorithms/util/stage_router.rs
Defines routing types, projects signals into coding dimensions, and calculates signed scores with confidence.
Tier decisions and classifier integration
crates/libsy/src/algorithms/util/stage_router.rs
Applies override, threshold, and fall-open routing rules, integrates StageClassifier, and adds unit tests.
Public API forwarding
crates/libsy/src/lib.rs, crates/switchyard-components/src/stage_router.rs
Publishes the libsy stage-router API and forwards the components API to it.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

I’m a rabbit routing through the code,
With signals tucked beneath my load.
Strong or weak, the tiers now know,
Tests can make the pathway flow.
Hop, hop—exports bloom in sight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: adding the libsy stage router with signals-only behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/libsy/src/algorithms/util/stage_router.rs`:
- Line 310: Make StageClassifier public and add its documentation in
crates/libsy/src/algorithms/util/stage_router.rs:310-310, then re-export it from
stage_router in crates/libsy/src/lib.rs:92-96 and forward that export through
the components bridge in crates/switchyard-components/src/stage_router.rs:12-15
so consumers can construct and register it.
- Around line 329-359: Update the PickOutcome::Resolved and
PickOutcome::ConsultClassifier branches to use the outcome-provided confidence
for resolved selections instead of deriving it from signed score. Keep fall-open
results genuinely ambiguous: do not remap the signed score onto default_tier or
emit high confidence for the default weak target, while preserving
default_target state handling.

In `@crates/libsy/src/algorithms/util/tool_signals.rs`:
- Around line 259-262: Add /// documentation for ToolSignalProcessor and its
recent_window field in crates/libsy/src/algorithms/util/tool_signals.rs:259-262.
Add a /// comment in crates/libsy/src/core/state.rs:24 describing the lifecycle
and absence semantics of tool_signals, and document the public stage_router
module in crates/libsy/src/lib.rs:92; use Rust documentation comments for all
three public APIs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d6162c00-6a3b-48de-ae14-2c8c1702ca6b

📥 Commits

Reviewing files that changed from the base of the PR and between 3269dce and ac31ad5.

📒 Files selected for processing (7)
  • crates/libsy/src/algorithms/util.rs
  • crates/libsy/src/algorithms/util/stage_router.rs
  • crates/libsy/src/algorithms/util/tool_signals.rs
  • crates/libsy/src/core/state.rs
  • crates/libsy/src/lib.rs
  • crates/libsy/src/signal.rs
  • crates/switchyard-components/src/stage_router.rs
💤 Files with no reviewable changes (1)
  • crates/libsy/src/signal.rs

}
}

struct StageClassifier {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Expose the classifier through the intended bridge. StageClassifier is private inside a pub(crate) module, and neither forwarding layer exports it. No consumer can construct or register this Classifier; only in-module tests can execute it.

  • crates/libsy/src/algorithms/util/stage_router.rs#L310-L310: make StageClassifier public and document it.
  • crates/libsy/src/lib.rs#L92-L96: re-export StageClassifier from stage_router.
  • crates/switchyard-components/src/stage_router.rs#L12-L15: forward StageClassifier through the components bridge.
📍 Affects 3 files
  • crates/libsy/src/algorithms/util/stage_router.rs#L310-L310 (this comment)
  • crates/libsy/src/lib.rs#L92-L96
  • crates/switchyard-components/src/stage_router.rs#L12-L15
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy/src/algorithms/util/stage_router.rs` at line 310, Make
StageClassifier public and add its documentation in
crates/libsy/src/algorithms/util/stage_router.rs:310-310, then re-export it from
stage_router in crates/libsy/src/lib.rs:92-96 and forward that export through
the components bridge in crates/switchyard-components/src/stage_router.rs:12-15
so consumers can construct and register it.

Comment on lines +329 to +359
PickOutcome::Resolved { tier, score, .. } => {
let target = match tier {
Tier::Capable => "strong",
Tier::Efficient => "weak",
};
let conf = (score + 1.0) / 2.0;
// TODO add the non-target to this score set?
Ok(Classification::Scores(vec![Score {
target: target.to_string(),
confidence: conf,
}]))
}
PickOutcome::ConsultClassifier {
score,
default_tier,
..
} => {
let target = match default_tier {
Tier::Capable => "strong",
Tier::Efficient => "weak",
};
state.extra.insert(
"default_target".to_string(),
StateValue::String(target.to_string()),
);
let conf = (score + 1.0) / 2.0;
// TODO add the non-target to this score set?
Ok(Classification::Ambiguous(vec![Score {
target: target.to_string(),
confidence: conf,
}]))

@coderabbitai coderabbitai Bot Jul 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve confidence for the selected target.

score is signed toward Capable, but target switches to "weak" for negative scores. A -0.6 resolved weak result is emitted with 0.2 confidence, while hard override/de-escalation outcomes with score = 0.0 emit 0.5 despite being resolved. Fall-open can also report high confidence for the default weak target when the signal points capable. Use PickOutcome’s confidence for resolved outcomes and represent fall-open as genuinely ambiguous rather than remapping the signed score.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy/src/algorithms/util/stage_router.rs` around lines 329 - 359,
Update the PickOutcome::Resolved and PickOutcome::ConsultClassifier branches to
use the outcome-provided confidence for resolved selections instead of deriving
it from signed score. Keep fall-open results genuinely ambiguous: do not remap
the signed score onto default_tier or emit high confidence for the default weak
target, while preserving default_target state handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confidence is just the magnitude of the score. The logic is correct here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@messiaen, agreed that confidence is the score’s magnitude—that is exactly why the current mapping is problematic.

StageClassifier::score emits (score + 1.0) / 2.0, which is not the magnitude:

  • score = -0.6 selects "weak" but emits 0.2, rather than 0.6.
  • An override has score = 0.0 and provided confidence Some(1.0), but emits 0.5.
  • A fall-open positive score can still default to "weak" under EfficientFirst, yet the mapping reports a high confidence for that default target.

So the concern is not with confidence = score.abs(); it is that Lines 342 and 359 remap the signed score instead of using the outcome confidence (and, for fall-open, associate directional evidence with the default tier).

Comment on lines +259 to +262
#[derive(Debug, Clone)]
pub struct ToolSignalProcessor {
pub recent_window: usize,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new public Rust API.

  • crates/libsy/src/algorithms/util/tool_signals.rs#L259-L262: add /// docs for ToolSignalProcessor and recent_window.
  • crates/libsy/src/core/state.rs#L24-L24: add a /// doc explaining the lifecycle and absence semantics of tool_signals.
  • crates/libsy/src/lib.rs#L92-L92: add a /// doc comment for the public stage_router module.

As per coding guidelines, “Use /// documentation comments for public Rust items.”

📍 Affects 3 files
  • crates/libsy/src/algorithms/util/tool_signals.rs#L259-L262 (this comment)
  • crates/libsy/src/core/state.rs#L24-L24
  • crates/libsy/src/lib.rs#L92-L92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy/src/algorithms/util/tool_signals.rs` around lines 259 - 262, Add
/// documentation for ToolSignalProcessor and its recent_window field in
crates/libsy/src/algorithms/util/tool_signals.rs:259-262. Add a /// comment in
crates/libsy/src/core/state.rs:24 describing the lifecycle and absence semantics
of tool_signals, and document the public stage_router module in
crates/libsy/src/lib.rs:92; use Rust documentation comments for all three public
APIs.

Source: Coding guidelines

@@ -0,0 +1,527 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make the structure src/algorithsm//*.rs
with src/algorithsm/.rs

@messiaen messiaen Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal was to separate the public stuff (algorithms) and the non-public (util).

The public part of this would be some instantiation FallThrough ... probably.

//! Stage-router tier selection — thin re-export of libsy's decision core.
//!
//! Decides whether a coding-agent turn goes to the **capable** (strong) or
//! **efficient** (weak) tier from its [`ToolResultSignal`]. Single source of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also replace/delete these stage router files in switchyard-components

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only dependency is dimension_collector I think. Which I don't think we need to move.

tool_signal was moved previous PR

pub struct ToolSignalProcessor {
pub recent_window: usize,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also replace any parsing happening here, to switchyard-protocol/IR
too much API specific parsing happening here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. I think some of it is necessary. @sabhatinas can clarify.

Let's do this is a separate PR.

Signed-off-by: Greg Clark <grclark@nvidia.com>

feat(libsy): bridge back to components for stage_router

Signed-off-by: Greg Clark <grclark@nvidia.com>
@messiaen
messiaen force-pushed the grclark/libsy-stage-router branch from ac31ad5 to 0775ad6 Compare July 24, 2026 20:58
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.

3 participants