Skip to content
Merged
3 changes: 3 additions & 0 deletions lib/crewai/src/crewai/memory/memory_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def _ensure_memory_kind(value: Any) -> Any:
Pass-through for non-dict values (instances, ``bool``, ``None``).
"""
if isinstance(value, dict) and "memory_kind" not in value:
value = dict(value)
if "scopes" in value:
value["memory_kind"] = "slice"
elif "root_path" in value:
Expand Down Expand Up @@ -55,6 +56,7 @@ def _accept_memory(cls, data: Any, handler: Any) -> MemoryScope:
return data
if not isinstance(data, dict):
raise ValueError(f"Expected dict or MemoryScope, got {type(data).__name__}")
data = dict(data)
memory = data.pop("memory", None)
instance: MemoryScope = handler(data)
if memory is not None:
Expand Down Expand Up @@ -245,6 +247,7 @@ def _accept_memory(cls, data: Any, handler: Any) -> MemorySlice:
return data
if not isinstance(data, dict):
raise ValueError(f"Expected dict or MemorySlice, got {type(data).__name__}")
data = dict(data)
memory = data.pop("memory", None)
data["scopes"] = [s.rstrip("/") or "/" for s in data.get("scopes", [])]
instance: MemorySlice = handler(data)
Expand Down
44 changes: 44 additions & 0 deletions lib/crewai/tests/memory/test_unified_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,50 @@ def test_memory_scope_slice(tmp_path: Path, mock_embedder: MagicMock) -> None:
assert "/a" in sl.scopes and "/b" in sl.scopes


def test_memory_scope_config_can_be_reused() -> None:
"""Constructing a scope must not remove the memory from caller-owned config."""
from crewai.memory.memory_scope import MemoryScope

memory = MagicMock()
config = {"memory": memory, "root_path": "/agent/1"}

first = MemoryScope.model_validate(config)
second = MemoryScope.model_validate(config)

assert config == {"memory": memory, "root_path": "/agent/1"}
assert first._require_memory() is memory
assert second._require_memory() is memory


def test_memory_slice_config_can_be_reused_without_normalizing_it_in_place() -> None:
"""Constructing a slice must preserve caller-owned dependencies and paths."""
from crewai.memory.memory_scope import MemorySlice

memory = MagicMock()
config = {"memory": memory, "scopes": ["/team/", "/"]}

first = MemorySlice.model_validate(config)
second = MemorySlice.model_validate(config)

assert config == {"memory": memory, "scopes": ["/team/", "/"]}
assert first.scopes == ["/team", "/"]
assert second.scopes == ["/team", "/"]
assert first._require_memory() is memory
assert second._require_memory() is memory


def test_memory_kind_inference_preserves_input() -> None:
"""Inferring a legacy config's discriminator must not mutate that config."""
from crewai.memory.memory_scope import _ensure_memory_kind

config = {"root_path": "/agent/1"}

normalized = _ensure_memory_kind(config)

assert config == {"root_path": "/agent/1"}
assert normalized == {"root_path": "/agent/1", "memory_kind": "scope"}


def test_memory_list_scopes_info_tree(tmp_path: Path, mock_embedder: MagicMock) -> None:
from crewai.memory.unified_memory import Memory

Expand Down
Loading