Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d81e0fc
Add backward-compatible smart routing configuration versions
lilly-luo Oct 8, 2026
3913637
Version smart routing presets and validate complete flag definitions
lilly-luo Oct 8, 2026
9bbd850
Require explicit smart routing version names without aliases
lilly-luo Oct 8, 2026
34c00c8
Document how to extend smart routing configurations
lilly-luo Oct 8, 2026
e8377ea
Verify smart routing versions override all legacy flags
lilly-luo Oct 8, 2026
6614bd1
Include orchestrator in smart routing environment controls
lilly-luo Oct 8, 2026
5e0785d
Resolve smart routing versions before CLI command processing
lilly-luo Oct 8, 2026
1aa9880
Replace smart routing config tests with exhaustive permutation grid
lilly-luo Oct 8, 2026
98e4102
Simplify smart routing configuration permutation test
lilly-luo Oct 8, 2026
c4ffbbb
Rename smart router selector and add customer routing preset
lilly-luo Oct 8, 2026
9bc917d
Limit smart router preset coverage to routing tests
lilly-luo Oct 8, 2026
31819bf
Preserve legacy integration cases alongside smart router presets
lilly-luo Oct 8, 2026
e84d777
Cover four smart router presets in the routing CUJ
lilly-luo Oct 8, 2026
d1dd938
Merge remote-tracking branch 'origin/main' into lillyluo/smart-routin…
lilly-luo Oct 8, 2026
142994e
Name and document smart router configuration versions
lilly-luo Oct 8, 2026
7d36e28
Use named smart router versions in the routing CUJ
lilly-luo Oct 8, 2026
0c82331
Fix async Claude CUJ evidence and clean up routing presets
lilly-luo Oct 9, 2026
0e58854
Simplify routing presets and fix CUJ evidence matching
lilly-luo Oct 9, 2026
853c7ce
Trim routing config and CUJ regression coverage
lilly-luo Oct 9, 2026
aa926c4
Filter CUJ inference requests before decoding payloads
lilly-luo Oct 9, 2026
4050124
Merge branch 'main' into lillyluo/smart-routing-config-version
lilly-luo Oct 9, 2026
424ad93
Verify Claude thinking-display recovery in smart-routing CUJs
lilly-luo Oct 9, 2026
a3ee548
Confirm Claude background-work dialog during TUI exit
lilly-luo Oct 9, 2026
524dba3
Wait longer for Claude exit without confirming background-work dialog
lilly-luo Oct 9, 2026
c3cbb47
Wait for Claude background tasks before requesting exit
lilly-luo Oct 9, 2026
2c4e6bd
Verify chained Claude native compatibility retries in CUJs
lilly-luo Oct 9, 2026
913ce4e
Acknowledge Claude auto-mode billing notice in CUJ TUI waits
lilly-luo Oct 9, 2026
dfc2069
Fix Claude smart-routing CUJ completion evidence
lilly-luo Oct 9, 2026
52dc912
Recognize completed Claude background tasks before exit
lilly-luo Oct 9, 2026
09548c6
Wait for Claude background work before CUJ4 exit
lilly-luo Oct 9, 2026
995eca1
Wait for routed child evidence without reconstructing parent turns
lilly-luo Oct 9, 2026
2a44f77
Correlate Claude compatibility retries with their agent context
lilly-luo Oct 9, 2026
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
50 changes: 50 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,56 @@ Tests live in `tests/`.
- Add or update focused tests for behavior changes.
- Do not modify generated or lock files unless the dependency graph intentionally changes.

## Smart-routing configuration

`SMART_ROUTER_CONFIG_VERSION` is the external selector. Version definitions live in
`src/ucode/smart_routing/config.py` under `_VERSIONS`; their managed environment keys
are registered in `SMART_ROUTING_ENV_KEYS` in `src/ucode/constants.py`.
An unset or empty selector preserves legacy environment-flag behavior.
A valid nonempty selector overrides every conflicting legacy value in that registry.
Do not use `setdefault` or preserve inherited values for version-owned parameters.
Resolve and materialize valid selectors at the CLI boundary before argument parsing or callbacks.
Unknown selectors are a no-op: do not apply a preset, mutate the inherited environment, or raise
an error. Existing legacy flags and explicit launch/session controls retain their behavior.
Restore the inherited environment on every exit.
Explicit launch/session on/off controls still apply after version expansion.
Managed routing defaults must not rewrite already-resolved version flags.

### Adding a parameter

1. Define its environment-variable constant in `src/ucode/constants.py` and add it to
`SMART_ROUTING_ENV_KEYS`.
2. Set an explicit value for it in **every** `_VERSIONS` entry, including existing versions.
Choose values that preserve existing versions' behavior. Current parameters accept only
the strings `"0"` and `"1"`; do not use booleans, empty strings, or omitted keys.
3. Add its consumer in the appropriate routing module. Use `resolve_environment` for
config-aware reads, or the legacy flags materialized by `apply_config` at launch.
Keep launch-scoped changes restorable and preserve legacy behavior without a selector.
4. `SMART_ROUTING_ENV_KEYS` controls environment snapshots, restoration, and launch/session
off overrides. Keep routing activation limited to the V2 and subagent-only flags:
orchestration alone must not enable routing. Add regression tests for the new parameter's
controls. Session overrides must apply after version resolution.

`_validate_versions` runs at module import and rejects missing keys, unknown keys, and
invalid values. Do not weaken the complete-key check or infer required keys from `_VERSIONS`.
If a new parameter needs nonbinary values, add parameter-specific validation and tests.

### Adding a config type or revision

1. Add a complete mapping to `_VERSIONS` with an explicit suffix, such as `new_mode_v0`.
For a changed existing mode, add `existing_mode_v1` rather than changing its `_v0` behavior.
Do not add unsuffixed names or version aliases.
2. Define every key in `SMART_ROUTING_ENV_KEYS`; never rely on the caller's
inherited environment to fill missing values.
3. Update the version table and examples in `README.md` and any affected bundled-skill docs.
4. Extend `tests/test_smart_routing_config.py` for the new mode, precedence over legacy flags,
environment restoration, validation failures, and applicable session/launch behavior.
Update unknown-version tests when a previously rejected version becomes supported.
Follow `tests/AGENTS.md` and update its coverage READMEs.

Run `uv run pytest tests/test_smart_routing_config.py` plus relevant routing/CLI tests,
then `just lint`. These component checks do not establish live agent or gateway coverage.

## Style

- Keep user-facing CLI errors actionable.
Expand Down
54 changes: 47 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,55 @@ The generated shell hooks expect Git Bash; PowerShell-only setups are not covere

### Smart Router Orchestrator

Smart-routed Claude and Codex sessions install `smart-router`. Set
`ENABLE_SMART_ROUTER_ORCHESTRATOR=1` at launch to also install and activate Smart Router
Orchestrator through the bundled `smart-router-orchestrator` skill; orchestration is off by
default. For example:
Use `SMART_ROUTER_CONFIG_VERSION` at launch to select a smart-routing configuration:

| Version | Subagent routing | First-prompt routing | Orchestrator |
| --- | --- | --- | --- |
| `first_prompt_and_subagent_no_orch_v0` | On | On | Off |
| `subagent_only_v0` | On | Off | Off |
| `subagent_only_v1` | On | Off | Off |
| `subagent_orch_v0` | On | Off | On |
| `subagent_orch_v1` | On | Off | On |

`first_prompt_and_subagent_no_orch_v0` is the customer configuration for first-prompt
and subagent routing without orchestration: `ENABLE_SMART_ROUTING_V2=1`,
`ENABLE_SMART_ROUTING_SUBAGENT_ONLY=0`, and `ENABLE_SMART_ROUTER_ORCHESTRATOR=0`.

`subagent_only_v1` sets both `ENABLE_SMART_ROUTING_V2` and
`ENABLE_SMART_ROUTING_SUBAGENT_ONLY` to `"1"`. Subagent-only takes precedence,
so first-prompt routing remains off; orchestration is also off.

`subagent_orch_v1` enables all three legacy flags. Like `subagent_only_v1`, it routes
subagents rather than the first prompt, and it additionally enables orchestration.

`SMART_ROUTER_NAME` still selects the router independently of the preset.

Smart-routed Claude and Codex sessions install `smart-router`. The `subagent_orch_v0`
and `subagent_orch_v1` versions also install and activate the bundled `smart-router-orchestrator` skill.
For example:

```bash
ENABLE_SMART_ROUTER_ORCHESTRATOR=1 ENABLE_SMART_ROUTING_SUBAGENT_ONLY=1 ug claude
SMART_ROUTER_CONFIG_VERSION=subagent_orch_v0 ug claude
```

The version takes precedence over conflicting legacy flags. Before parsing command options
or running any command callbacks, UG expands it into
`ENABLE_SMART_ROUTING_V2`, `ENABLE_SMART_ROUTING_SUBAGENT_ONLY`, and
`ENABLE_SMART_ROUTER_ORCHESTRATOR` for the launched session. When the version is
unset or empty, these legacy flags retain their existing behavior, including
first-prompt routing through `ENABLE_SMART_ROUTING_V2=1`. Unknown versions are ignored:
no preset is applied, the inherited environment is unchanged, and commands continue normally.
Explicit launch/session on/off controls apply after expansion. Orchestration remains off by default.
Workspace smart-routing defaults do not rewrite the selected version's flags.

Version names require an explicit suffix. Future revisions use new `_v1`, `_v2`,
etc. names without changing existing versions.

Version definitions fail validation at module import if any flag in
`SMART_ROUTING_ENV_KEYS` is missing, has a value other than `"0"` or `"1"`,
or an unknown flag is present. Register new managed flags in that tuple and
explicitly set them in every version.

Use `ug codex` in the same command for Codex. Smart Router Orchestrator assigns bounded work
to explorer, researcher, worker, tester, and reviewer roles while the root plans,
integrates, and verifies results. Easy tasks and explicit requests not to delegate
Expand All @@ -274,8 +314,8 @@ Once opted in, orchestration follows the existing smart-routing launch eligibili
and session controls. Turning Smart Router off through its skill stops new automatic delegation;
turning it on restores orchestration only in opted-in sessions. Explicit user
requests for subagents still use normal harness behavior while routing is off.
Stored skill files do not activate orchestration when the feature flag is unset or
`ENABLE_SMART_ROUTER_ORCHESTRATOR=0`, or in non-routed sessions. Existing Isaac pilot gating
Stored skill files do not activate orchestration without an opted-in configuration,
or in non-routed sessions. Existing Isaac pilot gating
and UG launch exclusions still apply.

Hooks refresh orchestration state before each prompt and after compaction. A
Expand Down
9 changes: 7 additions & 2 deletions skills/smart-router-orchestrator/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Smart Router Orchestrator

UG bundles the `smart-router-orchestrator` workflow and five Claude role definitions.
Smart-routed Claude and Codex launches install and
activate this skill alongside `smart-router` only with `ENABLE_SMART_ROUTER_ORCHESTRATOR=1`.
Smart-routed Claude and Codex launches install and activate this skill alongside
`smart-router` with `SMART_ROUTER_CONFIG_VERSION=subagent_orch_v0` or `subagent_orch_v1`.
UG expands the selected version into the session's legacy feature flags. The `_v1` revision
enables both V2 and subagent-only routing flags alongside orchestration; subagent-only still
takes precedence, so the first prompt is not routed.
The existing `ENABLE_SMART_ROUTER_ORCHESTRATOR=1` opt-in remains supported when
`SMART_ROUTER_CONFIG_VERSION` is unset; `subagent_only_v0` explicitly leaves orchestration off.
The feature is off by default; routing alone installs only `smart-router`.

The workflow is injected before root prompts and after compaction. The hook checks
Expand Down
38 changes: 30 additions & 8 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,22 @@ def revert() -> int:
class _HelpOrderedGroup(TyperGroup):
"""Keep top-level help organized across commands and nested Typer apps."""

def make_context(
self,
info_name: str | None,
args: list[str],
parent: _click.Context | None = None,
**extra: Any,
) -> _click.Context:
previous = smart_routing_v2.apply_config()
try:
ctx = super().make_context(info_name, args, parent, **extra)
except BaseException:
smart_routing_v2.restore_smart_routing_env(previous)
raise
ctx.call_on_close(lambda: smart_routing_v2.restore_smart_routing_env(previous))
return ctx

def list_commands(self, ctx: _click.Context) -> list[str]:
commands = super().list_commands(ctx)
order = {name: index for index, name in enumerate(_HELP_COMMAND_ORDER)}
Expand Down Expand Up @@ -2478,10 +2494,11 @@ def _auto_configure_tool(tool: str, custom_oauth: CustomOAuthConfig | None = Non
@contextmanager
def _smart_routing_v2_flag(enabled: bool | None) -> Iterator[None]:
"""Apply an explicit routing choice without leaking into an embedding process."""
if enabled is None:
yield
return
previous = smart_routing_v2.override_smart_routing(enabled)
previous = (
smart_routing_v2.apply_config()
if enabled is None
else smart_routing_v2.override_smart_routing(enabled)
)
try:
yield
finally:
Expand Down Expand Up @@ -3180,7 +3197,11 @@ def _launch_tool(
)
print_success(f"Starting {TOOL_SPECS[tool]['display']}")
with _smart_routing_v2_flag(
True if managed_smart_routing_enabled and smart_routing_enabled else None
True
if managed_smart_routing_enabled
and smart_routing_enabled
and not smart_routing_v2.smart_routing_enabled()
else None
):
launch_agent(tool, state, ctx.args, options=launch_options)
except RuntimeError as exc:
Expand Down Expand Up @@ -3276,9 +3297,10 @@ def default(
return
set_dry_run(dry_run)
try:
_launch_managed_default(
ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace
)
with _smart_routing_v2_flag(None):
_launch_managed_default(
ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace
)
except typer.Exit:
# `typer.Exit` subclasses RuntimeError, so it has to be re-raised ahead of the handler
# below. Otherwise a launch that already reported its own error is followed by
Expand Down
2 changes: 2 additions & 0 deletions src/ucode/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
ENABLE_SMART_ROUTING_ENV_VAR = "ENABLE_SMART_ROUTING_V2"
ENABLE_SUBAGENT_ROUTING_ENV_VAR = "ENABLE_SMART_ROUTING_SUBAGENT_ONLY"
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR = "ENABLE_SMART_ROUTER_ORCHESTRATOR"
SMART_ROUTER_CONFIG_VERSION_ENV_VAR = "SMART_ROUTER_CONFIG_VERSION"
SMART_ROUTING_ENV_KEYS = (
ENABLE_SMART_ROUTING_ENV_VAR,
ENABLE_SUBAGENT_ROUTING_ENV_VAR,
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR,
)

MODEL_PROVIDER_SERVICE_HEADER = "Databricks-Model-Provider-Service"
Expand Down
101 changes: 101 additions & 0 deletions src/ucode/smart_routing/config.py
Comment thread
lilly-luo marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Resolve external smart-routing versions into backward-compatible feature flags."""

from __future__ import annotations

import os
from collections.abc import Mapping, MutableMapping

from ucode.constants import (
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR,
ENABLE_SMART_ROUTING_ENV_VAR,
ENABLE_SUBAGENT_ROUTING_ENV_VAR,
SMART_ROUTER_CONFIG_VERSION_ENV_VAR,
SMART_ROUTING_ENV_KEYS,
)

# Customer preset: route the first prompt and subagents, without orchestration.
FIRST_PROMPT_AND_SUBAGENT_NO_ORCH_V0 = "first_prompt_and_subagent_no_orch_v0"

# Route only subagents, with V2 disabled and no orchestration.
SUBAGENT_ONLY_V0 = "subagent_only_v0"

# Route only subagents, with both V2 and subagent-only flags enabled; no orchestration.
SUBAGENT_ONLY_V1 = "subagent_only_v1"

# Route only subagents and inject the Smart Router Orchestrator workflow.
SUBAGENT_ORCH_V0 = "subagent_orch_v0"

# Route only subagents with V2, subagent-only, and orchestration all enabled.
SUBAGENT_ORCH_V1 = "subagent_orch_v1"

_VERSIONS = {
FIRST_PROMPT_AND_SUBAGENT_NO_ORCH_V0: {
ENABLE_SMART_ROUTING_ENV_VAR: "1",
ENABLE_SUBAGENT_ROUTING_ENV_VAR: "0",
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR: "0",
},
SUBAGENT_ONLY_V0: {
ENABLE_SMART_ROUTING_ENV_VAR: "0",
ENABLE_SUBAGENT_ROUTING_ENV_VAR: "1",
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR: "0",
},
SUBAGENT_ONLY_V1: {
ENABLE_SMART_ROUTING_ENV_VAR: "1",
ENABLE_SUBAGENT_ROUTING_ENV_VAR: "1",
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR: "0",
},
SUBAGENT_ORCH_V0: {
ENABLE_SMART_ROUTING_ENV_VAR: "0",
ENABLE_SUBAGENT_ROUTING_ENV_VAR: "1",
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR: "1",
},
SUBAGENT_ORCH_V1: {
ENABLE_SMART_ROUTING_ENV_VAR: "1",
ENABLE_SUBAGENT_ROUTING_ENV_VAR: "1",
ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR: "1",
},
}


def _validate_versions(versions: Mapping[str, Mapping[str, str]]) -> None:
"""Require every version to explicitly configure the complete managed flag set."""
expected = set(SMART_ROUTING_ENV_KEYS)
for version, values in versions.items():
missing = expected - values.keys()
unexpected = values.keys() - expected
if missing or unexpected:
raise ValueError(
f"Invalid smart-routing version {version!r}: "
f"missing env vars {sorted(missing)}; unexpected env vars {sorted(unexpected)}."
)
for key, value in values.items():
if value not in ("0", "1"):
raise ValueError(
f"Invalid smart-routing version {version!r}: "
f"{key} must be '0' or '1', got {value!r}."
)


_validate_versions(_VERSIONS)


def resolve_environment(env: Mapping[str, str] | None = None) -> dict[str, str]:
"""Expand a version before applying any launch or session-specific overrides."""
resolved = dict(os.environ if env is None else env)
version = resolved.pop(SMART_ROUTER_CONFIG_VERSION_ENV_VAR, "").strip()
resolved.update(_VERSIONS.get(version, {}))
return resolved


def apply_config(env: MutableMapping[str, str] | None = None) -> dict[str, str | None]:
"""Consume the launch selector, returning the values needed to restore its input."""
target = os.environ if env is None else env
version = target.get(SMART_ROUTER_CONFIG_VERSION_ENV_VAR, "").strip()
preset = _VERSIONS.get(version)
if preset is None:
return {}
keys = (*SMART_ROUTING_ENV_KEYS, SMART_ROUTER_CONFIG_VERSION_ENV_VAR)
previous = {key: target.get(key) for key in keys}
target.update(preset)
target.pop(SMART_ROUTER_CONFIG_VERSION_ENV_VAR, None)
return previous
3 changes: 2 additions & 1 deletion src/ucode/smart_routing/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from ucode import skills
from ucode.constants import ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR
from ucode.smart_routing.config import resolve_environment
from ucode.smart_routing.hooks import sync_managed_hooks
from ucode.smart_routing.session_env import effective_environment, session_env_path

Expand All @@ -27,7 +28,7 @@


def feature_enabled(env: Mapping[str, str] | None = None) -> bool:
source = os.environ if env is None else env
source = resolve_environment(env)
return source.get(ENABLE_SMART_ROUTER_ORCHESTRATOR_ENV_VAR) == "1"


Expand Down
3 changes: 2 additions & 1 deletion src/ucode/smart_routing/session_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from ucode.config_io import atomic_write_json
from ucode.constants import SMART_ROUTING_ENV_KEYS
from ucode.smart_routing.config import resolve_environment

SESSION_ENV_VAR = "UCODE_SESSION_ENV_FILE"
SESSION_PYTHON_ENV_VAR = "UCODE_SMART_ROUTER_PYTHON"
Expand Down Expand Up @@ -53,7 +54,7 @@ def _read(path: Path) -> dict[str, str]:

def effective_environment(env: Mapping[str, str] | None = None) -> dict[str, str]:
"""Overlay the latest session controls on the hook process environment."""
effective = dict(os.environ if env is None else env)
effective = resolve_environment(env)
try:
path = session_env_path(effective)
except RuntimeError:
Expand Down
Loading
Loading