diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py index b36595ec9..1abfb6e5a 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py @@ -30,7 +30,7 @@ class CrewAgentExecutorMixin: memory = getattr(self.agent, "memory", None) or ( getattr(self.crew, "_memory", None) if self.crew else None ) - if memory is None or not self.task: + if memory is None or not self.task or getattr(memory, "_read_only", False): return if ( f"Action: {sanitize_tool_name('Delegate work to coworker')}" diff --git a/lib/crewai/src/crewai/lite_agent.py b/lib/crewai/src/crewai/lite_agent.py index 5b7725a3b..66b710890 100644 --- a/lib/crewai/src/crewai/lite_agent.py +++ b/lib/crewai/src/crewai/lite_agent.py @@ -599,8 +599,8 @@ class LiteAgent(FlowTrackable, BaseModel): ) def _save_to_memory(self, output_text: str) -> None: - """Extract discrete memories from the run and remember each. No-op if _memory is None.""" - if self._memory is None: + """Extract discrete memories from the run and remember each. No-op if _memory is None or read-only.""" + if self._memory is None or getattr(self._memory, "_read_only", False): return input_str = self._get_last_user_content() or "User request" try: diff --git a/lib/crewai/src/crewai/memory/memory_scope.py b/lib/crewai/src/crewai/memory/memory_scope.py index b828e3faf..705ec07de 100644 --- a/lib/crewai/src/crewai/memory/memory_scope.py +++ b/lib/crewai/src/crewai/memory/memory_scope.py @@ -145,7 +145,7 @@ class MemoryScope: class MemorySlice: - """View over multiple scopes: recall searches all, remember requires explicit scope unless read_only.""" + """View over multiple scopes: recall searches all, remember is a no-op when read_only.""" def __init__( self, @@ -160,7 +160,7 @@ class MemorySlice: memory: The underlying Memory instance. scopes: List of scope paths to include. categories: Optional category filter for recall. - read_only: If True, remember() raises PermissionError. + read_only: If True, remember() is a silent no-op. """ self._memory = memory self._scopes = [s.rstrip("/") or "/" for s in scopes] @@ -176,10 +176,10 @@ class MemorySlice: importance: float | None = None, source: str | None = None, private: bool = False, - ) -> MemoryRecord: - """Remember into an explicit scope. Required when read_only=False.""" + ) -> MemoryRecord | None: + """Remember into an explicit scope. No-op when read_only=True.""" if self._read_only: - raise PermissionError("This MemorySlice is read-only") + return None return self._memory.remember( content, scope=scope, diff --git a/lib/crewai/src/crewai/memory/unified_memory.py b/lib/crewai/src/crewai/memory/unified_memory.py index a15f77afd..8ac293f9e 100644 --- a/lib/crewai/src/crewai/memory/unified_memory.py +++ b/lib/crewai/src/crewai/memory/unified_memory.py @@ -88,6 +88,10 @@ class Memory: # Queries shorter than this skip LLM analysis (saving ~1-3s). # Longer queries (full task descriptions) benefit from LLM distillation. query_analysis_threshold: int = 200, + # When True, all write operations (remember, remember_many) are silently + # skipped. Useful for sharing a read-only view of memory across agents + # without any of them persisting new memories. + read_only: bool = False, ) -> None: """Initialize Memory. @@ -107,7 +111,9 @@ class Memory: complex_query_threshold: For complex queries, explore deeper below this confidence. exploration_budget: Number of LLM-driven exploration rounds during deep recall. query_analysis_threshold: Queries shorter than this skip LLM analysis during deep recall. + read_only: If True, remember() and remember_many() are silent no-ops. """ + self._read_only = read_only self._config = MemoryConfig( recency_weight=recency_weight, semantic_weight=semantic_weight, @@ -335,11 +341,13 @@ class Memory: agent_role: Optional agent role for event metadata. Returns: - The created MemoryRecord. + The created MemoryRecord, or None if this memory is read-only. Raises: Exception: On save failure (events emitted). """ + if self._read_only: + return None # type: ignore[return-value] _source_type = "unified_memory" try: crewai_event_bus.emit( @@ -420,7 +428,7 @@ class Memory: Returns: Empty list (records are not available until the background save completes). """ - if not contents: + if not contents or self._read_only: return [] self._submit_save( diff --git a/lib/crewai/src/crewai/tools/memory_tools.py b/lib/crewai/src/crewai/tools/memory_tools.py index c7e04db39..f088fef73 100644 --- a/lib/crewai/src/crewai/tools/memory_tools.py +++ b/lib/crewai/src/crewai/tools/memory_tools.py @@ -104,20 +104,28 @@ class RememberTool(BaseTool): def create_memory_tools(memory: Any) -> list[BaseTool]: """Create Recall and Remember tools for the given memory instance. + When memory is read-only (``_read_only=True``), only the RecallMemoryTool + is returned — the RememberTool is omitted so agents are never offered a + save capability they cannot use. + Args: memory: A Memory, MemoryScope, or MemorySlice instance. Returns: - List containing a RecallMemoryTool and a RememberTool. + List containing a RecallMemoryTool and, if not read-only, a RememberTool. """ i18n = get_i18n() - return [ + tools: list[BaseTool] = [ RecallMemoryTool( memory=memory, description=i18n.tools("recall_memory"), ), - RememberTool( - memory=memory, - description=i18n.tools("save_to_memory"), - ), ] + if not getattr(memory, "_read_only", False): + tools.append( + RememberTool( + memory=memory, + description=i18n.tools("save_to_memory"), + ) + ) + return tools diff --git a/lib/crewai/tests/memory/test_unified_memory.py b/lib/crewai/tests/memory/test_unified_memory.py index 5b25b8077..e7eae4fdb 100644 --- a/lib/crewai/tests/memory/test_unified_memory.py +++ b/lib/crewai/tests/memory/test_unified_memory.py @@ -218,14 +218,15 @@ def test_memory_slice_recall(tmp_path: Path, mock_embedder: MagicMock) -> None: assert isinstance(matches, list) -def test_memory_slice_remember_raises_when_read_only(tmp_path: Path, mock_embedder: MagicMock) -> None: +def test_memory_slice_remember_is_noop_when_read_only(tmp_path: Path, mock_embedder: MagicMock) -> None: from crewai.memory.unified_memory import Memory from crewai.memory.memory_scope import MemorySlice mem = Memory(storage=str(tmp_path / "db7"), llm=MagicMock(), embedder=mock_embedder) sl = MemorySlice(mem, ["/a"], read_only=True) - with pytest.raises(PermissionError): - sl.remember("x", scope="/a") + result = sl.remember("x", scope="/a") + assert result is None + assert mem.list_records() == [] # --- Flow memory ---