diff --git a/tests/test_session.py b/tests/test_session.py index cd91f612..55bf375c 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -338,6 +338,28 @@ class TestPlanExec: # Last user message in second call is the coaching message assert "did not follow" in captured_messages[1][-1]["content"] + def test_plan_includes_template_content(self, tmp_db, tmp_path, monkeypatch): + """Plan agent system message includes template guardrails.""" + monkeypatch.chdir(tmp_path) + session = _make_session() + session._template_content = "SAFETY: Do not produce harmful plans." + _, _, messages = self._run_plan(session, "build something") + sys_content = messages[0]["content"] + assert "SAFETY: Do not produce harmful plans." in sys_content + assert ChatSession._PLAN_IDENTITY in sys_content + # Template appears before plan identity + tpl_pos = sys_content.index("SAFETY:") + identity_pos = sys_content.index(ChatSession._PLAN_IDENTITY) + assert tpl_pos < identity_pos + + def test_plan_no_template_is_identity_only(self, tmp_db, tmp_path, monkeypatch): + """Without templates, plan system message is exactly _PLAN_IDENTITY.""" + monkeypatch.chdir(tmp_path) + session = _make_session() + assert session._template_content is None + _, _, messages = self._run_plan(session, "build something") + assert messages[0]["content"] == ChatSession._PLAN_IDENTITY + # --------------------------------------------------------------------------- # Plan validation @@ -557,6 +579,27 @@ class TestPlanRefinement: assert msgs[3]["role"] == "user" assert "add tests too" in msgs[3]["content"] + def test_refine_plan_includes_template_content(self, tmp_db, tmp_path, monkeypatch): + """_refine_plan system message includes template guardrails.""" + monkeypatch.chdir(tmp_path) + session = _make_session() + session._template_content = "SAFETY: guardrails here" + captured = {} + + def fake_run_agent(messages, **kwargs): + captured["messages"] = list(messages) + return self.GOOD_PLAN + + with patch.object(session, "_run_agent", side_effect=fake_run_agent): + session._refine_plan(self.GOOD_PLAN, "add auth", "add tests too") + + sys_content = captured["messages"][0]["content"] + assert "SAFETY: guardrails here" in sys_content + assert ChatSession._PLAN_IDENTITY in sys_content + tpl_pos = sys_content.index("SAFETY:") + identity_pos = sys_content.index(ChatSession._PLAN_IDENTITY) + assert tpl_pos < identity_pos + # --------------------------------------------------------------------------- # Vision / image support diff --git a/turnstone/core/session.py b/turnstone/core/session.py index ff80bf19..6e0d452a 100644 --- a/turnstone/core/session.py +++ b/turnstone/core/session.py @@ -3549,6 +3549,16 @@ class ChatSession: "and functions in every step." ) + def _plan_system_content(self) -> str: + """Plan agent system message: template guardrails + plan identity.""" + if not self._template_content: + return self._PLAN_IDENTITY + tpl = self._template_content + if len(tpl) > _MAX_TEMPLATE_CONTENT: + log.warning("template_content.truncated", length=len(tpl), agent="plan") + tpl = tpl[:_MAX_TEMPLATE_CONTENT] + return tpl + "\n\n" + self._PLAN_IDENTITY + _MIN_PLAN_LENGTH = 100 _PLAN_REQUIRED_SECTIONS = ("## goal", "## current state", "## plan", "## risks") _MIN_PLAN_SECTIONS = 2 @@ -3626,10 +3636,11 @@ class ChatSession: prior_plan_msgs = [msg, self.messages[j]] break - # Plan agent gets its own identity only — no main session system - # prompt or conversation history. It's an autonomous sub-agent. + # Plan agent gets template guardrails + its own identity — no tool + # patterns, MCP resources, or general conversation history (only + # prior plan tool_call/result pairs are forwarded for refinement). agent_messages: list[dict[str, Any]] = [ - {"role": "system", "content": self._PLAN_IDENTITY}, + {"role": "system", "content": self._plan_system_content()}, ] agent_messages.extend(prior_plan_msgs) agent_messages.append({"role": "user", "content": prompt}) @@ -3700,7 +3711,7 @@ class ChatSession: """Re-run the plan agent incorporating user feedback.""" tc_id = f"plan_refine_{uuid.uuid4().hex[:8]}" agent_messages: list[dict[str, Any]] = [ - {"role": "system", "content": self._PLAN_IDENTITY}, + {"role": "system", "content": self._plan_system_content()}, { "role": "assistant", "content": None,