Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agents/skills/switchyard-lib-core/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ right validation set. If the change is driven by a launcher need, also read
| An OpenAI-compatible provider target such as NVIDIA Inference Hub or OpenRouter | Use the existing OpenAI-compatible backend/profile with `base_url`, `api_key`, and model id wiring. Add a new backend only when the provider has a real wire-format, auth, retry, or health contract that cannot fit that path. |
| Direct Rust component bindings | Add concrete PyO3 classes under `crates/switchyard-py/src/component_bindings/`, keep config bindings near the component binding that consumes them, and expose them lazily from `switchyard_rust/components.py`. Do not keep growing `core_bindings.rs` or `switchyard_rust/core.py` with concrete component classes. |
| Route YAML / model dispatch | Use `switchyard/cli/route_bundle.py` and `switchyard/lib/route_table_builders.py`. They build `RouteTable` entries from profile-backed runtimes and keep launchers plus `switchyard serve --routing-profiles` on one path. |
| Route sub-agent traffic to a fixed worker target | Set `subagent_target: <target-id>` in any profile's common envelope (consumed like `type` in Rust `SerializedProfileConfig`, `crates/switchyard-components-v2/src/config/parsing.rs`). The Python loader (`switchyard/lib/profiles/loader.py`) wraps the built profile in `SubagentOverrideProfile`; detection is the Rust-bound `is_subagent_request(headers)` from `switchyard_rust.profiles`, a thin wrapper over `Metadata::from_headers` + `Metadata::is_subagent_work` in `crates/protocol/src/metadata.rs` (the canonical lineage fact and work-vs-maintenance policy). On the libsy `Algorithm` path, wrap with the `SubagentOverride` combinator (`crates/libsy/src/algorithms/subagent_override.rs`; Python: `switchyard.libsy.algorithms.subagent_override`). Do not re-implement header sniffing inside individual profiles or algorithms. |
| Route sub-agent traffic to a fixed worker target | Add the `SubagentOverride` classifier (`crates/libsy/src/algorithms/util/subagent.rs`) to a `FallThrough` cascade: it scores one fixed worker target for delegated work and abstains otherwise, so later classifiers serve ordinary traffic. Detection is `Metadata::from_headers` + `Metadata::is_subagent_work` in `crates/protocol/src/metadata.rs` — the canonical lineage fact and work-vs-maintenance policy. Do not re-implement header sniffing inside individual profiles or algorithms. Rust-only today; there is no profile-envelope key and no Python binding for it. |
| Keep a sub-agent on whatever target it was first routed to | Register `AffinityRouter` (`crates/libsy/src/algorithms/util/affinity.rs`) with `FallThrough::with_component`, which wires its `Processor` and `Classifier` roles from one `Arc` — registering the two separately is easy to half-wire, and a missing processor leaves the classifier permanently empty. Order it before the classifier that seeds the decision. Opt-in; nothing enables it by default. Distinct from the `session_affinity` / Redis pin store in the row below, which is the `switchyard-core` session cache, not a libsy classifier. |
| Shared/persistent session-affinity pins across workers or pod churn | Configure the latency route with `session_affinity: true` + `affinity_store: redis` + `affinity_store_url` (optional `affinity_store_ttl_seconds`, `affinity_key_prefix`); the escalation_router route takes the same `affinity_store*` keys (no `session_affinity` flag — its latch is always on; default prefix `swyd:esc:`). `SessionAffinity` keeps the Rust `SessionCache` as L1 and reads/writes through the `AffinityPinStore` L2 (`switchyard/lib/redis_pin_store.py`), fail-open behind a 0.1s socket timeout and a 3-failure/10s-cooldown circuit breaker (`switchyard_affinity_l2_breaker_open` gauge). Requires the `switchyard[affinity-redis]` extra. |
| Stats / telemetry | Reuse `StatsRequestProcessor`, `StatsResponseProcessor`, `StatsLlmBackend`, and `StatsAccumulator`. A profile config should thread one accumulator through all three when stats are enabled. Do not write a parallel collector. |
| A fixed-path endpoint contributed by per-route components | Set `Endpoint.register_once = True`; `build_switchyard_app(...)` mounts the first instance while still running every component's lifecycle. Leave the default `False` for configurable endpoint classes that may mount distinct instances. |
Expand Down
5 changes: 2 additions & 3 deletions crates/libsy/src/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ pub mod fall_through;
pub mod llm_class;
pub mod noop;
pub mod rand;
pub mod subagent_override;

pub use fall_through::{FallThrough, FallThroughDecision};
pub use llm_class::{ClassifierDecision, ClassifierTier, LlmClassifier};
pub use noop::{Noop, NoopDecision};
pub use rand::{Random, RandomDecision};
pub use subagent_override::{SubagentDecision, SubagentOverride};
pub use util::{AffinityRouter, SubagentOverride};

pub(crate) mod util;
pub mod util;
13 changes: 13 additions & 0 deletions crates/libsy/src/algorithms/fall_through.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ impl FallThrough {
self.classifiers.push(classifier);
self
}

/// Registers one dual-role component in *both* the processor chain and the classifier
/// cascade.
///
/// A component that writes state as a [`Processor`] and reads it back as a
/// [`Classifier`] — such as [`AffinityRouter`](crate::algorithms::AffinityRouter) —
/// shares that state through the instance, so both roles must be the same `Arc`.
/// Registering the two separately is easy to half-wire: omit the processor and the
/// classifier silently never sees an assignment. This registers both at once.
pub fn with_component<T: Processor + Classifier + 'static>(self, component: Arc<T>) -> Self {
self.with_processor(component.clone())
.with_classifier(component)
}
}
#[async_trait]
impl Algorithm<SharedState> for FallThrough {
Expand Down
206 changes: 0 additions & 206 deletions crates/libsy/src/algorithms/subagent_override.rs

This file was deleted.

7 changes: 4 additions & 3 deletions crates/libsy/src/algorithms/util.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

mod affinity;
pub mod affinity;
pub mod subagent;

#[allow(unused_imports)]
pub(crate) use affinity::AffinityRouter;
pub use affinity::AffinityRouter;
pub use subagent::SubagentOverride;
2 changes: 0 additions & 2 deletions crates/libsy/src/algorithms/util/affinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
//! session. [`AffinityRouter::for_subagents`] narrows affinity to explicitly identified
//! child agents, leaving root traffic to later classifiers on every turn.

#![allow(dead_code)]

use std::collections::{HashMap, HashSet};
use std::sync::Mutex;

Expand Down
Loading
Loading