Skip to content

fix(subprocess): register the agent system instruction in subprocess-spawned child sessions - #295

Open
Brian Krabach (bkrabach) wants to merge 1 commit into
mainfrom
fix/subprocess-child-system-instruction
Open

fix(subprocess): register the agent system instruction in subprocess-spawned child sessions#295
Brian Krabach (bkrabach) wants to merge 1 commit into
mainfrom
fix/subprocess-child-system-instruction

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Summary

Subprocess-spawned agents were silently dropping their system instruction (persona). The instruction key crossed the process boundary intact inside config (via merge_configs() on the parent side), but nothing on the subprocess child path ever read it.

Root cause: Two defects, now fixed:

  1. Defect 1 (shipped bug): System instruction never registered in subprocess child

    • config['instruction'] present but never consumed
    • Agent runs with NO persona at all (generic session)
    • Live in production: amplifier-bundle-dot-graph recipes use spawn_mode: subprocess
  2. Defect 2 (found in review): @mention bleed-through via shared deduplicator

    • First fix version shared session's ContentDeduplicator across persona + prompt expansion
    • ContentDeduplicator.get_unique_files() returns EVERY file ever seen, re-serializes on each call
    • Persona content leaked into prompt context block (verified: 205 chars extra per turn)

Fix

For Defect 1:

In _run_child_session, resolve config.get('instruction') or config.get('system', {}).get('instruction') with an isinstance dict guard, expand @mentions, then register via:

  • Preferred: set_system_prompt_factory(...) (allows composable hooks)
  • Fallback: add_message({'role': 'system', ...}) (for contexts without factory)

Mirrors in-process path in amplifier-app-cli/session_spawner.py exactly.

For Defect 2:

Persona expansion now uses a fresh ContentDeduplicator() instead of sharing the session's instance, matching the already-correct pattern at amplifier_foundation/bundle/_prepared.py:411-412 ("Fresh deduplicator each call").

Verification Evidence

RED proven twice, independently:

Pre-fix probe (persona present in config, nothing registered):

AssertionError: SUBPROCESS CHILD NEVER RECEIVES THE AGENT PERSONA.
    config['instruction'] was present: 'You are ROB-PERSONA-MARKER-9x7. Always a'...
    set_system_prompt_factory awaited: 0
    add_message(role=system) calls   : 0
    => the agent runs with NO persona; it is a generic session.

Revert-production-only (tests kept):

FAILED TestChildSystemInstruction::test_system_instruction_registers_factory
FAILED TestChildSystemInstruction::test_system_instruction_fallback_add_message_when_no_factory
FAILED TestChildSystemInstruction::test_system_instruction_nested_system_dict_form
FAILED TestChildSystemInstruction::test_system_instruction_mentions_expanded
4 failed, 58 passed

Bleed-through regression test, RED-proven by reverting only the dedup line:

E       assert 'PERSONA-FILE-CONTENT-XYZ' not in '<context_fi...task.md now.'
E         'PERSONA-FILE-CONTENT-XYZ' is contained here:
E           sona.md">
E           PERSONA-FILE-CONTENT-XYZ
E           </context_file>
FAILED TestChildSystemInstruction::test_persona_mentions_do_not_bleed_into_prompt

Test suite results:

  • Baseline on main: 1549 passed
  • This branch: 1556 passed (+7 new persona tests)
  • All tests green, zero failures

Lint: ruff check and ruff format --check both clean.

Review Trail

  1. Independent code review: APPROVED, two non-blocking follow-ups (one suggested regression test, now included)
  2. Council review (six lenses): 5/6 initially said DO-NOT-MERGE-YET due to potential @mention bleed-through. Executed the real dedup code and proved the bleed-through exists. The fix (fresh deduplicator) and regression test (RED-proven) are now included. All council concerns resolved.

Known Remaining Gaps

  • Three hand-synced implementations of "install a persona" now exist: bundle/_prepared.py, amplifier-app-cli/session_spawner.py, and this fix. Tied together only by comments. This bug's existence IS a sync failure. Should be ticketed.
  • session_spawner.py (in-process) still shares one deduplicator across its persona and instruction expansions — the same bleed-through defect, pre-existing, out of scope. Should be ticketed against amplifier-app-cli.
  • Tests mocked at AmplifierSession boundary; no end-to-end subprocess run with real session included.

Files Changed

  • amplifier_foundation/subprocess_runner.py — Added persona registration block in _run_child_session (after capability registration, before prompt expansion)
  • tests/test_subprocess_runner.py — Added TestChildSystemInstruction class with 7 tests (factory registration, fallback path, nested form, no-instruction, @mention expansion, bleed-through regression, context-is-None no-crash)

…spawned child sessions

Subprocess-spawned agents were silently dropping their system instruction (persona).
The 'instruction' key crossed the process boundary intact inside 'config' (via
merge_configs() on the parent side), but nothing on the subprocess child path ever
read it. This left subprocess-spawned agents running as generic sessions with NO
persona at all.

Fix: in _run_child_session, resolve config.get('instruction') or config.get('system',
{}).get('instruction') with an isinstance dict guard, expand @mentions using a fresh
ContentDeduplicator (not the session's shared instance, which would cause mention
content to bleed through into the prompt's context block), then register via
set_system_prompt_factory (preferred) or add_message(role=system) (fallback).

Also fixed a regression defect: the first version shared the session's
ContentDeduplicator across persona expansion and prompt expansion. Since
ContentDeduplicator.get_unique_files() returns every file the instance has ever
seen and re-serializes the whole set on each call, this made the persona's resolved
@mention content leak into the prompt's context block, costing tokens on every
affected turn. Now uses a fresh deduplicator for persona expansion, matching the
already-correct pattern at bundle/_prepared.py:411-412.

Evidence:
- RED proven (before fix): config['instruction'] present, set_system_prompt_factory
  awaited 0 times, subprocess child runs with NO persona
- GREEN proven (after fix, revert-only): 1556 tests pass (+7 new persona tests)
- Regression test added: test_persona_mentions_do_not_bleed_into_prompt, RED-proven
  by reverting only the fresh-dedup line (persona content bleeds, test fails)

Test suite: baseline 1549 passed, this branch 1556 passed (all new tests pass).
Lint: ruff check and ruff format --check both clean.

Reviews:
- Independent code review: APPROVED, two non-blocking follow-ups
- Council (six lenses): 5/6 DO-NOT-MERGE-YET (regression test added, now passing),
  1 FAIL (same issue, same fix applied)

Known remaining gaps:
- Three hand-synced implementations of 'install a persona' now exist (bundle,
  app-cli, foundation). This bug's existence IS a sync failure. Should be ticketed.
- session_spawner.py (in-process) still shares one deduplicator across its persona
  and instruction expansions. Pre-existing, out of scope here. Should be ticketed.
- Tests mocked at AmplifierSession boundary; no end-to-end subprocess run with
  real session included.

🤖 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.

2 participants