Skip to content

feat(libsy): decide api - #207

Open
messiaen wants to merge 4 commits into
mainfrom
grclark/libsy-decide
Open

feat(libsy): decide api#207
messiaen wants to merge 4 commits into
mainfrom
grclark/libsy-decide

Conversation

@messiaen

@messiaen messiaen commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Added decide api

Still allows offloading, but add a Driver::final_decision method which Algorithms use to signal this model call is the selected model.

So run_decision_only_stream functions the same as run_steam except the last model call is return as a DecidedCall instead of a CallLlm and then a ReturnToAgent

In the streaming case Driver::final_decision calls Driver::call_llm_target.
In the decide case if simply returns a DecidedCall containing the Decision, Request and optionally the Response if the algo happens to compute it as part of the decision (e.g. escalation router)

async fn run(..)    -> Result<(Vec<Arc<dyn Decision>>, Response)>
async fn decide(..) -> Result<(Vec<Arc<dyn Decision>>, DecidedCall)>
fn run_stream(..)               -> StepStream              // Stream<Result<Step>>
fn run_decision_only_stream(..) -> DecisionOnlyStepStream  // Stream<Result<DecisionOnlyStep>>

type DecidedCall = (Arc<dyn Decision>, Box<Request>, Option<Box<Response>>);

changes create_run_task signature to support both decide and stream apis

async fn create_run_task(self: Arc<Self>, ctx: Context, driver: Driver, request: Request)
    -> Result<ResponseOrDecision>;

pub enum ResponseOrDecision {
    Response(Box<Response>),   // the call was served
    Decision(DecidedCall),     // the call is handed back
}

Algorithms don't change other than create_run_task signature and calling final_decision

Driver hold a decide_only flag which is set when the Driver is created in decide, run, run_stream, etc

Why

Integrators want the ability to just get the decision without having libsy run the final model request.

Closes #

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 decision-only request handling, allowing routing decisions to be returned without serving the final model call.
    • Added support for returning an already-available response alongside routing decisions.
    • Added classification utilities for selecting the highest-confidence route.
  • Bug Fixes

    • Prevented duplicate model calls when a classifier has already produced a response.
    • Improved fallback behavior when routing services are unavailable.
  • Documentation

    • Expanded guidance for request execution modes, decision-only flows, and algorithm behavior.

@messiaen
messiaen requested a review from a team as a code owner July 30, 2026 18:25
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR introduces decision-only request execution, adds ResponseOrDecision terminal results, propagates optional classifier responses, updates routing algorithms and streams, generalizes observability, and documents the new run, decide, and streaming behaviors.

Changes

Decision-only execution

Layer / File(s) Summary
Terminal and classifier contracts
crates/libsy/src/core/algorithm.rs, crates/libsy/src/core/classifier.rs
Algorithms can return either a served response or a routing decision with request and optional response data.
Mode-aware driver streams
crates/libsy/src/core/algorithm.rs
Normal and decision-only streams share driving logic while exposing separate run and decide behavior.
Algorithm routing and response propagation
crates/libsy/src/algorithms/*, crates/libsy/examples/ensemble.rs
Routing implementations adopt tuple classifier results, preserve classifier-produced responses, and use final_decision.
Public guidance and integration validation
crates/libsy/README.md, crates/libsy/src/lib.rs, crates/libsy/src/observability.rs, crates/libsy/tests/*
Documentation describes the new entry points, observability accepts generic results, and integration tests use the updated contract.

Estimated code review effort: 5 (Critical) | ~120 minutes

Poem

I’m a rabbit who routes through the clover bright,
With choices returned before calls take flight.
Responses may hop back, decisions may stay,
Streams now know which path to play.
Sniff, sniff—four entry points lead the way!

🚥 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 clearly matches the main change: adding a libsy decide API.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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.

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: 2

🧹 Nitpick comments (3)
crates/libsy/src/core/algorithm.rs (1)

736-757: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Prefer an error item over unreachable! for the stream-shape mismatch.

run_stream_inner is an overridable provided method; an override that returns the other variant turns into a panic in a public entry point rather than a LibsyError. Yielding a single Err(LibsyError::AlgorithmError { .. }) stream keeps the failure mode consistent with the rest of the driver, which already decodes incompatible terminals as errors.

🤖 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/core/algorithm.rs` around lines 736 - 757, The public stream
helpers around run_stream_inner must handle an unexpected AnyStepStream variant
without panicking. Replace both unreachable! branches in run_step_stream and
run_decision_only_stream with streams yielding a single
Err(LibsyError::AlgorithmError { .. }) item, preserving the existing successful
variant handling and using the established error-stream construction pattern.
crates/libsy/src/core/classifier.rs (1)

45-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten max_classification’s doc. The current wording is redundant; spell out that Ambiguous preserves the variant, so a plain argmax(false) on the result still yields None unless callers opt into ignoring ambiguity.

🤖 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/core/classifier.rs` around lines 45 - 55, Update the
documentation for max_classification to remove the redundant wording and state
that the Ambiguous variant is preserved. Document that calling argmax(false) on
the result still returns None unless ambiguity is explicitly ignored.
crates/libsy/examples/ensemble.rs (1)

386-398: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

decide on this example fails while still exploring.

The exploration branch always yields ResponseOrDecision::Response, which the decision-only stream rejects (crates/libsy/src/core/algorithm.rs:308-341). So EnsembleOrchAlgo::decide errors until the algorithm commits. That's a defensible design for an ensemble, but worth stating in the type/method docs so example readers aren't surprised.

🤖 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/examples/ensemble.rs` around lines 386 - 398, Document the
exploration behavior in the relevant EnsembleOrchAlgo decision method/type docs
near the shown branch: while no committed model is available, decide returns a
ResponseOrDecision::Response from the winning candidate, and the decision-only
stream therefore rejects it until commitment. Clarify that this is intentional
ensemble behavior without changing the implementation.
🤖 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/core/algorithm.rs`:
- Around line 35-40: Add `///` Rust documentation to the public
`DecisionOnlyStepStream` type alias, the `AnyStepStream` enum and each of its
variants, and the `ResponseOrDecision` enum and each of its variants. Match the
concise documentation style used by neighboring public items such as
`DecidedCall` and `DecisionOnlyStep`.
- Around line 289-290: Update the terminal-decoder comments in
crates/libsy/src/core/algorithm.rs at lines 289-290 and 322-323. In stream(),
explain that a decision-only terminal is an error because the caller requested a
served response; in decision_only_stream(), explain that a served response is an
error because the stream expects a decision handoff.

---

Nitpick comments:
In `@crates/libsy/examples/ensemble.rs`:
- Around line 386-398: Document the exploration behavior in the relevant
EnsembleOrchAlgo decision method/type docs near the shown branch: while no
committed model is available, decide returns a ResponseOrDecision::Response from
the winning candidate, and the decision-only stream therefore rejects it until
commitment. Clarify that this is intentional ensemble behavior without changing
the implementation.

In `@crates/libsy/src/core/algorithm.rs`:
- Around line 736-757: The public stream helpers around run_stream_inner must
handle an unexpected AnyStepStream variant without panicking. Replace both
unreachable! branches in run_step_stream and run_decision_only_stream with
streams yielding a single Err(LibsyError::AlgorithmError { .. }) item,
preserving the existing successful variant handling and using the established
error-stream construction pattern.

In `@crates/libsy/src/core/classifier.rs`:
- Around line 45-55: Update the documentation for max_classification to remove
the redundant wording and state that the Ambiguous variant is preserved.
Document that calling argmax(false) on the result still returns None unless
ambiguity is explicitly ignored.
🪄 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: b6341a0c-e4b6-4648-8c5f-4c615862d1be

📥 Commits

Reviewing files that changed from the base of the PR and between a3a3d31 and 5cdcc2c.

📒 Files selected for processing (19)
  • crates/libsy/README.md
  • crates/libsy/examples/ensemble.rs
  • crates/libsy/src/algorithms/fall_through.rs
  • crates/libsy/src/algorithms/llm_class.rs
  • crates/libsy/src/algorithms/noop.rs
  • crates/libsy/src/algorithms/passthrough.rs
  • crates/libsy/src/algorithms/rand.rs
  • crates/libsy/src/algorithms/stage.rs
  • crates/libsy/src/algorithms/subagent_affinity_tests.rs
  • crates/libsy/src/algorithms/util/affinity.rs
  • crates/libsy/src/algorithms/util/llm_judge.rs
  • crates/libsy/src/algorithms/util/stage.rs
  • crates/libsy/src/algorithms/util/subagent.rs
  • crates/libsy/src/core/algorithm.rs
  • crates/libsy/src/core/classifier.rs
  • crates/libsy/src/lib.rs
  • crates/libsy/src/observability.rs
  • crates/libsy/tests/observability.rs
  • crates/libsy/tests/prompts.rs

Comment on lines +35 to +40
pub type DecisionOnlyStepStream = Pin<Box<dyn Stream<Item = Result<DecisionOnlyStep>> + Send>>;

pub enum AnyStepStream {
Step(StepStream),
DecisionOnly(DecisionOnlyStepStream),
}

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 | 🟠 Major | ⚡ Quick win

Add /// docs to the new public items.

DecisionOnlyStepStream, AnyStepStream (and its variants), and ResponseOrDecision (and its variants) are public but undocumented, unlike the neighbouring DecidedCall/DecisionOnlyStep.

📝 Suggested docs
+/// A stream of [`DecisionOnlyStep`]s: the decision-only shape of a run.
 pub type DecisionOnlyStepStream = Pin<Box<dyn Stream<Item = Result<DecisionOnlyStep>> + Send>>;
 
+/// Either step-stream shape a run can be driven as, selected by the entry point.
 pub enum AnyStepStream {
+    /// A run that serves its final call.
     Step(StepStream),
+    /// A run that hands its final call back.
     DecisionOnly(DecisionOnlyStepStream),
 }
+/// The terminal payload of a run: the served response, or the decided call handed back.
 pub enum ResponseOrDecision {
+    /// The run served its final call and this is the answer.
     Response(Box<Response>),
+    /// The run stopped at its final decision; see [`DecidedCall`].
     Decision((Arc<dyn Decision>, Box<Request>, Option<Box<Response>>)),
 }

As per coding guidelines: "Add documentation for public functions, classes, methods, API entry points, and public Rust items; ... Rust uses ///".

Also applies to: 350-353

🤖 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/core/algorithm.rs` around lines 35 - 40, Add `///` Rust
documentation to the public `DecisionOnlyStepStream` type alias, the
`AnyStepStream` enum and each of its variants, and the `ResponseOrDecision` enum
and each of its variants. Match the concise documentation style used by
neighboring public items such as `DecidedCall` and `DecisionOnlyStep`.

Source: Coding guidelines

Comment on lines +289 to +290
// Algorithms always run with `decision_only` off today, so the terminal payload
// is always a served response. A handoff has no path to the agent yet.

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

Pre-decision_only comment left behind and duplicated in both terminal decoders. The text "Algorithms always run with decision_only off today, so the terminal payload is always a served response. A handoff has no path to the agent yet" predates this PR, which adds exactly that path; it is now misleading in stream() and inverted in decision_only_stream().

  • crates/libsy/src/core/algorithm.rs#L289-L290: replace with why a decision-only terminal is an error on the served stream (the caller asked for a response).
  • crates/libsy/src/core/algorithm.rs#L322-L323: replace with the mirror rationale — this stream expects the decision handoff, so a served response means the algorithm ignored the mode.
📍 Affects 1 file
  • crates/libsy/src/core/algorithm.rs#L289-L290 (this comment)
  • crates/libsy/src/core/algorithm.rs#L322-L323
🤖 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/core/algorithm.rs` around lines 289 - 290, Update the
terminal-decoder comments in crates/libsy/src/core/algorithm.rs at lines 289-290
and 322-323. In stream(), explain that a decision-only terminal is an error
because the caller requested a served response; in decision_only_stream(),
explain that a served response is an error because the stream expects a decision
handoff.

messiaen added 4 commits July 31, 2026 10:46
Signed-off-by: Greg Clark <grclark@nvidia.com>

chore(libsy): wip

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

feat(libsy): finish up decide api

Signed-off-by: Greg Clark <grclark@nvidia.com>
Signed-off-by: Greg Clark <grclark@nvidia.com>
Signed-off-by: Greg Clark <grclark@nvidia.com>
Signed-off-by: Greg Clark <grclark@nvidia.com>
@messiaen
messiaen force-pushed the grclark/libsy-decide branch from 280bf91 to 456c0c3 Compare July 31, 2026 16:54
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