fix(spawn): register system-prompt factory for sub-sessions so prefix-placement hooks can compose - #257
Merged
Brian Krabach (bkrabach) merged 1 commit intoAug 5, 2026
Conversation
…-placement hooks can compose The session_spawner spawn_sub_session path was injecting the system instruction as a static message via add_message() and never registered a system-prompt factory. The skills-visibility hook uses placement='prefix' by default, which works by composing/wrapping the context module's registered factory. At tool-skills/hooks.py:216-221 it explicitly refuses to wrap when no factory exists -- correctly, because in context-simple a registered factory takes precedence over static system messages (context-simple/__init__.py:268-271), so wrapping a nonexistent factory would silently drop the agent's instruction. Result: every delegated sub-agent hit the fallback path and re-injected the entire skills index on every provider:request outside the cached prefix, plus emitted a confusing warning blaming the context module. The context module was never at fault -- context-simple implements set_system_prompt_factory at __init__.py:195. amplifier-foundation already does this correctly in both its session paths (_prepared.py:661,664 and :874,875). app-cli's session_spawner -- the path that actually runs every delegate/Task sub-agent -- was the only gap. This fix mirrors the foundation _prepared.py spawn path: prefer factory registration, fall back to add_message. The instruction content is byte-identical to before; the factory is a closure returning that resolved string, bound to a local to avoid late-binding risk. Real verification (driving the actual context-simple module and SkillsVisibilityHook with 108-skill catalog): BEFORE FIX (session_spawner add_message only) prefix placement achieved : False hook action : inject_context per-request injection : 2,218 chars skills index IN system msg: False agent instruction present : True warnings : 1 AFTER FIX (session_spawner registers factory) prefix placement achieved : True hook action : continue per-request injection : 0 chars skills index IN system msg: True agent instruction present : True warnings : 0 Test suite delta: +4 new tests, 1301 passed (baseline 1297), no regressions. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Brian Krabach (bkrabach)
deleted the
fix/spawn-register-system-prompt-factory
branch
August 5, 2026 18:44
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.
Summary
Delegated sub-agent sessions were paying an uncached token tax on every single LLM request, for the entire life of the sub-session.
session_spawner.pyinjected the agent's system instruction as a STATIC message viaadd_message()and never registered a system-prompt factory.The skills-visibility hook defaults to
placement="prefix", which works by WRAPPING the context module's registered system-prompt factory so the skills index rides the provider's stable cached prefix. Attool-skills/hooks.py:216-221it explicitly REFUSES to wrap when no factory is registered — correctly, because incontext-simplea registered factory takes precedence over static system messages (context-simple/__init__.py:268-271), so wrapping a nonexistent factory would silently DROP the agent's instruction.Result: every delegated sub-agent hit the fallback path and re-injected the entire skills index as a fresh per-request message on every
provider:request, outside the cached prefix, plus emitted a confusing warning blaming the context module.The context module was never at fault.
context-simpleimplementsset_system_prompt_factoryat__init__.py:195.amplifier-foundationalready does this correctly in both its session paths (_prepared.py:661,664and:874,875). app-cli'ssession_spawner— the path that actually runs everydelegate/Task sub-agent — was the only gap.The Fix
amplifier_app_cli/session_spawner.py:788— prefer factory registration, fall back toadd_message, mirroring the foundation_prepared.pyspawn path. The instruction content is byte-identical to before (system_instructionis already resolved at that point; the factory is a closure returning that resolved string, bound to a local to avoid late-binding). No re-expansion per request — deliberately out of scope.Verification Evidence
Test suite, run by the reviewer independently:
main:1297 passed, 1 skipped, 13 deselected, 1 xfailed1301 passed, 1 skipped, 13 deselected, 1 xfailedEnd-to-end mechanism proof driving the REAL
context-simplemodule and the REALSkillsVisibilityHook(108-skill synthetic catalog):Critically,
agent instruction present: Truein BOTH cases — the fix does not drop the system instruction, which is precisely the hazard the hook's refusal-to-wrap guard exists to prevent.Note on magnitude: The proof used 108 synthetic skills with short descriptions (~2.2KB). A real catalog carries ~19,766 bytes of skill descriptions (~5k tokens), so the real-world per-request saving is substantially larger. The mechanism is what the proof establishes; the magnitude scales with the actual catalog.
Lint/typecheck:
ruff checkon both changed files → All checks passed (baseline and post-change)pyright session_spawner.py→ 3 errors, all pre-existing and unrelated (lines 486, 577, 578), identical before and afterruff formatdebt in test_session_spawner.py present in baseline tooAlso checked for the same bug elsewhere in the package: The only other
add_message({"role": "system"...})-shaped call site is the resume/transcript-restore path (~line 1298), which replays a saved transcript message-by-message. Different semantics, not the same bug, deliberately left unchanged.