diff --git a/workspace/claude_sdk_executor.py b/workspace/claude_sdk_executor.py index 8f8ce7e8..f702eef5 100644 --- a/workspace/claude_sdk_executor.py +++ b/workspace/claude_sdk_executor.py @@ -50,6 +50,7 @@ from executor_helpers import ( commit_memory, extract_message_text, get_a2a_instructions, + get_hma_instructions, get_mcp_server_path, get_system_prompt, read_delegation_results, @@ -211,12 +212,12 @@ class ClaudeSDKExecutor(AgentExecutor): return CONFIG_MOUNT def _build_system_prompt(self) -> str | None: - """Compose system prompt from file + A2A delegation instructions.""" + """Compose system prompt from file + A2A + HMA memory instructions.""" base = get_system_prompt(self.config_path, fallback=self.system_prompt) a2a = get_a2a_instructions(mcp=True) - if base and a2a: - return f"{base}\n\n{a2a}" - return base or a2a + hma = get_hma_instructions() + parts = [p for p in (base, a2a, hma) if p] + return "\n\n".join(parts) if parts else None def _prepare_prompt(self, user_input: str) -> str: """Prepend delegation results that arrived while idle.""" diff --git a/workspace/executor_helpers.py b/workspace/executor_helpers.py index 5bc50c90..0d6e2d85 100644 --- a/workspace/executor_helpers.py +++ b/workspace/executor_helpers.py @@ -290,6 +290,31 @@ def get_a2a_instructions(mcp: bool = True) -> str: return _A2A_INSTRUCTIONS_MCP if mcp else _A2A_INSTRUCTIONS_CLI +_HMA_INSTRUCTIONS = """## Hierarchical Memory (HMA) +You have persistent memory tools that survive across sessions and restarts: + +- **commit_memory(content, scope)**: Save important information. + - LOCAL: private to you only (default) + - TEAM: shared with your parent workspace and siblings (same team) + - GLOBAL: shared with the entire org (only root workspaces can write) + +- **recall_memory(query)**: Search your accessible memories. Returns LOCAL + TEAM + GLOBAL matches. + +**When to use memory:** +- After making a decision or learning something non-obvious → commit_memory("decision X because Y", scope="TEAM") +- Before starting work → recall_memory("what did the team decide about X") +- When you discover org-wide knowledge (repo locations, API patterns, conventions) → commit_memory(fact, scope="GLOBAL") if you are a root workspace, or scope="TEAM" to share with your team +- After completing a task → commit_memory("completed task X, PR #N opened", scope="TEAM") so your lead and teammates know + +**Memory is automatically recalled** at the start of each new session. Use it proactively during work to share context. +""" + + +def get_hma_instructions() -> str: + """Return HMA memory instructions for system-prompt injection.""" + return _HMA_INSTRUCTIONS + + # ======================================================================== # Misc text helpers # ======================================================================== diff --git a/workspace/tests/test_claude_sdk_executor.py b/workspace/tests/test_claude_sdk_executor.py index e3781ad9..ac3b0c3d 100644 --- a/workspace/tests/test_claude_sdk_executor.py +++ b/workspace/tests/test_claude_sdk_executor.py @@ -479,7 +479,8 @@ def test_build_system_prompt_combines_base_and_a2a_via_fixture(): """Direct test bypassing the execute() path.""" e = _make_executor() with patch("claude_sdk_executor.get_system_prompt", return_value="BASE"), \ - patch("claude_sdk_executor.get_a2a_instructions", return_value="A2A"): + patch("claude_sdk_executor.get_a2a_instructions", return_value="A2A"), \ + patch("claude_sdk_executor.get_hma_instructions", return_value=""): out = e._build_system_prompt() assert out == "BASE\n\nA2A" @@ -487,17 +488,31 @@ def test_build_system_prompt_combines_base_and_a2a_via_fixture(): def test_build_system_prompt_base_only(): e = _make_executor() with patch("claude_sdk_executor.get_system_prompt", return_value="BASE"), \ - patch("claude_sdk_executor.get_a2a_instructions", return_value=""): + patch("claude_sdk_executor.get_a2a_instructions", return_value=""), \ + patch("claude_sdk_executor.get_hma_instructions", return_value=""): assert e._build_system_prompt() == "BASE" def test_build_system_prompt_a2a_only(): e = _make_executor() with patch("claude_sdk_executor.get_system_prompt", return_value=None), \ - patch("claude_sdk_executor.get_a2a_instructions", return_value="A2A"): + patch("claude_sdk_executor.get_a2a_instructions", return_value="A2A"), \ + patch("claude_sdk_executor.get_hma_instructions", return_value=""): assert e._build_system_prompt() == "A2A" +def test_build_system_prompt_includes_hma(): + """HMA instructions are appended when present.""" + e = _make_executor() + with patch("claude_sdk_executor.get_system_prompt", return_value="BASE"), \ + patch("claude_sdk_executor.get_a2a_instructions", return_value="A2A"), \ + patch("claude_sdk_executor.get_hma_instructions", return_value="## Hierarchical Memory"): + out = e._build_system_prompt() + assert "BASE" in out + assert "A2A" in out + assert "## Hierarchical Memory" in out + + def test_prepare_prompt_no_delegation_returns_unchanged(): e = _make_executor() with patch("claude_sdk_executor.read_delegation_results", return_value=""):