Files
OpenJarvis/tests/agents/test_persona_persistent.py
T
krypticmouseandClaude Opus 4.7 4ece9a933e fix(agents): persistent agents honor SOUL.md / MEMORY.md / USER.md persona files (#376)
SystemPromptBuilder (which loads the SOUL/MEMORY/USER persona files) was wired
only into the one-shot `jarvis ask` path, so persistent agents run through
AgentExecutor ignored persona entirely. The naive "pass prompt_builder" fix is
insufficient: `prompt_builder` was dropped at every __init__ hop
(ToolUsingAgent never accepted/forwarded it), and monitor_operative/operative
assemble their own system prompt and never consult `_prompt_builder` — so they
would silently ignore it even if it arrived.

Fix:
- `SystemPromptBuilder.persona_sections()` returns just the SOUL/MEMORY/USER
  sections (no agent template), for agents that build their own prompt and want
  to *append* persona rather than have it replace their instructions.
- `BaseAgent._apply_persona()` appends persona to a self-assembled prompt
  (no-op without a builder or persona files).
- Thread `prompt_builder` through the __init__ chain: ToolUsingAgent now
  accepts and forwards it to BaseAgent; monitor_operative and operative forward
  it and call `_apply_persona()` on their assembled system prompt.
- AgentExecutor constructs a SystemPromptBuilder from config and passes it to
  any agent whose __init__ accepts it (same gating as session_store /
  memory_backend). Agents that override __init__ without forwarding (e.g.
  orchestrator) opt out automatically and keep their own machinery.

Specialized prompts are preserved — persona is appended, not substituted. The
one-shot path is unchanged.

Verified: persona_sections() excludes the template but build() still includes
both; monitor_operative/operative receive the builder through the chain and
their assembled prompt includes the SOUL content. New tests in
tests/agents/test_persona_persistent.py (11 passed; ruff clean).

Closes #376

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:21:43 +00:00

97 lines
3.8 KiB
Python

"""Persona files reach persistent agents, not just one-shot `jarvis ask` (#376).
SOUL.md / MEMORY.md / USER.md are loaded by `jarvis ask` via SystemPromptBuilder.
Persistent agents (monitor_operative, operative) assemble their own system
prompt and previously ignored these files entirely. These tests verify the
persona is now appended to their prompt without replacing their specialized
instructions.
"""
from __future__ import annotations
from openjarvis.core.config import MemoryFilesConfig, SystemPromptConfig
from openjarvis.prompt.builder import SystemPromptBuilder
def _builder_with_soul(tmp_path, text="You are Kira."):
soul = tmp_path / "SOUL.md"
soul.write_text(text, encoding="utf-8")
mf = MemoryFilesConfig(
soul_path=str(soul),
memory_path=str(tmp_path / "MEMORY.md"), # absent → ignored
user_path=str(tmp_path / "USER.md"), # absent → ignored
)
return SystemPromptBuilder(
agent_template="AGENT_TEMPLATE",
memory_files_config=mf,
system_prompt_config=SystemPromptConfig(),
)
class TestPersonaSections:
def test_persona_sections_excludes_template(self, tmp_path):
builder = _builder_with_soul(tmp_path)
persona = builder.persona_sections()
assert "You are Kira." in persona
assert "AGENT_TEMPLATE" not in persona # template is NOT in persona
def test_full_build_still_includes_template_and_persona(self, tmp_path):
builder = _builder_with_soul(tmp_path)
full = builder.build()
assert "AGENT_TEMPLATE" in full
assert "You are Kira." in full
def test_persona_sections_empty_when_no_files(self, tmp_path):
mf = MemoryFilesConfig(
soul_path=str(tmp_path / "nope.md"),
memory_path=str(tmp_path / "nope2.md"),
user_path=str(tmp_path / "nope3.md"),
)
builder = SystemPromptBuilder(agent_template="T", memory_files_config=mf)
assert builder.persona_sections() == ""
class TestApplyPersona:
def test_appends_to_base_prompt(self, tmp_path):
from openjarvis.agents.simple import SimpleAgent
agent = SimpleAgent(object(), "m", prompt_builder=_builder_with_soul(tmp_path))
out = agent._apply_persona("MONITOR INSTRUCTIONS")
assert out.startswith("MONITOR INSTRUCTIONS")
assert "You are Kira." in out
def test_noop_without_builder(self):
from openjarvis.agents.simple import SimpleAgent
agent = SimpleAgent(object(), "m")
assert agent._apply_persona("BASE") == "BASE"
class TestPersistentAgentsReceivePromptBuilder:
"""The __init__ chain must forward prompt_builder through to BaseAgent."""
def test_monitor_operative_forwards_prompt_builder(self, tmp_path):
from openjarvis.agents.monitor_operative import MonitorOperativeAgent
builder = _builder_with_soul(tmp_path)
agent = MonitorOperativeAgent(object(), "m", prompt_builder=builder)
assert agent._prompt_builder is builder
applied = agent._apply_persona("MONITOR INSTRUCTIONS")
assert "MONITOR INSTRUCTIONS" in applied
assert "You are Kira." in applied
def test_operative_forwards_prompt_builder(self, tmp_path):
from openjarvis.agents.operative import OperativeAgent
builder = _builder_with_soul(tmp_path)
agent = OperativeAgent(object(), "m", prompt_builder=builder)
assert agent._prompt_builder is builder
assert "You are Kira." in agent._apply_persona("OP INSTRUCTIONS")
def test_monitor_operative_without_builder_is_unaffected(self):
from openjarvis.agents.monitor_operative import MonitorOperativeAgent
agent = MonitorOperativeAgent(object(), "m")
assert agent._prompt_builder is None
assert agent._apply_persona("BASE") == "BASE"