agent-runtime-core 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,9 @@ from agent_runtime_core.persistence.base import (
41
41
  # Optional stores
42
42
  KnowledgeStore,
43
43
  AuditStore,
44
+ # Shared memory
45
+ SharedMemoryStore,
46
+ MemoryItem,
44
47
  # Enums
45
48
  Scope,
46
49
  TaskState,
@@ -71,6 +74,7 @@ from agent_runtime_core.persistence.file import (
71
74
  FileTaskStore,
72
75
  FilePreferencesStore,
73
76
  FileKnowledgeStore,
77
+ InMemorySharedMemoryStore,
74
78
  )
75
79
 
76
80
  from agent_runtime_core.persistence.manager import (
@@ -89,6 +93,9 @@ __all__ = [
89
93
  # Abstract interfaces - optional
90
94
  "KnowledgeStore",
91
95
  "AuditStore",
96
+ # Shared memory
97
+ "SharedMemoryStore",
98
+ "MemoryItem",
92
99
  # Enums
93
100
  "Scope",
94
101
  "TaskState",
@@ -117,6 +124,7 @@ __all__ = [
117
124
  "FileTaskStore",
118
125
  "FilePreferencesStore",
119
126
  "FileKnowledgeStore",
127
+ "InMemorySharedMemoryStore",
120
128
  # Manager
121
129
  "PersistenceManager",
122
130
  "PersistenceConfig",
@@ -29,7 +29,7 @@ from abc import ABC, abstractmethod
29
29
  from dataclasses import dataclass, field
30
30
  from datetime import datetime
31
31
  from enum import Enum
32
- from typing import Any, Optional, AsyncIterator
32
+ from typing import Any, Optional, AsyncIterator, List, Dict
33
33
  from uuid import UUID
34
34
 
35
35
 
@@ -735,3 +735,320 @@ class AuditStore(ABC):
735
735
  async def close(self) -> None:
736
736
  """Close any connections. Override if needed."""
737
737
  pass
738
+
739
+
740
+ # =============================================================================
741
+ # Shared Memory Models and Store
742
+ # =============================================================================
743
+
744
+
745
+ @dataclass
746
+ class MemoryItem:
747
+ """
748
+ A single memory item with semantic key.
749
+
750
+ Memory items use dot-notation keys for hierarchical organization:
751
+ - user.name → "Chris"
752
+ - user.preferences.theme → "dark"
753
+ - user.preferences.language → "en"
754
+ - project.name → "Agent Libraries"
755
+ - conversation.summary → "Discussed privacy features..."
756
+
757
+ Attributes:
758
+ id: Unique identifier for this memory
759
+ key: Semantic key using dot-notation (e.g., "user.preferences.theme")
760
+ value: The actual value (any JSON-serializable type)
761
+ scope: Memory scope (CONVERSATION, USER, SYSTEM)
762
+ created_at: When this memory was created
763
+ updated_at: When this memory was last updated
764
+ source: What created this memory (e.g., "agent:triage", "user:explicit")
765
+ confidence: How confident the agent is (0.0-1.0)
766
+ metadata: Additional context about this memory
767
+ expires_at: Optional expiration time
768
+ conversation_id: For CONVERSATION scope, the conversation this belongs to
769
+ system_id: For SYSTEM scope, the system this belongs to
770
+ """
771
+
772
+ id: UUID
773
+ key: str
774
+ value: Any
775
+ scope: str = "conversation" # MemoryScope value as string
776
+ created_at: datetime = field(default_factory=datetime.utcnow)
777
+ updated_at: datetime = field(default_factory=datetime.utcnow)
778
+ source: str = "agent"
779
+ confidence: float = 1.0
780
+ metadata: dict = field(default_factory=dict)
781
+ expires_at: Optional[datetime] = None
782
+ conversation_id: Optional[UUID] = None
783
+ system_id: Optional[str] = None
784
+
785
+ def to_dict(self) -> dict:
786
+ """Convert to dictionary for serialization."""
787
+ return {
788
+ "id": str(self.id),
789
+ "key": self.key,
790
+ "value": self.value,
791
+ "scope": self.scope,
792
+ "created_at": self.created_at.isoformat() if self.created_at else None,
793
+ "updated_at": self.updated_at.isoformat() if self.updated_at else None,
794
+ "source": self.source,
795
+ "confidence": self.confidence,
796
+ "metadata": self.metadata,
797
+ "expires_at": self.expires_at.isoformat() if self.expires_at else None,
798
+ "conversation_id": str(self.conversation_id) if self.conversation_id else None,
799
+ "system_id": self.system_id,
800
+ }
801
+
802
+ @classmethod
803
+ def from_dict(cls, data: dict) -> "MemoryItem":
804
+ """Create from dictionary."""
805
+ return cls(
806
+ id=UUID(data["id"]) if isinstance(data.get("id"), str) else data.get("id"),
807
+ key=data["key"],
808
+ value=data["value"],
809
+ scope=data.get("scope", "conversation"),
810
+ created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.utcnow(),
811
+ updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.utcnow(),
812
+ source=data.get("source", "agent"),
813
+ confidence=data.get("confidence", 1.0),
814
+ metadata=data.get("metadata", {}),
815
+ expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
816
+ conversation_id=UUID(data["conversation_id"]) if data.get("conversation_id") else None,
817
+ system_id=data.get("system_id"),
818
+ )
819
+
820
+
821
+ class SharedMemoryStore(ABC):
822
+ """
823
+ Abstract interface for shared memory storage.
824
+
825
+ Shared memory stores handle user-scoped memories that can be shared
826
+ across agents in a system. This is different from the basic MemoryStore
827
+ which is just key-value - SharedMemoryStore has:
828
+
829
+ - Semantic keys with dot-notation hierarchy
830
+ - Scope awareness (conversation, user, system)
831
+ - Confidence scores
832
+ - Source tracking
833
+ - Expiration support
834
+ - Batch operations
835
+ - Filtering and listing
836
+
837
+ Privacy enforcement should happen at the framework level (e.g., Django)
838
+ before calling these methods.
839
+
840
+ Example:
841
+ store = DjangoSharedMemoryStore(user=request.user)
842
+
843
+ # Set a memory
844
+ await store.set("user.preferences.theme", "dark", scope=MemoryScope.USER)
845
+
846
+ # Get a memory
847
+ theme = await store.get("user.preferences.theme")
848
+
849
+ # List all user preferences
850
+ prefs = await store.list(prefix="user.preferences", scope=MemoryScope.USER)
851
+
852
+ # Delete a memory
853
+ await store.delete("user.preferences.theme")
854
+ """
855
+
856
+ @abstractmethod
857
+ async def get(
858
+ self,
859
+ key: str,
860
+ scope: Optional[str] = None,
861
+ conversation_id: Optional[UUID] = None,
862
+ system_id: Optional[str] = None,
863
+ ) -> Optional[MemoryItem]:
864
+ """
865
+ Get a memory item by key.
866
+
867
+ Args:
868
+ key: The semantic key (e.g., "user.preferences.theme")
869
+ scope: Optional scope filter (conversation, user, system)
870
+ conversation_id: For conversation-scoped memories
871
+ system_id: For system-scoped memories
872
+
873
+ Returns:
874
+ The MemoryItem if found, None otherwise
875
+ """
876
+ ...
877
+
878
+ @abstractmethod
879
+ async def set(
880
+ self,
881
+ key: str,
882
+ value: Any,
883
+ scope: str = "user",
884
+ source: str = "agent",
885
+ confidence: float = 1.0,
886
+ metadata: Optional[dict] = None,
887
+ expires_at: Optional[datetime] = None,
888
+ conversation_id: Optional[UUID] = None,
889
+ system_id: Optional[str] = None,
890
+ ) -> MemoryItem:
891
+ """
892
+ Set a memory item. Creates or updates.
893
+
894
+ Args:
895
+ key: The semantic key (e.g., "user.preferences.theme")
896
+ value: The value to store (any JSON-serializable type)
897
+ scope: Memory scope (conversation, user, system)
898
+ source: What created this (e.g., "agent:triage")
899
+ confidence: Confidence score (0.0-1.0)
900
+ metadata: Additional context
901
+ expires_at: Optional expiration time
902
+ conversation_id: For conversation-scoped memories
903
+ system_id: For system-scoped memories
904
+
905
+ Returns:
906
+ The created/updated MemoryItem
907
+ """
908
+ ...
909
+
910
+ @abstractmethod
911
+ async def delete(
912
+ self,
913
+ key: str,
914
+ scope: Optional[str] = None,
915
+ conversation_id: Optional[UUID] = None,
916
+ system_id: Optional[str] = None,
917
+ ) -> bool:
918
+ """
919
+ Delete a memory item.
920
+
921
+ Args:
922
+ key: The semantic key
923
+ scope: Optional scope filter
924
+ conversation_id: For conversation-scoped memories
925
+ system_id: For system-scoped memories
926
+
927
+ Returns:
928
+ True if the memory existed and was deleted
929
+ """
930
+ ...
931
+
932
+ @abstractmethod
933
+ async def list(
934
+ self,
935
+ prefix: Optional[str] = None,
936
+ scope: Optional[str] = None,
937
+ conversation_id: Optional[UUID] = None,
938
+ system_id: Optional[str] = None,
939
+ source: Optional[str] = None,
940
+ min_confidence: Optional[float] = None,
941
+ limit: int = 100,
942
+ ) -> List[MemoryItem]:
943
+ """
944
+ List memory items with optional filters.
945
+
946
+ Args:
947
+ prefix: Filter by key prefix (e.g., "user.preferences")
948
+ scope: Filter by scope
949
+ conversation_id: For conversation-scoped memories
950
+ system_id: For system-scoped memories
951
+ source: Filter by source
952
+ min_confidence: Minimum confidence score
953
+ limit: Maximum number of items to return
954
+
955
+ Returns:
956
+ List of matching MemoryItems
957
+ """
958
+ ...
959
+
960
+ @abstractmethod
961
+ async def get_many(
962
+ self,
963
+ keys: List[str],
964
+ scope: Optional[str] = None,
965
+ conversation_id: Optional[UUID] = None,
966
+ system_id: Optional[str] = None,
967
+ ) -> Dict[str, MemoryItem]:
968
+ """
969
+ Get multiple memory items by keys.
970
+
971
+ Args:
972
+ keys: List of semantic keys
973
+ scope: Optional scope filter
974
+ conversation_id: For conversation-scoped memories
975
+ system_id: For system-scoped memories
976
+
977
+ Returns:
978
+ Dictionary of key -> MemoryItem for found items
979
+ """
980
+ ...
981
+
982
+ @abstractmethod
983
+ async def set_many(
984
+ self,
985
+ items: List[tuple],
986
+ scope: str = "user",
987
+ source: str = "agent",
988
+ conversation_id: Optional[UUID] = None,
989
+ system_id: Optional[str] = None,
990
+ ) -> List[MemoryItem]:
991
+ """
992
+ Set multiple memory items atomically.
993
+
994
+ Args:
995
+ items: List of (key, value) tuples
996
+ scope: Memory scope for all items
997
+ source: Source for all items
998
+ conversation_id: For conversation-scoped memories
999
+ system_id: For system-scoped memories
1000
+
1001
+ Returns:
1002
+ List of created/updated MemoryItems
1003
+ """
1004
+ ...
1005
+
1006
+ @abstractmethod
1007
+ async def clear(
1008
+ self,
1009
+ scope: Optional[str] = None,
1010
+ conversation_id: Optional[UUID] = None,
1011
+ system_id: Optional[str] = None,
1012
+ prefix: Optional[str] = None,
1013
+ ) -> int:
1014
+ """
1015
+ Clear memory items.
1016
+
1017
+ Args:
1018
+ scope: Optional scope filter
1019
+ conversation_id: For conversation-scoped memories
1020
+ system_id: For system-scoped memories
1021
+ prefix: Optional key prefix filter
1022
+
1023
+ Returns:
1024
+ Number of items deleted
1025
+ """
1026
+ ...
1027
+
1028
+ async def get_value(
1029
+ self,
1030
+ key: str,
1031
+ default: Any = None,
1032
+ scope: Optional[str] = None,
1033
+ conversation_id: Optional[UUID] = None,
1034
+ system_id: Optional[str] = None,
1035
+ ) -> Any:
1036
+ """
1037
+ Convenience method to get just the value.
1038
+
1039
+ Args:
1040
+ key: The semantic key
1041
+ default: Default value if not found
1042
+ scope: Optional scope filter
1043
+ conversation_id: For conversation-scoped memories
1044
+ system_id: For system-scoped memories
1045
+
1046
+ Returns:
1047
+ The value if found, default otherwise
1048
+ """
1049
+ item = await self.get(key, scope, conversation_id, system_id)
1050
+ return item.value if item else default
1051
+
1052
+ async def close(self) -> None:
1053
+ """Close any connections. Override if needed."""
1054
+ pass
@@ -12,7 +12,7 @@ import json
12
12
  import os
13
13
  from datetime import datetime
14
14
  from pathlib import Path
15
- from typing import Any, Optional
15
+ from typing import Any, Optional, List, Dict
16
16
  from uuid import UUID
17
17
 
18
18
  from agent_runtime_core.persistence.base import (
@@ -781,4 +781,228 @@ try:
781
781
  from agent_runtime_core.vectorstore.embeddings import EmbeddingClient
782
782
  except ImportError:
783
783
  VectorStore = None # type: ignore
784
- EmbeddingClient = None # type: ignore
784
+ EmbeddingClient = None # type: ignore
785
+
786
+
787
+ # =============================================================================
788
+ # In-Memory Shared Memory Store (for testing)
789
+ # =============================================================================
790
+
791
+
792
+ from agent_runtime_core.persistence.base import SharedMemoryStore, MemoryItem
793
+
794
+
795
+ class InMemorySharedMemoryStore(SharedMemoryStore):
796
+ """
797
+ In-memory implementation of SharedMemoryStore for testing.
798
+
799
+ This store keeps all data in memory and is not persistent.
800
+ Useful for unit tests and development.
801
+
802
+ Example:
803
+ store = InMemorySharedMemoryStore()
804
+
805
+ # Set a memory
806
+ await store.set("user.name", "Chris", scope="user")
807
+
808
+ # Get a memory
809
+ item = await store.get("user.name")
810
+ print(item.value) # "Chris"
811
+ """
812
+
813
+ def __init__(self):
814
+ # Storage: dict[composite_key, MemoryItem]
815
+ # composite_key = f"{scope}:{conversation_id or ''}:{system_id or ''}:{key}"
816
+ self._storage: dict[str, MemoryItem] = {}
817
+
818
+ def _make_composite_key(
819
+ self,
820
+ key: str,
821
+ scope: Optional[str] = None,
822
+ conversation_id: Optional[UUID] = None,
823
+ system_id: Optional[str] = None,
824
+ ) -> str:
825
+ """Create a composite key for storage lookup."""
826
+ scope_part = scope or "user"
827
+ conv_part = str(conversation_id) if conversation_id else ""
828
+ sys_part = system_id or ""
829
+ return f"{scope_part}:{conv_part}:{sys_part}:{key}"
830
+
831
+ def _matches_filter(
832
+ self,
833
+ item: MemoryItem,
834
+ prefix: Optional[str] = None,
835
+ scope: Optional[str] = None,
836
+ conversation_id: Optional[UUID] = None,
837
+ system_id: Optional[str] = None,
838
+ source: Optional[str] = None,
839
+ min_confidence: Optional[float] = None,
840
+ ) -> bool:
841
+ """Check if an item matches the given filters."""
842
+ if prefix and not item.key.startswith(prefix):
843
+ return False
844
+ if scope and item.scope != scope:
845
+ return False
846
+ if conversation_id and item.conversation_id != conversation_id:
847
+ return False
848
+ if system_id and item.system_id != system_id:
849
+ return False
850
+ if source and item.source != source:
851
+ return False
852
+ if min_confidence is not None and item.confidence < min_confidence:
853
+ return False
854
+ return True
855
+
856
+ async def get(
857
+ self,
858
+ key: str,
859
+ scope: Optional[str] = None,
860
+ conversation_id: Optional[UUID] = None,
861
+ system_id: Optional[str] = None,
862
+ ) -> Optional[MemoryItem]:
863
+ """Get a memory item by key."""
864
+ composite_key = self._make_composite_key(key, scope, conversation_id, system_id)
865
+ return self._storage.get(composite_key)
866
+
867
+ async def set(
868
+ self,
869
+ key: str,
870
+ value: Any,
871
+ scope: str = "user",
872
+ source: str = "agent",
873
+ confidence: float = 1.0,
874
+ metadata: Optional[dict] = None,
875
+ expires_at: Optional[datetime] = None,
876
+ conversation_id: Optional[UUID] = None,
877
+ system_id: Optional[str] = None,
878
+ ) -> MemoryItem:
879
+ """Set a memory item. Creates or updates."""
880
+ from uuid import uuid4
881
+
882
+ composite_key = self._make_composite_key(key, scope, conversation_id, system_id)
883
+ existing = self._storage.get(composite_key)
884
+
885
+ now = datetime.utcnow()
886
+ if existing:
887
+ # Update existing
888
+ item = MemoryItem(
889
+ id=existing.id,
890
+ key=key,
891
+ value=value,
892
+ scope=scope,
893
+ created_at=existing.created_at,
894
+ updated_at=now,
895
+ source=source,
896
+ confidence=confidence,
897
+ metadata=metadata or {},
898
+ expires_at=expires_at,
899
+ conversation_id=conversation_id,
900
+ system_id=system_id,
901
+ )
902
+ else:
903
+ # Create new
904
+ item = MemoryItem(
905
+ id=uuid4(),
906
+ key=key,
907
+ value=value,
908
+ scope=scope,
909
+ created_at=now,
910
+ updated_at=now,
911
+ source=source,
912
+ confidence=confidence,
913
+ metadata=metadata or {},
914
+ expires_at=expires_at,
915
+ conversation_id=conversation_id,
916
+ system_id=system_id,
917
+ )
918
+
919
+ self._storage[composite_key] = item
920
+ return item
921
+
922
+ async def delete(
923
+ self,
924
+ key: str,
925
+ scope: Optional[str] = None,
926
+ conversation_id: Optional[UUID] = None,
927
+ system_id: Optional[str] = None,
928
+ ) -> bool:
929
+ """Delete a memory item."""
930
+ composite_key = self._make_composite_key(key, scope, conversation_id, system_id)
931
+ if composite_key in self._storage:
932
+ del self._storage[composite_key]
933
+ return True
934
+ return False
935
+
936
+ async def list(
937
+ self,
938
+ prefix: Optional[str] = None,
939
+ scope: Optional[str] = None,
940
+ conversation_id: Optional[UUID] = None,
941
+ system_id: Optional[str] = None,
942
+ source: Optional[str] = None,
943
+ min_confidence: Optional[float] = None,
944
+ limit: int = 100,
945
+ ) -> List[MemoryItem]:
946
+ """List memory items with optional filters."""
947
+ results = []
948
+ for item in self._storage.values():
949
+ if self._matches_filter(
950
+ item, prefix, scope, conversation_id, system_id, source, min_confidence
951
+ ):
952
+ results.append(item)
953
+
954
+ # Sort by updated_at descending
955
+ results.sort(key=lambda x: x.updated_at, reverse=True)
956
+ return results[:limit]
957
+
958
+ async def get_many(
959
+ self,
960
+ keys: List[str],
961
+ scope: Optional[str] = None,
962
+ conversation_id: Optional[UUID] = None,
963
+ system_id: Optional[str] = None,
964
+ ) -> Dict[str, MemoryItem]:
965
+ """Get multiple memory items by keys."""
966
+ results = {}
967
+ for key in keys:
968
+ item = await self.get(key, scope, conversation_id, system_id)
969
+ if item:
970
+ results[key] = item
971
+ return results
972
+
973
+ async def set_many(
974
+ self,
975
+ items: List[tuple],
976
+ scope: str = "user",
977
+ source: str = "agent",
978
+ conversation_id: Optional[UUID] = None,
979
+ system_id: Optional[str] = None,
980
+ ) -> List[MemoryItem]:
981
+ """Set multiple memory items atomically."""
982
+ results = []
983
+ for key, value in items:
984
+ item = await self.set(
985
+ key, value, scope, source,
986
+ conversation_id=conversation_id,
987
+ system_id=system_id,
988
+ )
989
+ results.append(item)
990
+ return results
991
+
992
+ async def clear(
993
+ self,
994
+ scope: Optional[str] = None,
995
+ conversation_id: Optional[UUID] = None,
996
+ system_id: Optional[str] = None,
997
+ prefix: Optional[str] = None,
998
+ ) -> int:
999
+ """Clear memory items."""
1000
+ to_delete = []
1001
+ for composite_key, item in self._storage.items():
1002
+ if self._matches_filter(item, prefix, scope, conversation_id, system_id):
1003
+ to_delete.append(composite_key)
1004
+
1005
+ for key in to_delete:
1006
+ del self._storage[key]
1007
+
1008
+ return len(to_delete)