strands-agents-session 0.1.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.
@@ -0,0 +1,18 @@
1
+ """Backend-agnostic session management for Strands Agents.
2
+
3
+ Provides a ``SessionStorage`` interface and a ``KeyValueSessionManager`` that
4
+ implements the full Strands ``SessionRepository`` over it, so new storage
5
+ backends (DynamoDB, Redis, MongoDB, …) only need to implement a handful of
6
+ keyed-record methods.
7
+ """
8
+
9
+ from . import keys
10
+ from .manager import KeyValueSessionManager
11
+ from .storage import InMemorySessionStorage, SessionStorage
12
+
13
+ __all__ = [
14
+ "KeyValueSessionManager",
15
+ "SessionStorage",
16
+ "InMemorySessionStorage",
17
+ "keys",
18
+ ]
@@ -0,0 +1,37 @@
1
+ """Key construction helpers shared across session storage backends.
2
+
3
+ Single-logical-table layout (keys are opaque strings, so any KV/range store fits):
4
+
5
+ Session : pk = "SESSION#<session_id>" sk = "META"
6
+ Agent : pk = "SESSION#<session_id>" sk = "AGENT#<agent_id>"
7
+ Message : pk = "SESSION#<session_id>#AGENT#<agent_id>" sk = "MSG#<zero-padded id>"
8
+
9
+ Message sort keys are zero-padded so that lexical ordering matches numeric
10
+ ordering — making ``list_messages(offset, limit)`` a simple ordered range scan,
11
+ matching the ``removed_message_count`` offset semantics Strands relies on.
12
+ """
13
+
14
+ # Width for zero-padding message ids so lexical sort == numeric sort.
15
+ MESSAGE_ID_WIDTH = 20
16
+
17
+ SESSION_SK = "META"
18
+
19
+
20
+ def session_pk(session_id: str) -> str:
21
+ return f"SESSION#{session_id}"
22
+
23
+
24
+ def agent_sk(agent_id: str) -> str:
25
+ return f"AGENT#{agent_id}"
26
+
27
+
28
+ def agent_id_from_sk(sk: str) -> str:
29
+ return sk.split("AGENT#", 1)[1]
30
+
31
+
32
+ def messages_pk(session_id: str, agent_id: str) -> str:
33
+ return f"SESSION#{session_id}#AGENT#{agent_id}"
34
+
35
+
36
+ def message_sk(message_id: int) -> str:
37
+ return f"MSG#{int(message_id):0{MESSAGE_ID_WIDTH}d}"
@@ -0,0 +1,146 @@
1
+ """Backend-agnostic Strands session manager over a ``SessionStorage``.
2
+
3
+ Implements the Strands ``SessionRepository`` (all 8 CRUD methods) in terms of a
4
+ small ``SessionStorage`` interface, and mixes in ``RepositorySessionManager`` so
5
+ the Strands session lifecycle (message indexing, restore, offsetting, tool-use
6
+ repair, change detection) is reused unchanged.
7
+
8
+ A concrete backend only needs to provide a ``SessionStorage`` — see
9
+ ``strands_session_dynamodb`` for an example.
10
+ """
11
+
12
+ import json
13
+ from typing import Any, List, Optional
14
+
15
+ from strands.session.repository_session_manager import RepositorySessionManager
16
+ from strands.session.session_repository import SessionRepository
17
+ from strands.types.exceptions import SessionException
18
+ from strands.types.session import Session, SessionAgent, SessionMessage
19
+
20
+ from .keys import (
21
+ SESSION_SK,
22
+ agent_id_from_sk,
23
+ agent_sk,
24
+ message_sk,
25
+ messages_pk,
26
+ session_pk,
27
+ )
28
+ from .storage import SessionStorage
29
+
30
+
31
+ class KeyValueSessionManager(RepositorySessionManager, SessionRepository):
32
+ """Strands session manager backed by any ``SessionStorage`` implementation."""
33
+
34
+ def __init__(self, session_id: str, storage: SessionStorage, **kwargs: Any) -> None:
35
+ self.storage = storage
36
+ super().__init__(session_id=session_id, session_repository=self)
37
+
38
+ # ------------------------------------------------------------------
39
+ # helpers
40
+ # ------------------------------------------------------------------
41
+
42
+ @staticmethod
43
+ def _record(pk: str, sk: str, payload: dict) -> dict:
44
+ return {"pk": pk, "sk": sk, "data": json.dumps(payload)}
45
+
46
+ # ------------------------------------------------------------------
47
+ # sessions
48
+ # ------------------------------------------------------------------
49
+
50
+ def create_session(self, session: Session, **kwargs: Any) -> Session:
51
+ pk = session_pk(session.session_id)
52
+ if self.storage.get(pk, SESSION_SK) is not None:
53
+ raise SessionException(f"Session {session.session_id} already exists")
54
+ self.storage.put(self._record(pk, SESSION_SK, session.to_dict()))
55
+ return session
56
+
57
+ def read_session(self, session_id: str, **kwargs: Any) -> Optional[Session]:
58
+ rec = self.storage.get(session_pk(session_id), SESSION_SK)
59
+ if rec is None:
60
+ return None
61
+ return Session.from_dict(json.loads(rec["data"]))
62
+
63
+ def delete_session(self, session_id: str, **kwargs: Any) -> None:
64
+ pk = session_pk(session_id)
65
+ items = self.storage.query(pk)
66
+ agent_ids = [agent_id_from_sk(i["sk"]) for i in items if i["sk"].startswith("AGENT#")]
67
+ for agent_id in agent_ids:
68
+ self.storage.delete_partition(messages_pk(session_id, agent_id))
69
+ self.storage.delete_partition(pk)
70
+
71
+ # ------------------------------------------------------------------
72
+ # agents
73
+ # ------------------------------------------------------------------
74
+
75
+ def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
76
+ self.storage.put(
77
+ self._record(session_pk(session_id), agent_sk(session_agent.agent_id), session_agent.to_dict())
78
+ )
79
+
80
+ def read_agent(self, session_id: str, agent_id: str, **kwargs: Any) -> Optional[SessionAgent]:
81
+ rec = self.storage.get(session_pk(session_id), agent_sk(agent_id))
82
+ if rec is None:
83
+ return None
84
+ return SessionAgent.from_dict(json.loads(rec["data"]))
85
+
86
+ def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
87
+ previous = self.read_agent(session_id, session_agent.agent_id)
88
+ if previous is None:
89
+ raise SessionException(
90
+ f"Agent {session_agent.agent_id} in session {session_id} does not exist"
91
+ )
92
+ session_agent.created_at = previous.created_at # preserve creation timestamp
93
+ self.storage.put(
94
+ self._record(session_pk(session_id), agent_sk(session_agent.agent_id), session_agent.to_dict())
95
+ )
96
+
97
+ # ------------------------------------------------------------------
98
+ # messages
99
+ # ------------------------------------------------------------------
100
+
101
+ def create_message(
102
+ self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any
103
+ ) -> None:
104
+ self.storage.put(
105
+ self._record(
106
+ messages_pk(session_id, agent_id),
107
+ message_sk(session_message.message_id),
108
+ session_message.to_dict(),
109
+ )
110
+ )
111
+
112
+ def read_message(
113
+ self, session_id: str, agent_id: str, message_id: int, **kwargs: Any
114
+ ) -> Optional[SessionMessage]:
115
+ rec = self.storage.get(messages_pk(session_id, agent_id), message_sk(message_id))
116
+ if rec is None:
117
+ return None
118
+ return SessionMessage.from_dict(json.loads(rec["data"]))
119
+
120
+ def update_message(
121
+ self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any
122
+ ) -> None:
123
+ previous = self.read_message(session_id, agent_id, session_message.message_id)
124
+ if previous is None:
125
+ raise SessionException(f"Message {session_message.message_id} does not exist")
126
+ session_message.created_at = previous.created_at
127
+ self.storage.put(
128
+ self._record(
129
+ messages_pk(session_id, agent_id),
130
+ message_sk(session_message.message_id),
131
+ session_message.to_dict(),
132
+ )
133
+ )
134
+
135
+ def list_messages(
136
+ self,
137
+ session_id: str,
138
+ agent_id: str,
139
+ limit: Optional[int] = None,
140
+ offset: int = 0,
141
+ **kwargs: Any,
142
+ ) -> List[SessionMessage]:
143
+ records = self.storage.query(
144
+ messages_pk(session_id, agent_id), sk_gte=message_sk(offset), limit=limit
145
+ )
146
+ return [SessionMessage.from_dict(json.loads(r["data"])) for r in records]
@@ -0,0 +1,69 @@
1
+ """Storage interface for session backends.
2
+
3
+ A backend implements this small keyed-record interface; the
4
+ ``KeyValueSessionManager`` builds the full Strands ``SessionRepository`` on top
5
+ of it. Records are plain dicts with at least ``pk``, ``sk``, and ``data`` keys
6
+ (``data`` is a serialized payload string).
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ class SessionStorage(ABC):
14
+ """Minimal ordered keyed-record store backing a session manager."""
15
+
16
+ @abstractmethod
17
+ def put(self, item: Dict[str, Any]) -> None:
18
+ """Insert or overwrite a record. ``item`` has ``pk``, ``sk``, ``data``."""
19
+
20
+ @abstractmethod
21
+ def get(self, pk: str, sk: str) -> Optional[Dict[str, Any]]:
22
+ """Return the record for (pk, sk), or None."""
23
+
24
+ @abstractmethod
25
+ def query(
26
+ self, pk: str, sk_gte: Optional[str] = None, limit: Optional[int] = None
27
+ ) -> List[Dict[str, Any]]:
28
+ """Return records under ``pk`` ordered by ``sk`` ascending.
29
+
30
+ If ``sk_gte`` is given, only records with ``sk >= sk_gte``. If ``limit``
31
+ is given, return at most that many.
32
+ """
33
+
34
+ @abstractmethod
35
+ def delete(self, pk: str, sk: str) -> None:
36
+ """Delete the record for (pk, sk) if present."""
37
+
38
+ @abstractmethod
39
+ def delete_partition(self, pk: str) -> None:
40
+ """Delete every record under ``pk``."""
41
+
42
+
43
+ class InMemorySessionStorage(SessionStorage):
44
+ """In-memory storage — useful for tests and local development."""
45
+
46
+ def __init__(self) -> None:
47
+ self._data: Dict[str, Dict[str, Dict[str, Any]]] = {}
48
+
49
+ def put(self, item: Dict[str, Any]) -> None:
50
+ self._data.setdefault(item["pk"], {})[item["sk"]] = dict(item)
51
+
52
+ def get(self, pk: str, sk: str) -> Optional[Dict[str, Any]]:
53
+ rec = self._data.get(pk, {}).get(sk)
54
+ return dict(rec) if rec is not None else None
55
+
56
+ def query(
57
+ self, pk: str, sk_gte: Optional[str] = None, limit: Optional[int] = None
58
+ ) -> List[Dict[str, Any]]:
59
+ partition = self._data.get(pk, {})
60
+ keys = sorted(k for k in partition if sk_gte is None or k >= sk_gte)
61
+ if limit is not None:
62
+ keys = keys[:limit]
63
+ return [dict(partition[k]) for k in keys]
64
+
65
+ def delete(self, pk: str, sk: str) -> None:
66
+ self._data.get(pk, {}).pop(sk, None)
67
+
68
+ def delete_partition(self, pk: str) -> None:
69
+ self._data.pop(pk, None)
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: strands-agents-session
3
+ Version: 0.1.0
4
+ Summary: Backend-agnostic session management for Strands Agents — a SessionStorage interface + KeyValueSessionManager to build session backends (DynamoDB, Redis, MongoDB, …)
5
+ Project-URL: Homepage, https://github.com/skamalj/strands_agents_session
6
+ Project-URL: Repository, https://github.com/skamalj/strands_agents_session.git
7
+ Author-email: Kamal <skamalj@github.com>
8
+ Keywords: agent-state,memory,persistence,session,session-manager,strands,strands-agents
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: strands-agents
15
+ Description-Content-Type: text/markdown
16
+
17
+ # strands-agents-session
18
+
19
+ Backend-agnostic **session management** for [Strands Agents](https://strandsagents.com). Provides the shared machinery so you can build a session storage backend (DynamoDB, Redis, MongoDB, Postgres, …) by implementing just a handful of methods.
20
+
21
+ This is the base package of a family. Concrete backends are shipped separately, e.g. [`strands-session-dynamodb`](https://pypi.org/project/strands-session-dynamodb/).
22
+
23
+ ## What it gives you
24
+
25
+ - **`KeyValueSessionManager`** — implements the full Strands `SessionRepository` (all 8 CRUD methods) and mixes in `RepositorySessionManager`, so the Strands session lifecycle (message indexing, restore, `removed_message_count` offsetting, tool-use repair, change detection) is reused unchanged.
26
+ - **`SessionStorage`** — a tiny ordered keyed-record interface (`put` / `get` / `query` / `delete` / `delete_partition`). Implement it and you have a working Strands session backend.
27
+ - **`InMemorySessionStorage`** — a ready in-memory backend for tests and local development.
28
+ - **`keys`** — shared key/serialization conventions (zero-padded message sort keys so `list_messages(offset, limit)` is a native ordered range scan).
29
+
30
+ > **Storage only, by design.** Message *pruning* in Strands is a `ConversationManager` concern, deliberately decoupled from storage. This package (and its backends) never prune — doing so at the storage layer would corrupt Strands' message-index/offset restore logic.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install strands-agents-session
36
+ ```
37
+
38
+ **Requires Python 3.10+.**
39
+
40
+ ## Using the in-memory backend
41
+
42
+ ```python
43
+ from strands import Agent
44
+ from strands_agents_session import KeyValueSessionManager, InMemorySessionStorage
45
+
46
+ session_manager = KeyValueSessionManager(
47
+ session_id="user-123",
48
+ storage=InMemorySessionStorage(),
49
+ )
50
+ agent = Agent(session_manager=session_manager)
51
+ ```
52
+
53
+ ## Building a backend
54
+
55
+ Implement `SessionStorage` and hand it to `KeyValueSessionManager`:
56
+
57
+ ```python
58
+ from strands_agents_session import KeyValueSessionManager, SessionStorage
59
+
60
+ class MyStorage(SessionStorage):
61
+ def put(self, item): ... # item = {"pk", "sk", "data"}
62
+ def get(self, pk, sk): ... # -> item | None
63
+ def query(self, pk, sk_gte=None, limit=None): ... # ordered by sk asc
64
+ def delete(self, pk, sk): ...
65
+ def delete_partition(self, pk): ...
66
+
67
+ class MySessionManager(KeyValueSessionManager):
68
+ def __init__(self, session_id, **cfg):
69
+ super().__init__(session_id=session_id, storage=MyStorage(**cfg))
70
+ ```
71
+
72
+ That's it — all the `SessionRepository` methods, restore logic, and pagination come from the base.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,7 @@
1
+ strands_agents_session/__init__.py,sha256=4kuX31bxGrYwTt5XrABBMAzAN5htXk-3U-lx843THl4,548
2
+ strands_agents_session/keys.py,sha256=zDNqXe38uTyoH3YilXCdFOWHJ2v9DXuMKk5erNoxqdQ,1176
3
+ strands_agents_session/manager.py,sha256=EUpsZV8kZdPtd34G40moOp8luF6Od4z4vaFBUhDi2To,5943
4
+ strands_agents_session/storage.py,sha256=fjOXhupXjVF0B1WZy_UEIEjZm0-AQwjE9YunIqA0X7Y,2423
5
+ strands_agents_session-0.1.0.dist-info/METADATA,sha256=EC2K9SnKQJAxU4kJ-GRpv08O8H6YoqGaDtmd3lgvsZ4,3492
6
+ strands_agents_session-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ strands_agents_session-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any