Skip to content

feat(profiles): route sub-agent requests to a profile-level subagent_target#112

Merged
linj-glitch merged 6 commits into
mainfrom
linj/subagent-routing
Jul 23, 2026
Merged

feat(profiles): route sub-agent requests to a profile-level subagent_target#112
linj-glitch merged 6 commits into
mainfrom
linj/subagent-routing

Conversation

@linj-glitch

@linj-glitch linj-glitch commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds envelope-level sub-agent routing per the sub-agent routing design: any profile config may name an optional subagent_target, and requests that carry a recognized delegated sub-agent signal bypass the profile's routing and run as a direct passthrough to that target. Everything else — including profiles without the field — behaves exactly as today.

profiles:
  smart:
    type: random-routing
    strong: strong
    weak: weak
    strong_probability: 0.3
    subagent_target: weak

This keeps a sub-agent loop on one intentional, cache-compatible target and honors an explicit orchestrator-vs-worker model split, instead of re-routing every worker turn through the profile's router.

How it works

  • Common envelope field (Rust)subagent_target is consumed in SerializedProfileConfig::deserialize exactly like type, so profile-owned bodies (which deny_unknown_fields) never see it and the field works for every profile type, Rust- or Python-defined. An unknown target reference fails at resolve() / load time — a startup configuration error.
  • Detection (Rust binding) — new is_subagent_request(headers) pyfunction reuses the canonical harness header normalization from switchyard-protocol (Metadata::from_headers, feat(switchyard-protocol): add common utility to hydrate Metadata from headers #92) and layers the routing policy on top:
    • Codex x-openai-subagent: collab_spawn / review and Claude Code agent lineage route as sub-agent work;
    • Codex harness maintenance (compact, memory_consolidation) and unrecognized kinds stay on normal profile routing;
    • explicit x-switchyard-is-subagent decides directly, in both directions.
  • Override wrap (Python) — the profile loader wraps any profile whose envelope names a subagent_target in SubagentOverrideProfile, which selects per request between the wrapped profile and a PassthroughProfileConfig branch for the resolved target. It forwards the full context-aware profile surface (so stats and route-selection context propagation are preserved) and never rewrites the request/response. An override-branch failure surfaces as a normal target error — it is not silently re-routed through the main router.

Policy note: the work-vs-maintenance kind filter lives in the binding, not the protocol crate, so the canonical Metadata::from_headers semantics are unchanged.

Testing

  • 4 new Rust config tests (envelope strip/expose, absent default, unknown-target rejection at resolve, non-string rejection); cargo test green for switchyard-components-v2, switchyard-py, switchyard-protocol; clippy and fmt clean.
  • 20 new Python tests (tests/test_subagent_routing.py): detection matrix, loader wrapping for both a Rust (passthrough) and a Python (header-routing) profile against a mock upstream, maintenance-kind fall-through, startup rejection of unknown targets on both profile kinds, lifecycle spanning both branches, and no-fallback-on-override-failure.
  • uv run ruff check ., uv run mypy switchyard (strict), and the full suite pass: 2026 passed. The only failures are 3 pre-existing tests/e2e/ cases that 401 against an external LiteLLM proxy (environmental, unrelated).
  • Docs (docs/routing_algorithms/overview.md) and the switchyard-lib-core skill updated alongside the code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional subagent_target routing for profiles.
    • Delegated sub-agent requests can bypass normal routing and go directly to the configured target.
    • Added detection for supported sub-agent request headers and signals.
    • Added configuration validation for missing or invalid target references.
  • Documentation

    • Documented sub-agent override routing, supported signals, and error behavior.
  • Tests

    • Added coverage for detection, routing, configuration validation, and failure handling.

@linj-glitch
linj-glitch requested a review from a team as a code owner July 22, 2026 07:06
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://NVIDIA-NeMo.github.io/Switchyard/pr-preview/pr-112/

Built to branch gh-pages at 2026-07-23 07:35 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds optional subagent_target profile configuration, validates and exposes it across Rust and Python layers, detects delegated requests, and routes them through a new override wrapper. Documentation and tests cover detection, configuration errors, routing, lifecycle behavior, and response processing.

Changes

Sub-agent Override Routing

Layer / File(s) Summary
Profile target contract
crates/switchyard-components-v2/src/config/parsing.rs, crates/switchyard-components-v2/src/config/resolving.rs, crates/switchyard-components-v2/tests/config.rs
Profile envelopes parse, expose, serialize, and validate optional subagent_target values, with coverage for omission, invalid types, body stripping, and unknown targets.
Sub-agent detection API
crates/switchyard-py/Cargo.toml, crates/switchyard-py/src/profile_bindings/*, switchyard_rust/profiles.py, switchyard_rust/profiles.pyi
Python bindings expose is_subagent_request, using explicit headers and protocol metadata, and publish the native helper with typing.
Override wrapper and loader wiring
switchyard/lib/profiles/loader.py, switchyard/lib/profiles/subagent_override.py, tests/test_subagent_routing.py
Configured profiles are wrapped with branch-preserving override routing, including context handling, lifecycle components, response processing, target failures, and end-to-end routing tests.
Routing guidance
.agents/skills/switchyard-lib-core/SKILL.md, docs/routing_algorithms/overview.md
Developer and routing documentation describe subagent_target, recognized signals, passthrough behavior, and shared detection logic.

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

Poem

I’m a rabbit routing through the night,
Sending worker hops to targets right.
Headers whisper, “sub-agent here!”
The override path then hops near.
Normal trails stay where they roam—
All branches safely find their home.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: routing sub-agent requests to a profile-level subagent_target.
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: 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 `@docs/routing_algorithms/overview.md`:
- Around line 91-95: Update the routing behavior description near the recognized
sub-agent signals and its corresponding section to state that an explicit
x-switchyard-is-subagent: false header takes precedence over Codex or Claude
agent signals and preserves normal profile routing, while true continues to
enable direct passthrough.
🪄 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: 24d05bd0-b6b4-41b2-b89f-f648c12ece25

📥 Commits

Reviewing files that changed from the base of the PR and between e09b5b0 and df00c90.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !Cargo.lock
📒 Files selected for processing (13)
  • .agents/skills/switchyard-lib-core/SKILL.md
  • crates/switchyard-components-v2/src/config/parsing.rs
  • crates/switchyard-components-v2/src/config/resolving.rs
  • crates/switchyard-components-v2/tests/config.rs
  • crates/switchyard-py/Cargo.toml
  • crates/switchyard-py/src/profile_bindings/mod.rs
  • crates/switchyard-py/src/profile_bindings/typed.rs
  • docs/routing_algorithms/overview.md
  • switchyard/lib/profiles/loader.py
  • switchyard/lib/profiles/subagent_override.py
  • switchyard_rust/profiles.py
  • switchyard_rust/profiles.pyi
  • tests/test_subagent_routing.py

Comment thread docs/routing_algorithms/overview.md
@messiaen

Copy link
Copy Markdown
Contributor

@linj-glitch

Detection (Rust binding) — new is_subagent_request(headers) pyfunction reuses the canonical harness header normalization from switchyard-protocol (Metadata::from_headers, #92) and layers the routing policy on top:
Codex x-openai-subagent: collab_spawn / review and Claude Code agent lineage route as sub-agent work;
Codex harness maintenance (compact, memory_consolidation) and unrecognized kinds stay on normal profile routing;
explicit x-switchyard-is-subagent decides directly, in both directions.

Shouldn't we fix Metadata::from_headers to correctly detect is_subagent so that all algorithms would benifit?

@messiaen

Copy link
Copy Markdown
Contributor

@linj-glitch can we have an implementation of this in libsy

@linj-glitch

linj-glitch commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

@messiaen Agreed in spirit — implemented in 29015c9, with one refinement: the comment bundles two things that I think deserve different treatment.

Fixed in Metadata::from_headers (all consumers benefit): the explicit-flag handling was a genuine detection bug. The doc comment promised that an explicit x-switchyard-is-subagent decides sub-agent status, but parse_sub_agent only honored it when a parent id accompanied it — explicit false alone was overridden by mere presence of x-openai-subagent. That's now fixed at the source: the explicit boolean decides the lineage fact in both directions, and inference only runs when it's absent.

Kept out of is_subagent (deliberately): the work-vs-maintenance kind filter. is_subagent is documented as a lineage fact — "the harness identified this request as coming from a child agent" — and a Codex compact turn genuinely is one (the harness stamps it with subagent_kind). Folding the routing policy into the field would make it misreport lineage to telemetry and cost-attribution consumers. Instead the policy now lives right next to the fact as Metadata::is_subagent_work() in the protocol crate, so every algorithm — libsy included — shares one implementation and one place to extend the kind allow-list. The Python binding is now a thin two-liner over from_headers(...).is_subagent_work().

One intentional behavior consequence: x-switchyard-is-subagent: true combined with a maintenance kind (compact) now stays on normal routing — the explicit flag asserts the lineage fact, and the kind policy applies uniformly on top. A client that wants forced worker routing sets the flag without a maintenance kind.

Coverage: 2 new protocol tests (explicit-flag decides without a parent header; is_subagent_work matrix including the lineage-fact-preserved assertion for maintenance kinds), and the Python detection matrix updated for the combined-header case. All 22 protocol tests, consumer crates (switchyard-server, switchyard-libsy, switchyard-llm-client), and the full Python suite (2026) pass.

@linj-glitch

Copy link
Copy Markdown
Contributor Author

@messiaen libsy implementation landed in a7c4a57 (coordinated with @GRClark — building blocks kept small so it composes with the fallthrough work when that lands).

It's a combinator, mirroring the profile-envelope shape one level down:

let algorithm = SubagentOverride::new(inner_algorithm, worker_target);

SubagentOverride (crates/libsy/src/algorithms/subagent_override.rs) checks request.metadata.is_subagent_work() — the shared policy from 29015c9 — and either delegates create_run_task to the wrapped algorithm untouched or makes a single driver.call_llm_target to the fixed worker target (the same single-call shape as Random). Wrapped algorithms never learn about harnesses or lineage headers, and a worker failure surfaces as a normal target error rather than re-entering the wrapped algorithm.

Also exposed through the minimal Python surface as switchyard.libsy.algorithms.subagent_override(inner, worker); Algorithm.run now accepts optional headers, normalized via Metadata::from_headers exactly as an HTTP host would, so the signals behave identically in Python and over HTTP.

Coverage: 3 Rust tests (no-metadata delegation, Claude-lineage and Codex review → worker, compact / explicit-false stay on the wrapped algorithm) plus an end-to-end Python binding test. Full suites pass (35 libsy tests, 2027 Python).

🤖 Generated with Claude Code

@linj-glitch

Copy link
Copy Markdown
Contributor Author

@messiaen the PR is updated with both rounds of feedback — the Metadata::from_headers fix + shared is_subagent_work() policy (29015c9) and the libsy SubagentOverride combinator (a7c4a57). Could you take another look when you get a chance?

@linj-glitch
linj-glitch requested a review from messiaen July 22, 2026 20:36
…target

Signed-off-by: Lin Jia <linj@nvidia.com>
…t work policy

Signed-off-by: Lin Jia <linj@nvidia.com>
…o a fixed target

Signed-off-by: Lin Jia <linj@nvidia.com>
Claude Code only sends x-claude-code-agent-id for child agents; root agents
omit it. Change the predicate from an agent-id/session-id inequality check to
agent.is_some(), matching the official contract and handling agent-id-only
requests that the old check silently missed.

Remove CODEX_PARENT_THREAD_ID_PATH and CODEX_PARENT_THREAD_ID_HEADER from
SUBAGENT_SIGNAL_PATHS and drop the `parent.is_some()` inference arm from
parse_sub_agent. Codex parent-thread-id is correlation data, not a routing
signal; the old code created a false-positive path where a kindless Codex
turn with only a parent thread ID routed as sub-agent work. Parent ids are
still captured in Metadata for observability via HEADER_CONFIG.

Add tests for the corrected cases and document x-switchyard-is-subagent: false
override behavior in the routing overview.

Signed-off-by: Lin Jia <linj@nvidia.com>
@linj-glitch
linj-glitch force-pushed the linj/subagent-routing branch from a9b0804 to afde9cc Compare July 23, 2026 06:43
…trailers

Signed-off-by: Lin Jia <linj@nvidia.com>
…ee bugs

Remove correlation-only headers (Relay, Dynamo, OpenCode) from routing
signals; add `is_delegated_work` field derived from raw harness signals
independent of the operator `x-switchyard-agent-kind` label; add Rust
`SubagentOverrideProfile` in components-v2 with full stats recording so
Rust-profiled deployments flow through the Rust stats pipeline.

Signed-off-by: Lin Jia <linj@nvidia.com>
@linj-glitch
linj-glitch force-pushed the linj/subagent-routing branch from c62b745 to 1e01762 Compare July 23, 2026 07:35
@linj-glitch
linj-glitch enabled auto-merge (squash) July 23, 2026 07:36
@linj-glitch
linj-glitch merged commit 060ad75 into main Jul 23, 2026
19 checks passed
@linj-glitch
linj-glitch deleted the linj/subagent-routing branch July 23, 2026 07:40
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