feat(routing): sub-agent override as a libsy classifier, restored on the serve path - #145
Merged
linj-glitch merged 10 commits intoJul 27, 2026
Conversation
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
WalkthroughChangesSub-agent routing
Estimated code review effort: 4 (Complex) | ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/tests/subagent_affinity.rs`:
- Around line 231-242: Update distinct_children_are_pinned_independently to seed
child-1 with the fixed worker override, then route child-2 through a
shared-affinity cascade whose override selects reviewer. Assert child-2 resolves
to reviewer and re-check child-1 still resolves to worker, exercising
independent sibling pinning rather than allowing both paths to pass via the same
override.
🪄 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: 64a8be41-03e9-407f-8ebc-1844ddd0a527
📒 Files selected for processing (12)
.agents/skills/switchyard-lib-core/SKILL.mdcrates/libsy/src/algorithms.rscrates/libsy/src/algorithms/fall_through.rscrates/libsy/src/algorithms/subagent_override.rscrates/libsy/src/algorithms/util.rscrates/libsy/src/algorithms/util/affinity.rscrates/libsy/src/algorithms/util/subagent.rscrates/libsy/tests/subagent_affinity.rscrates/switchyard-py/src/libsy_bindings.rsswitchyard/libsy/algorithms.pyswitchyard_rust/libsy.pytests/test_libsy_minimal_bindings.py
💤 Files with no reviewable changes (3)
- crates/libsy/src/algorithms/util/affinity.rs
- crates/libsy/src/algorithms/subagent_override.rs
- tests/test_libsy_minimal_bindings.py
Signed-off-by: Lin Jia <linj@nvidia.com>
linj-glitch
force-pushed
the
linj/switch-1068-combine-subagentoverride-classifier-with-affinity-routing
branch
from
July 27, 2026 06:42
9def569 to
1a3ae7c
Compare
Signed-off-by: Lin Jia <linj@nvidia.com>
ayushag-nv
reviewed
Jul 27, 2026
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
linj-glitch
force-pushed
the
linj/switch-1068-combine-subagentoverride-classifier-with-affinity-routing
branch
from
July 27, 2026 18:04
9933bed to
60f7024
Compare
|
eric-liu-nvidia
approved these changes
Jul 27, 2026
linj-glitch
enabled auto-merge (squash)
July 27, 2026 21:53
linj-glitch
deleted the
linj/switch-1068-combine-subagentoverride-classifier-with-affinity-routing
branch
July 27, 2026 21:56
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two halves of the same story: sub-agent routing becomes a composable classifier in libsy, and returns to the serve path where traffic actually flows.
libsy —
SubagentOverridemoves from anAlgorithmcombinator to aClassifier, so it composes withAffinityRouterin aFallThroughcascade:serve — the
subagent_targetroute envelope key returns, valid on any route type:Why
Closes SWITCH-1068. Per @GRClark: "I think SubagentOverride could be a classifier in libsy terms. That way we can easily combine affinity and override."
The two sat at different layers and could not be combined:
SubagentOverridewasAlgorithm<()>whileFallThroughisAlgorithm<SharedState>, so wrapping did not typecheck. And the arrangement described earlier in that thread — override wrapping an inner algorithm with affinity inside — could not have worked even with matching types: the override short-circuited todriver.call_llm_target, so a sub-agent request never entered the inner algorithm, the processor chain never sawEvent::Request, and affinity never latched. Sub-agent traffic was invisible to every stateful component. As sibling classifiers this resolves itself —FallThroughreplays the winning decision to the processors, so the override's turn-1 choice is latched for free.The serve half is a restoration, not a new feature.
subagent_targetshipped in #112 and was deleted by #119 ("nuke components-v2"), where it appears to be collateral rather than a decision — the commit targets the crate, not this feature. #112 also created libsy'ssubagent_override.rs, which survived; that asymmetry is why the classifier had no serve-path counterpart. Restoring it needs review from whoever drove #119 in case dropping it was intentional.Measured on real traffic
Both halves were exercised end to end against four SWE-Atlas QnA tasks driven by Claude Code through Switchyard, closed-book. This also confirmed the thing every sub-agent key depends on:
x-claude-code-agent-iddoes reach Switchyard, sois_subagent_requestfires on live traffic rather than only in tests.The serve-path arm below routes delegated work to an open-weights worker on a self-hosted vLLM deployment (
subagent_target), against an otherwise identical single-model control:49% cheaper at identical quality — same mean
agg_score, same reward pass rate, and the only task below ceiling (grafana, 0.846) scored the same in both arms. 322 of 506 requests (64%) were served by the worker for $4.19, while the root model handled 184 for $14.52.Traffic shape, which is what motivates pairing the override with affinity: the split arm spawned 17 sub-agents, none single-shot — median 39 turns, max 137. A 137-turn child under a re-scoring classifier is 137 routing decisions where one would do; affinity-first collapses that to 1 decision plus 136 replays.
Caveats, stated plainly: 4 tasks, one trial per arm, and three of the four sit at the 1.000
agg_scoreceiling, so the quality claim is mostly "ceiling tasks stayed at ceiling". Spawn counts varied 5-17 across runs on identical inputs, so run variance is large relative to the differences. The cost direction is solid — a big effect with a visible mechanism — but the magnitude is indicative, not decision-grade.Also observed: the routing log records one
session_idper task covering parent and all descendants, with noagent_idfield. Sub-agent routing is currently unobservable in our own telemetry, and it confirms why affinity keys onsession + agent— keying on session alone would pin a whole task, parent and every descendant, to one model. Telemetry gap left for a follow-up.How tested
cargo testgreen,cargo clippy --all-targetsclean,cargo fmt --alluv run ruff check .cleanuv run mypy switchyardclean (161 files)uv run pytest tests/ --ignore=tests/e2egreen (1954 passed, 9 skipped)tests/e2e/test_classifier_planner_chain_e2e.pyfails on a live-upstream 401 (LiteLLM Virtual Key expected. Received=nvap****). Pre-existing env key-format issue, unrelated.New coverage:
crates/libsy/tests/subagent_affinity.rs(8 tests) — drives the cascade through the public API. The key one is a pin outliving the policy that seeded it: two cascades share oneAffinityRouterbut disagree on the worker, and the second override is never consulted. Paired with a negative control (independent routers ⇒ the second override does win), so it cannot pass for the wrong reason. Also: a Codexcompactturn is sub-agent lineage but not delegated work, so it is keyed by affinity yet not forced to the worker; and dropping either classifier still leaves a valid cascade.tests/test_subagent_routing.py(8 tests) — detection policy, runtime branching, and route-bundle wiring, including that an unknown envelope key is still rejected.Notes for reviewers
One policy, two plumbings. The serve wrapper never sniffs headers:
is_subagent_requestis a 29-line binding overswitchyard_protocol::Metadata::from_headers(..).is_subagent_work()— the same function libsy's classifier calls. Socompactcannot count as delegated work in one engine and not the other. This is deliberately unlike the LLM classifier, where the libsy (390-line) and serve (2055-line) implementations have genuinely diverged.Public API removed: the Python
subagent_override(inner, worker)libsy binding is gone. A classifier scores a target name resolved from the cascade'sLlmTargetSet, so the old(inner: Algorithm, worker: LlmTarget)shape is not representable, and libsy ships no terminalClassifier— a Python-built cascade would fail with "every classifier abstained" on root traffic. Zero consumers across Gym, log2-infra, nv-OpenHands, open_source_model_improvement, and switchyard-bench. Exposing composition to Python needs a terminal classifier; left to a follow-up.FallThrough::with_componentis new:AffinityRouterholds assignments on the instance, so both roles must be the sameArc. Registering them separately compiles, runs, and silently never latches. This makes that unrepresentable and matches what the affinity module docs already claimed.Serve-path scope — one wrap, no per-type branching. The whole rule is three lines in
_build_switchyard_for_route:model,passthrough,stage_router,deterministic, andescalation_routerall funnel through that site, so they get it for free.random_routingexpands into its table entries on a separate path and so is unaffected — the key parses but does nothing there. That is deliberate: sub-agent logic branching onroute_typewas not worth the code, so there is none.Skill drift, separate commit:
switchyard-lib-core/SKILL.mddocumentedSubagentOverrideProfile,switchyard/lib/profiles/loader.py,is_subagent_request, andcrates/switchyard-components-v2/— all removed by #119, so the skill was telling agents to set a YAML key that did nothing. Rewritten to match reality.Benchmark artifacts included: five routing profiles under
benchmark/routing-profiles/(theqa-subagent-*ones are the usage documentation forsubagent_target, including the cross-provider case where the worker carries its ownbase_url/api_key), plusbenchmark/run_cost.pyfor per-model cost reporting. Cost-table additions: Claude Opus 5 at the published $5/$25 Opus-tier rate, and GLM-5.2 at third-party reference rates — that model runs on a self-hosted deployment with no per-token billing, so the entry answers "what would this traffic have cost at market rates", not what we are billed; provider rates vary about 2x. Run outputs and generated datasets are gitignored and not included.