Skip to content

feat(routing): honor bundle-declared routing matrix as weakest-precedence default - #262

Open
Sam Schillace (ramparte) wants to merge 3 commits into
mainfrom
pr/bundle-declared-routing-matrix
Open

feat(routing): honor bundle-declared routing matrix as weakest-precedence default#262
Sam Schillace (ramparte) wants to merge 3 commits into
mainfrom
pr/bundle-declared-routing-matrix

Conversation

@ramparte

Copy link
Copy Markdown
Contributor

Problem

An Amplifier bundle and the active routing matrix are independent settings with no linkage, so they silently disagree. Real incident: a stale project-level .amplifier/settings.local.yaml pinned routing: matrix: anthropic while the session ran on OpenAI. Every model_role delegation resolved to zero candidates — model_role 'fast' resolved to no candidates — with no error, no attribution, and no hint that a one-line file three directories away was the cause.

The failure was silent and unattributable. These PRs close both gaps.

What this does

  1. Consumes bundle-declared routing as weakest-precedence default. If a bundle declares routing.matrix: anthropic, the session inherits it unless overridden by settings files.

  2. Adds routing attribution. A new function get_routing_config_with_source() reports WHICH settings file (or bundle) set the active matrix, making failures diagnosable. Directly closes the incident's attribution gap.

  3. Extracts matrix discovery to non-fetching path. New lib/routing_matrices.py with discover_matrix_files(fetch=False) and known_matrix_names() ensures the session path never hits the network. commands/routing.py fetches with fetch=True. No more surprise network calls during session startup.

  4. Warns and falls back when bundle names uninstalled matrix. A bundle default must never brick a session, but must fail loudly. Falls back to system default with a clear warning.

  5. Adds attribution output. amplifier routing show and amplifier bundle show now report which source provided the active matrix.

Precedence

Bundle value is the WEAKEST source. Precedence, weakest to strongest:

  • built-in default
  • bundle-declared routing.matrix
  • user ~/.amplifier/settings.yaml
  • project .amplifier/settings.yaml
  • project .amplifier/settings.local.yaml

⚠️ Scope Disclosure — Please Read

This PR carries three commits. The first two are the feature; the third is necessary. Read carefully:

Commit 34ea3cb: fix: prepare routing before config injection

This commit introduces required_behaviors on load_and_prepare_bundle and moves routing-behavior composition before prepare(), which the feature builds on. Neither change exists on origin/main.

Scope change: This commit changes failure semantics: routing composition failures now propagate as hard errors instead of being swallowed as silent warnings. This may surface failures in environments that relied on the previous silent-warning behavior.

Reviewers should bless this deliberately. Also note: a sibling, never-reconciled implementation of the same ordering fix exists on branch fix/compose-routing-before-prepare (c2b4de7); 34ea3cb was chosen because the feature was literally built on it, not because the two were evaluated on merit — worth confirming which is intended.

Backward compatibility

No routing: key in bundle means unchanged behavior (pinned by test_bundle_without_routing_behaves_identically_to_today). Reads the field via getattr(bundle, "routing", {}) or {}, verified against the currently installed foundation which lacks the field, so this can land before or after the companion PR.

Testing

1301 -> 1321 passing (+20: 13 feature tests, 7 from 34ea3cb). Pyright identical to baseline after 8e67424.

Lint Note

lib/settings.py:607 adds 2 ruff findings (S112/BLE001) from an except Exception: continue in get_routing_config_with_source(). This copies an existing convention used 3x elsewhere in the same file, all currently unflagged/unfixed. Left consistent rather than selectively suppressed — reviewer's call whether to fix all four together.

Non-goals

  • No --matrix CLI flag
  • No profile system
  • No provider/matrix compatibility validation (that ships separately as a session:start hook)

Companion PR

Link to foundation PR will be inserted here.

…ence default

Bundles can now declare a default routing matrix via a top-level
`routing:` section in their frontmatter (companion PR to
amplifier-foundation adds `routing: dict[str, Any]` to the Bundle
dataclass, deep-merged on compose). This is consumed as the WEAKEST
source in the precedence chain:

  built-in default < bundle-declared routing.matrix
  < ~/.amplifier/settings.yaml < .amplifier/settings.yaml
  < .amplifier/settings.local.yaml

If a bundle declares no matrix, behavior is exactly as before this
change -- the bundle value is a default only.

Changes:
- lib/settings.py: add AppSettings.get_routing_config_with_source(),
  returning (merged config, highest-precedence scope file that set
  routing.matrix). get_routing_config() becomes a thin wrapper.
- lib/routing_matrices.py (new): extract matrix file discovery out of
  commands/routing.py into discover_matrix_files(fetch=bool) and
  known_matrix_names() (non-fetching, safe for the session-start hot
  path). commands/routing.py delegates with fetch=True.
- lib/bundle_loader/prepare.py: add on_bundle_loaded callback to
  load_and_prepare_bundle(), invoked right after the bundle (and its
  includes) load but BEFORE any app-policy behavior is composed onto
  it -- so an app-injected behavior can never masquerade as a bundle
  default. Returned behavior URIs are appended to compose_behaviors
  and required_behaviors.
- runtime/config.py: read the bundle's routing default via
  getattr(bundle, "routing", {}) or {} for forward-compat with
  foundation installs that lack the field. Merge bundle default under
  user settings (user wins key-by-key), drop+warn on an unknown
  bundle-declared matrix name (never bricks a session), and print
  observability messages (bundle default won / user override conflict
  / unknown matrix) after the prepare spinner stops.
- commands/routing.py: `routing show` gains a Source: line.
  commands/bundle.py: `bundle show` gains a Routing matrix: line.
- README.md: document the precedence chain and Source: line.

Tests: tests/test_bundle_routing_matrix.py (9),
tests/test_settings_routing_source.py (4). Updated
tests/test_routing_matrix_registration.py and
tests/test_general_config_overrides.py for the routing_matrices.py
extraction and get_routing_config_with_source() call site change.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
pyright flags `prepare.await_args.kwargs` as reportOptionalMemberAccess
since unittest.mock stubs type `await_args` as `_Call | None`. Add an
explicit `assert prepare.await_args is not None` before each access so
the new routing-composition tests (introduced by the bundle-declared
routing matrix feature) produce zero new pyright findings versus the
origin/main baseline. Test behavior and assertions are unchanged.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
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