Skip to content

feat(libsy): stage router with handoff notes, per-tier prompts, and LLM fallback - #170

Open
sabhatinas wants to merge 1 commit into
mainfrom
sabhatinas/switch-982-handoff-notes-in-stage-classifier
Open

feat(libsy): stage router with handoff notes, per-tier prompts, and LLM fallback#170
sabhatinas wants to merge 1 commit into
mainfrom
sabhatinas/switch-982-handoff-notes-in-stage-classifier

Conversation

@sabhatinas

@sabhatinas sabhatinas commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds StageRouter — signal-driven strong/weak routing for coding agents, assembled from libsy parts rather than a new algorithm:

tool signals → capability classifier (optional) → default tier

Signals decide most turns for free. Undecided ones go to the capability classifier, reused as-is from the llm_classifier route. Anything still undecided lands on the picker's default. Along the way the router can hand the chosen model a handoff note explaining why it got the turn, and a per-tier system prompt.

This brings the Rust router to feature parity with the stage_router YAML profiles, closing the last two open items on SWITCH-1110 (handoff-note injection and per-tier prompts).

What's new

  • Handoff notes reach the model — previously parked in session state for a step that never existed.
  • A system prompt per tier, ahead of the client's own, on every turn that tier serves.
  • Stage signals and the capability classifier run as one chain, with a guaranteed final answer.
  • The classifier now abstains when it can't tell, instead of quietly forcing the strong tier. The composition picks the fallback.
  • Each turn records who decided it. A classifier-decided turn used to look like a fall-open in the stats.
  • One shared classifier config, instead of the stage router restating the same knobs.
  • Classifier conversation window is now a setting, default 3 (was fixed at 5, while the signals used 3).
  • The request-text helpers are target-generic, so any router can attach a per-target system prompt or append a note — not just this one.

Config

StageRouterConfig {
    mode,                  // tier an undecided turn falls back to
    confidence_threshold,  // agreement the signals need to decide
    recent_window,         // recent tool results the signals read
    handoff_notes,         // note text + when it fires
    tier_prompts,          // system prompt per tier
    llm_fallback,          // LlmClassifierConfig: model, threshold, window
}

Thresholds and tier names are validated at construction, so a missing tier fails immediately.

Who decides a turn

Turn Decided by Recorded as
signals confident signals override / tests_passed / dimensions
signals unsure, classifier answers classifier llm-classifier
signals unsure, classifier can't tell or absent picker default fall_open

No classifier call on a turn the signals settle.

Handoff notes

Fires on signal-driven escalation; hand-back to weak if configured
Placement end of the last user turn, after the tool result
State none — describes this turn's signals, like the Python router
Lifetime forwarded request only, never in history

Parity gaps

This PR Python
evict-fallback, stats not here (server route config)
default note text you supply it built-in

Testing

  • End-to-end multi-turn sessions assert what the model was handed: one note per signal-driven turn, right prompt per tier, no classifier call when signals settled, a fresh classifier answer each turn, an unusable verdict landing on the picker default, and the correct record of who decided.
  • All green, plus lint and docs.
  • Two changes outside this feature. The "about to call the model" event was declared but never emitted; it now fires and carries the decision that routed the request, which is what lets a processor act on the chosen target without keeping state between turns. And the llm_classifier route closes its cascade with the strong tier, keeping its behaviour identical now that the classifier can abstain.

Refs SWITCH-982, SWITCH-1110.

@sabhatinas
sabhatinas requested a review from a team as a code owner July 28, 2026 22:06
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a public StageRouter that routes between strong and weak targets using tool signals, optional judge fallback, handoff notes, and tier-specific prompts. Request processors now receive a final ModelRequest replay before the routed model call.

Changes

Stage Routing

Layer / File(s) Summary
Request processor replay
crates/libsy/src/algorithms/fall_through.rs
FallThrough now replays ModelRequest after the request and decision events, with updated event-ordering tests.
Tier change and handoff notes
crates/libsy/src/algorithms/util/handoff_notes.rs, crates/libsy/src/algorithms/util/stage_router.rs
Handoff notes are configured and injected directly into outbound requests when resolved routing changes tiers; state tracking and note behavior are covered by unit tests.
Per-tier prompt processing
crates/libsy/src/algorithms/util/tier_prompts.rs, crates/libsy/src/algorithms/util.rs
A processor appends configured strong or weak system prompts during ModelRequest processing.
StageRouter cascade and public API
crates/libsy/src/algorithms/stage.rs, crates/libsy/src/algorithms.rs, crates/libsy/src/lib.rs, crates/libsy/tests/stage_router_handoff.rs
Adds the configurable signal cascade, optional LLM judge fallback, fall-open behavior, public exports, construction tests, and integration coverage for routing, notes, judge calls, and prompts.

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

Poem

I’m a rabbit routing through the hay,
Strong and weak tiers hop into play.
Notes appear when pathways bend,
Prompts follow each turn to the end.
Judge or signal, the cascade knows—
Then safely falls where default flows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 93.88% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new stage router with handoff notes, tier prompts, and LLM fallback.

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

Comment thread crates/libsy/src/algorithms/stage.rs Outdated
}

#[async_trait]
impl Algorithm<SharedState> for StageRouter {

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 use this directly in config.rs

…apability-classifier fallback

Signed-off-by: Sabhatina Selvam <sabhatinas@nvidia.com>
@sabhatinas
sabhatinas force-pushed the sabhatinas/switch-982-handoff-notes-in-stage-classifier branch from e1154ce to 72d8fe6 Compare July 29, 2026 00:43
///
/// Errors if either threshold in `config` is outside `[0.0, 1.0]` or a tier is
/// missing from `targets`.
pub fn stage_router(

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.

Do we want a factory function like this or a algorithm::Stage wrapper around FallThrough?
LlmTaskClassifier uses a wrapper. I'm guessing that is easier to configure on the server size?

@nachiketb-nvidia what are your thoughts?

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