[eric] agents: a custom system prompt bites on the FIRST reply; it was delivered but buried mid-prompt and ignored

This commit is contained in:
ciregenz
2026-08-15 20:48:12 -07:00
parent aec8ac90ec
commit 7853884d75
2 changed files with 46 additions and 1 deletions
@@ -442,7 +442,13 @@ AGENT_IDENTITY = (
@typechecked
def compose_system_prompt(default_prompt: Optional[str], mode_prompt: Optional[str], session_prompt: Optional[str], browser_ctx: Optional[str] = None, mcp_registry_ctx: Optional[str] = None, skills_catalog_ctx: Optional[str] = None) -> Optional[str]:
# Identity always leads so it overrides the preset's Claude Code persona, even when the user has no custom default/mode/session prompt of their own.
parts = [AGENT_IDENTITY] + [p for p in (default_prompt, mode_prompt, session_prompt, mcp_registry_ctx, skills_catalog_ctx, browser_ctx) if p]
# The session prompt goes LAST with explicit priority framing: buried mid-prompt it was delivered but ignored on turn one (Haik's hackathon blocker; live-proven with a marker token both ways).
framed_session = (
"## Instructions from your operator (highest priority)\n"
"The person who configured this agent wrote the following. Follow it over any earlier "
"guidance in this prompt, starting from your very first reply.\n\n" + session_prompt
) if session_prompt else None
parts = [AGENT_IDENTITY] + [p for p in (default_prompt, mode_prompt, mcp_registry_ctx, skills_catalog_ctx, browser_ctx, framed_session) if p]
return "\n\n".join(parts)
@@ -0,0 +1,39 @@
"""A custom system prompt must actually bite on the FIRST message (Haik's hackathon blocker).
The prompt was always DELIVERED (verified in the live CLI argv, and the model could quote it when
asked), but it sat mid-prompt between the identity block and the MCP registry, after the whole
default prompt, and the model ignored it: a marker-token drill on the packaged build showed 0/3
arms applying it. Moving it last with explicit operator framing flipped all arms to applied.
These pin the structural half of that fix, the part a unit test can hold: position and framing.
The behavioural half lives in the live drill (scratchpad/sysprompt_test.sh pattern).
"""
from backend.apps.agents.manager.prompt.prompt_context import compose_system_prompt
SESSION = "Always answer in pirate speak."
DEFAULT = "Be helpful and concise."
MODE = "You are in coding mode."
def test_session_prompt_is_the_last_content_in_the_composed_prompt():
out = compose_system_prompt(DEFAULT, MODE, SESSION, "browser ctx", "mcp ctx", "skills ctx")
assert out is not None
assert out.rstrip().endswith(SESSION), "anything after the operator's words dilutes them again"
def test_session_prompt_carries_priority_framing():
out = compose_system_prompt(DEFAULT, MODE, SESSION)
assert "highest priority" in out
assert "your very first reply" in out, "turn one is the reported failure; the framing must name it"
assert out.index("highest priority") < out.index(SESSION), "the framing introduces the prompt, not trails it"
def test_no_session_prompt_means_no_framing_block():
out = compose_system_prompt(DEFAULT, MODE, None, "browser ctx")
assert "highest priority" not in out, "an empty operator block would be framing around nothing"
assert DEFAULT in out and "browser ctx" in out
def test_default_and_mode_prompts_still_precede_context_blocks():
out = compose_system_prompt(DEFAULT, MODE, SESSION, "browser ctx", "mcp ctx")
assert out.index(DEFAULT) < out.index(MODE) < out.index("mcp ctx") < out.index(SESSION)