Skip to content

fix(routing): make the settings-injected hooks-routing loadable on a bundle that does not ship it - #295

Merged
Brian Krabach (bkrabach) merged 1 commit into
mainfrom
fix/settings-injected-hooks-routing-loadable
Sep 3, 2026
Merged

fix(routing): make the settings-injected hooks-routing loadable on a bundle that does not ship it#295
Brian Krabach (bkrabach) merged 1 commit into
mainfrom
fix/settings-injected-hooks-routing-loadable

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

The defect

routing.matrix: <name> in settings.yaml injects a hooks-routing override (runtime/config.py). When the active bundle does not already include the routing-matrix bundle, _apply_hook_overrides appends that override verbatim as the mount-plan entry — and the hook then fails at mount, silently:

Failed to load hook 'hooks-routing': Module 'hooks-routing' not found in prepared bundle. Available modules: [...]

No error reaches the user. What they see instead: no routing banner in the system prompt, no model_role parameter on the delegate tool (tool-delegate drops it when no model_role_resolver capability is registered), and every model_role falling through to the default provider.

Why it looked like it worked

Any session on a bundle that does include routing-matrix (foundation) editable-installs amplifier_module_hooks_routing into the app venv as a side effect, registering an amplifier.modules entry point. From then on the source-less injection resolves via the installed-package fallback for every bundle — until uv tool install / auto-update recreates the venv, and routing silently vanishes for every bundle that doesn't ship the hook.

Measured on anchors-amp-dev with routing.matrix: anthropic, across exactly such a reinstall (2026.09.03-b85867c): system prompt shrank 101,106 → 99,832 chars (the banner), delegate lost model_role, sub-agents ran on the priority-1 default provider.

Two independent gaps, both needed

1. No source on the injected entry (runtime/config.py). Nothing in the resolver chain knows where hooks-routing lives.

2. Even with a source, it couldn't load (lib/bundle_loader/resolvers.py). The entry reaches the mount plan after Bundle.prepare(), so it isn't in the bundle resolver's activated _paths. Foundation's BundleModuleResolver.async_resolve exists precisely to lazily activate such a module from its hint — but the kernel loader only takes that path when the mounted resolver exposes async_resolve, and AppModuleResolver (mounted at root as module-source-resolver, inherited by every child) did not. It fell to the sync path, where except ModuleNotFoundError: caught the builtin class while the bundle resolver raises amplifier_core.module_sources.ModuleNotFoundError — not a subclass — so the settings fallback (which honours source hints) never ran either.

The fix

runtime/config.py — when the bundle has no hooks-routing of its own, attach the canonical source: WELL_KNOWN_BUNDLES["routing-matrix"]["remote"] (the same entry amplifier routing / amplifier update already use) narrowed to #subdirectory=modules/hooks-routing, matching the bundle's own behaviors/routing.yaml. Append path only: merge_module_items lets an override's top-level keys win, so attaching unconditionally would clobber a bundle's deliberately pinned source.

lib/bundle_loader/resolvers.pyAppModuleResolver.async_resolve: bundle lazy activation first (sync resolve for an older foundation without it), then the settings fallback, then the same informative error. Catch both ModuleNotFoundError classes on both paths so the fallback policy actually applies. Policy unchanged: bundle → settings → error.

Measured — anchors-amp-dev, app venv wiped of the side-effect install, same prompt

delegate(agent='self', model_role='fast'):

before after
hooks-routing load Failed to load hook 'hooks-routing' loads; source = git+…routing-matrix@main#subdirectory=modules/hooks-routing
routing banner in system prompt
delegate:agent_spawned model_role=null (param absent) model_role='fast'[{provider: haiku, model: claude-haiku-4-5-20251001}]
child mount plan default provider pri=0 id=haiku effort=high fallback_on_overload=None
child llm:request opus-5 @ xhigh anthropic/claude-haiku-4-5-20251001, thinking budget 32000
[PROVIDER] warnings 0

Lazy activation installed the module into the venv on that run (entry point now present), so subsequent sessions are fast-path.

Tests

1647 pass, up from 1634. No existing test changed. 13 new:

  • TestRoutingHookSource / TestRoutingHookSourceIntegration (through the real resolve_bundle_config): source attached when the bundle lacks the hook; attached when the bundle has no hooks at all; a bundle's pinned source is never clobbered; no routing.matrix → nothing injected; source derives from WELL_KNOWN_BUNDLES.
  • TestAppModuleResolverLazyActivation: pins that the kernel exception is not the builtin; sync fallback now runs on the kernel class; async_resolve delegates to bundle lazy activation with the hint; falls back to settings on kernel exception; works with a bundle resolver lacking async_resolve; raises the informative error when both fail; profile_hint alias.

Verified the new resolver tests fail against the unfixed resolvers.py (6/7 — the 7th pins the exception-class fact). The pre-existing AppModuleResolver tests only ever raised the builtin class, which is how the mismatch survived. ruff check + ruff format --check clean on all four touched files.

Related

microsoft/amplifier-bundle-routing-matrix#57 fixed the other half of the reported symptom — once routing loads, the fast role now selects the instance configured for haiku instead of the priority-1 opus instance, so the child no longer inherits reasoning_effort: xhigh / fallback_on_overload: true. This PR is what makes that fix reachable on a bundle that doesn't ship routing-matrix.

…bundle that does not ship it

`routing.matrix: <name>` in settings.yaml injects a `hooks-routing` override
(runtime/config.py). When the active bundle does not already include the
routing-matrix bundle, `_apply_hook_overrides` APPENDS that override verbatim as
the mount-plan entry. Two independent gaps then combined to make the hook fail
at mount, silently:

1. The override carried no `source`. Nothing in the resolver chain knows where
   `hooks-routing` lives, so the kernel failed it by name:

     Failed to load hook 'hooks-routing': Module 'hooks-routing' not found in
     prepared bundle. Available modules: [...]

2. Even WITH a source, the module could not load. It reaches the mount plan
   AFTER Bundle.prepare() (settings are applied to the prepared mount plan), so
   it is not in the bundle resolver's activated `_paths`. Foundation's
   `BundleModuleResolver.async_resolve` exists precisely to lazily activate such
   a module from its source hint -- but the kernel loader only takes that path
   when the mounted resolver exposes `async_resolve`, and `AppModuleResolver`
   (mounted at root as `module-source-resolver`, inherited by every child) did
   not. The loader fell to the sync path, and there `except ModuleNotFoundError`
   caught the BUILTIN class while the bundle resolver raises
   `amplifier_core.module_sources.ModuleNotFoundError`, which is not a subclass
   of it -- so the settings fallback (which does honor source hints) never ran
   either.

Why it looked like it worked: any session on a bundle that DOES include
routing-matrix (foundation) editable-installs `amplifier_module_hooks_routing`
into the app venv as a side effect, registering an `amplifier.modules` entry
point. From then on the source-less injection resolves via the installed-package
fallback for every bundle -- until `uv tool install` / auto-update recreates the
venv, at which point routing silently vanishes for every bundle that does not
ship the hook. Measured on `anchors-amp-dev` with `routing.matrix: anthropic`
across exactly such a reinstall: the routing banner left the system prompt
(101,106 -> 99,832 chars), the delegate tool dropped its `model_role` parameter
(no `model_role_resolver` capability), and every `model_role` fell through to
the default provider. No error reached the user.

THE FIX (two halves, both needed)

runtime/config.py: when the bundle has no hooks-routing of its own, attach the
canonical source -- `WELL_KNOWN_BUNDLES["routing-matrix"]["remote"]` (the same
entry `amplifier routing` / `amplifier update` use) narrowed to
`#subdirectory=modules/hooks-routing`, matching the bundle's own
behaviors/routing.yaml. Only on the append path: merge_module_items lets an
override's top-level keys win, so attaching unconditionally would clobber a
bundle's deliberately pinned source.

lib/bundle_loader/resolvers.py: give `AppModuleResolver` an `async_resolve`
that delegates to the bundle resolver's lazy activation (falling back to sync
resolve for an older foundation), then the settings fallback, then the same
informative error. Catch BOTH ModuleNotFoundError classes on both paths so the
fallback policy actually applies.

MEASURED, anchors-amp-dev, app venv wiped of the side-effect install (the
post-reinstall condition), same prompt: delegate(agent=self, model_role=fast)

  before  Failed to load hook 'hooks-routing' ... not found in prepared bundle
          routing banner in system prompt: False
  after   hooks-routing source = git+...routing-matrix@main#subdirectory=modules/hooks-routing
          routing banner in system prompt: True
          delegate:agent_spawned model_role='fast'
            provider_preferences=[{provider: haiku, model: claude-haiku-4-5-20251001}]
          child: pri=0 id=haiku effort=high fallback_on_overload=None
          child llm:request: anthropic/claude-haiku-4-5-20251001, 0 [PROVIDER] warnings

Tests: 13 new. 1647 pass, up from 1634; no existing test changed. The new
resolver tests fail against the unfixed resolvers.py (6/7; the 7th pins the
exception-class fact). The pre-existing AppModuleResolver tests only ever
raised the builtin class, which is how the mismatch survived.
@bkrabach
Brian Krabach (bkrabach) merged commit 07e008a into main Sep 3, 2026
7 of 9 checks passed
@bkrabach
Brian Krabach (bkrabach) deleted the fix/settings-injected-hooks-routing-loadable branch September 3, 2026 07:07
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.

2 participants