Skip to content

Thread root agent's system_prompt into sub-agent decomposition prompts - #151

Closed
acdonaire wants to merge 2 commits into
ctrl-gaurav:mainfrom
acdonaire:fix/decomposition-language-leak
Closed

Thread root agent's system_prompt into sub-agent decomposition prompts#151
acdonaire wants to merge 2 commits into
ctrl-gaurav:mainfrom
acdonaire:fix/decomposition-language-leak

Conversation

@acdonaire

Copy link
Copy Markdown
Contributor

Problem

DecompositionEngine's parallel/sequential/hybrid prompt templates are
hardcoded in English and don't inherit the root agent's system_prompt.
In AgentMode.AUTO/SUB_AGENTS, this can leak English-language reasoning
into generated subtasks regardless of the agent's configured language —
found while testing PR #150's Spanish keyword support with a real
multi-agent run.

Solution

Thread system_prompt through context (root agent -> SubAgentRouter.route
-> DecompositionEngine.decompose) and use it to add an explicit language
instruction to the LLM decomposition prompt. No-op when system_prompt
is unset.

Validation

  • 137/137 existing tests pass unmodified
  • Verified with a mock llm_client: prompt sent to the model now includes
    the language instruction when system_prompt is set, and is byte-identical
    to before when it isn't
  • No behavior change for callers that don't set system_prompt

🤖 Generated with Claude Code

https://claude.ai/code/session_01JGxRqBhgH19YxWzGyrXqJr

acdonaire and others added 2 commits September 5, 2026 12:54
The parallel/sequential/hybrid decomposition prompt templates in
DecompositionEngine were hardcoded English text with no awareness of
the root agent's configured language or persona. In AgentMode.AUTO/
SUB_AGENTS, this meant generated subtask descriptions (and their
internal reasoning) could be in English even when system_prompt asked
for another language — verified empirically: a Spanish task with
system_prompt="Responde siempre en español" produced a subtask literally
titled "synthesis_specialist" with instruction "Synthesize the
findings...".

Fix: agent_orchestration.py now threads the agent's system_prompt into
the context dict passed to SubAgentRouter.route(), and
DecompositionEngine._llm_decompose() uses it to add an explicit
language instruction to the LLM decomposition prompt. No-op when
system_prompt is unset or when DecompositionEngine/SubAgentRouter are
used standalone without a parent Agent — behavior is unchanged in
both cases.

Tests: 137/137 passing across test_agent_decomposition.py,
test_agent_module_layout.py, and test_orchestration.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGxRqBhgH19YxWzGyrXqJr
Two related gaps, both stemming from the same root cause (system_prompt
not propagating past the root agent):

- sub_agent_manager.py: each specialization's default system_prompt
  (research/coding/analysis/synthesis/general) is a fixed English
  persona. The parent's model is reused (`model="inherit"`) but its
  system_prompt never was — spawned children kept an English persona
  regardless of the parent's configured language. Now appends a note
  derived from the parent's system_prompt, when set.

- orchestrator.py (defensive, lower severity): the hierarchical team's
  decomposition/synthesis prompts are fixed English text. This path
  already applies the manager's own system_prompt via a normal
  Agent.run() call, unlike DecompositionEngine's raw generate() bypass
  (see ctrl-gaurav#151), so it was less exposed — added an explicit language note
  anyway to remove ambiguity in the delegation-format instruction.

Found while auditing the codebase for the same class of bug fixed in
ctrl-gaurav#151.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGxRqBhgH19YxWzGyrXqJr
ctrl-gaurav pushed a commit that referenced this pull request Sep 5, 2026
Two related gaps, both stemming from the same root cause (system_prompt
not propagating past the root agent):

- sub_agent_manager.py: each specialization's default system_prompt
  (research/coding/analysis/synthesis/general) is a fixed English
  persona. The parent's model is reused (`model="inherit"`) but its
  system_prompt never was — spawned children kept an English persona
  regardless of the parent's configured language. Now appends a note
  derived from the parent's system_prompt, when set.

- orchestrator.py (defensive, lower severity): the hierarchical team's
  decomposition/synthesis prompts are fixed English text. This path
  already applies the manager's own system_prompt via a normal
  Agent.run() call, unlike DecompositionEngine's raw generate() bypass
  (see #151), so it was less exposed — added an explicit language note
  anyway to remove ambiguity in the delegation-format instruction.

Found while auditing the codebase for the same class of bug fixed in
#151.
@ctrl-gaurav

Copy link
Copy Markdown
Owner

Merged into main — thank you. Finding this while testing #150 with a real multi-agent run is exactly how it should have been found; it isn't visible from the unit tests.

Your commits landed with your authorship intact:

  • e3c1c35 Thread root agent's system_prompt into sub-agent decomposition prompts
  • c11aca9 Propagate root agent's system_prompt to spawned sub-agents

Why this shows as closed rather than merged: the commits were replayed onto main rather than merged as-is, which changed their hashes, so GitHub can't link them back automatically. The author on both is still you, so they count as your contributions.

The code went in unchanged. Two things I checked specifically, because both would have been quiet failures:

  • parent is in scope at the point you use it in _run_real_sub_agent (parent = self.parent_agent, line 565) — I flagged it while reading the diff, since the signature is (self, subtask, config), and it's fine.
  • All four .format() call sites on the templates pass language_note, and those templates aren't formatted anywhere else, so no KeyError is reachable.

Your "no-op when unset" claim held up: I asserted the decomposition prompt is byte-identical with no system_prompt, with context={}, and with an unrelated context key, across all four strategy branches.

One thing needed fixing before it could land (8285873): self.config.system_prompt in agent_orchestration.py pushed the mypy ratchet over its baseline — "AgentOrchestrationMixin" has no attribute "config" went 10 → 12, and scripts/mypy_ratchet.py fails the build on any increase. Rather than re-record the baseline, I declared config: AgentConfig in the mixin's existing TYPE_CHECKING block, which is what AgentStreamingMixin already does and which resolves all 12 rather than just the 2 you added. Worth running python scripts/mypy_ratchet.py locally before opening a PR — it's cheap and it's a required check.

I also added tests/unit/test_system_prompt_threading.py (12 cases, 5 of which fail against the pre-merge tree) covering the byte-identical no-op path, the note reaching all four strategies, and the 200-character truncation — that truncation is a good call and worth having pinned, since without it a long persona would crowd the task out of the prompt.

One small comment tidy in orchestrator.py (1d54274): the (PR #151) reference was removed, since shipped source here describes behaviour rather than the process that produced it. The sentence reads the same without it.

@ctrl-gaurav

Copy link
Copy Markdown
Owner

Landed on main as e3c1c35 and c11aca9. Closing — see the review comment above for what changed on top and why this reads as closed rather than merged.

@ctrl-gaurav ctrl-gaurav closed this Sep 5, 2026
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