diff --git a/cron/scheduler.py b/cron/scheduler.py index d41c7ed860c25..685b8ae560eaa 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1033,10 +1033,12 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: enabled_toolsets=_resolve_cron_enabled_toolsets(job, _cfg), disabled_toolsets=["cronjob", "messaging", "clarify"], quiet_mode=True, - # When a workdir is configured, inject AGENTS.md / CLAUDE.md / - # .cursorrules from that directory; otherwise preserve the old - # behaviour (don't inject SOUL.md/AGENTS.md from the scheduler cwd). + # Cron jobs should always inherit the user's SOUL.md identity from + # HERMES_HOME. When a workdir is configured, also inject project + # context files (AGENTS.md / CLAUDE.md / .cursorrules) from there. + # Without a workdir, keep cwd context discovery disabled. skip_context_files=not bool(_job_workdir), + load_soul_identity=True, skip_memory=True, # Cron system prompts would corrupt user representations platform="cron", session_id=_cron_session_id, diff --git a/run_agent.py b/run_agent.py index 895e68644cb67..dc6c0e13d22ea 100644 --- a/run_agent.py +++ b/run_agent.py @@ -926,6 +926,7 @@ def __init__( thread_id: str = None, gateway_session_key: str = None, skip_context_files: bool = False, + load_soul_identity: bool = False, skip_memory: bool = False, session_db=None, parent_session_id: str = None, @@ -977,6 +978,9 @@ def __init__( skip_context_files (bool): If True, skip auto-injection of SOUL.md, AGENTS.md, and .cursorrules into the system prompt. Use this for batch processing and data generation to avoid polluting trajectories with user-specific persona or project instructions. + load_soul_identity (bool): If True, still use ~/.hermes/SOUL.md as the primary + identity even when skip_context_files=True. Project context files from the cwd + remain skipped. """ _install_safe_stdio() @@ -1005,6 +1009,7 @@ def __init__( self._print_fn = None self.background_review_callback = None # Optional sync callback for gateway delivery self.skip_context_files = skip_context_files + self.load_soul_identity = load_soul_identity self.pass_session_id = pass_session_id self._credential_pool = credential_pool self.log_prefix_chars = log_prefix_chars @@ -4742,9 +4747,11 @@ def _build_system_prompt(self, system_message: str = None) -> str: # 6. Current date & time (frozen at build time) # 7. Platform-specific formatting hint - # Try SOUL.md as primary identity (unless context files are skipped) + # Try SOUL.md as primary identity unless the caller explicitly skipped it. + # Some execution modes (cron) still want HERMES_HOME persona while keeping + # cwd project instructions disabled. _soul_loaded = False - if not self.skip_context_files: + if self.load_soul_identity or not self.skip_context_files: _soul_content = load_soul_md() if _soul_content: prompt_parts = [_soul_content] diff --git a/scripts/release.py b/scripts/release.py index 57401d9a8ea76..3c3f7d565932c 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -67,6 +67,7 @@ "beliefanx@gmail.com": "BeliefanX", "jefferson@heimdallstrategy.com": "Mind-Dragon", "steve.westerhouse@origami-analytics.com": "westers", + "liuhao03@bilibili.com": "liuhao1024", "130918800+devorun@users.noreply.github.com": "devorun", "surat.s@itm.kmutnb.ac.th": "beesrsj2500", "beesr@bee.localdomain": "beesrsj2500", diff --git a/tests/cron/test_cron_workdir.py b/tests/cron/test_cron_workdir.py index 03777dd4709b0..5f317c4f4c220 100644 --- a/tests/cron/test_cron_workdir.py +++ b/tests/cron/test_cron_workdir.py @@ -265,6 +265,7 @@ def _install_stubs(monkeypatch, observed: dict): class FakeAgent: def __init__(self, **kwargs): observed["skip_context_files"] = kwargs.get("skip_context_files") + observed["load_soul_identity"] = kwargs.get("load_soul_identity") observed["terminal_cwd_during_init"] = os.environ.get( "TERMINAL_CWD", "_UNSET_" ) @@ -335,6 +336,7 @@ def test_workdir_sets_and_restores_terminal_cwd( # AIAgent was built with skip_context_files=False (feature ON). assert observed["skip_context_files"] is False + assert observed["load_soul_identity"] is True # TERMINAL_CWD was pointing at the job workdir while the agent ran. assert observed["terminal_cwd_during_init"] == str(tmp_path.resolve()) assert observed["terminal_cwd_during_run"] == str(tmp_path.resolve()) @@ -373,6 +375,8 @@ def test_no_workdir_leaves_terminal_cwd_untouched(self, monkeypatch): # Feature is OFF — skip_context_files stays True. assert observed["skip_context_files"] is True + # Cron still forces SOUL.md identity even when cwd context files stay off. + assert observed["load_soul_identity"] is True # TERMINAL_CWD saw the same value during init as it had before. assert observed["terminal_cwd_during_init"] == before # And after run_job completes, it's still the sentinel (nothing diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 2c13a1569f07b..5585eea484099 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -862,6 +862,26 @@ def test_always_has_identity(self, agent): prompt = agent._build_system_prompt() assert DEFAULT_AGENT_IDENTITY in prompt + def test_can_use_soul_identity_even_when_context_files_are_skipped(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("terminal")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + patch("run_agent.load_soul_md", return_value="SOUL IDENTITY"), + ): + agent = AIAgent( + api_key="test-k...7890", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + load_soul_identity=True, + skip_memory=True, + ) + prompt = agent._build_system_prompt() + + assert "SOUL IDENTITY" in prompt + assert DEFAULT_AGENT_IDENTITY not in prompt + def test_includes_system_message(self, agent): prompt = agent._build_system_prompt(system_message="Custom instruction") assert "Custom instruction" in prompt