agentrun-sdk 0.1.2__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.

Potentially problematic release.


This version of agentrun-sdk might be problematic. Click here for more details.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. agentrun_wrapper/tools/code_interpreter_client.py +186 -0
@@ -0,0 +1,216 @@
1
+ """File-based session manager for local filesystem storage."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ from typing import Any, Optional, cast
9
+
10
+ from ..types.exceptions import SessionException
11
+ from ..types.session import Session, SessionAgent, SessionMessage
12
+ from .repository_session_manager import RepositorySessionManager
13
+ from .session_repository import SessionRepository
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ SESSION_PREFIX = "session_"
18
+ AGENT_PREFIX = "agent_"
19
+ MESSAGE_PREFIX = "message_"
20
+
21
+
22
+ class FileSessionManager(RepositorySessionManager, SessionRepository):
23
+ """File-based session manager for local filesystem storage.
24
+
25
+ Creates the following filesystem structure for the session storage:
26
+ ```bash
27
+ /<sessions_dir>/
28
+ └── session_<session_id>/
29
+ ├── session.json # Session metadata
30
+ └── agents/
31
+ └── agent_<agent_id>/
32
+ ├── agent.json # Agent metadata
33
+ └── messages/
34
+ ├── message_<id1>.json
35
+ └── message_<id2>.json
36
+ ```
37
+ """
38
+
39
+ def __init__(self, session_id: str, storage_dir: Optional[str] = None, **kwargs: Any):
40
+ """Initialize FileSession with filesystem storage.
41
+
42
+ Args:
43
+ session_id: ID for the session
44
+ storage_dir: Directory for local filesystem storage (defaults to temp dir)
45
+ **kwargs: Additional keyword arguments for future extensibility.
46
+ """
47
+ self.storage_dir = storage_dir or os.path.join(tempfile.gettempdir(), "strands/sessions")
48
+ os.makedirs(self.storage_dir, exist_ok=True)
49
+
50
+ super().__init__(session_id=session_id, session_repository=self)
51
+
52
+ def _get_session_path(self, session_id: str) -> str:
53
+ """Get session directory path."""
54
+ return os.path.join(self.storage_dir, f"{SESSION_PREFIX}{session_id}")
55
+
56
+ def _get_agent_path(self, session_id: str, agent_id: str) -> str:
57
+ """Get agent directory path."""
58
+ session_path = self._get_session_path(session_id)
59
+ return os.path.join(session_path, "agents", f"{AGENT_PREFIX}{agent_id}")
60
+
61
+ def _get_message_path(self, session_id: str, agent_id: str, message_id: int) -> str:
62
+ """Get message file path.
63
+
64
+ Args:
65
+ session_id: ID of the session
66
+ agent_id: ID of the agent
67
+ message_id: Index of the message
68
+ Returns:
69
+ The filename for the message
70
+ """
71
+ agent_path = self._get_agent_path(session_id, agent_id)
72
+ return os.path.join(agent_path, "messages", f"{MESSAGE_PREFIX}{message_id}.json")
73
+
74
+ def _read_file(self, path: str) -> dict[str, Any]:
75
+ """Read JSON file."""
76
+ try:
77
+ with open(path, "r", encoding="utf-8") as f:
78
+ return cast(dict[str, Any], json.load(f))
79
+ except json.JSONDecodeError as e:
80
+ raise SessionException(f"Invalid JSON in file {path}: {str(e)}") from e
81
+
82
+ def _write_file(self, path: str, data: dict[str, Any]) -> None:
83
+ """Write JSON file."""
84
+ os.makedirs(os.path.dirname(path), exist_ok=True)
85
+ with open(path, "w", encoding="utf-8") as f:
86
+ json.dump(data, f, indent=2, ensure_ascii=False)
87
+
88
+ def create_session(self, session: Session, **kwargs: Any) -> Session:
89
+ """Create a new session."""
90
+ session_dir = self._get_session_path(session.session_id)
91
+ if os.path.exists(session_dir):
92
+ raise SessionException(f"Session {session.session_id} already exists")
93
+
94
+ # Create directory structure
95
+ os.makedirs(session_dir, exist_ok=True)
96
+ os.makedirs(os.path.join(session_dir, "agents"), exist_ok=True)
97
+
98
+ # Write session file
99
+ session_file = os.path.join(session_dir, "session.json")
100
+ session_dict = session.to_dict()
101
+ self._write_file(session_file, session_dict)
102
+
103
+ return session
104
+
105
+ def read_session(self, session_id: str, **kwargs: Any) -> Optional[Session]:
106
+ """Read session data."""
107
+ session_file = os.path.join(self._get_session_path(session_id), "session.json")
108
+ if not os.path.exists(session_file):
109
+ return None
110
+
111
+ session_data = self._read_file(session_file)
112
+ return Session.from_dict(session_data)
113
+
114
+ def delete_session(self, session_id: str, **kwargs: Any) -> None:
115
+ """Delete session and all associated data."""
116
+ session_dir = self._get_session_path(session_id)
117
+ if not os.path.exists(session_dir):
118
+ raise SessionException(f"Session {session_id} does not exist")
119
+
120
+ shutil.rmtree(session_dir)
121
+
122
+ def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
123
+ """Create a new agent in the session."""
124
+ agent_id = session_agent.agent_id
125
+
126
+ agent_dir = self._get_agent_path(session_id, agent_id)
127
+ os.makedirs(agent_dir, exist_ok=True)
128
+ os.makedirs(os.path.join(agent_dir, "messages"), exist_ok=True)
129
+
130
+ agent_file = os.path.join(agent_dir, "agent.json")
131
+ session_data = session_agent.to_dict()
132
+ self._write_file(agent_file, session_data)
133
+
134
+ def read_agent(self, session_id: str, agent_id: str, **kwargs: Any) -> Optional[SessionAgent]:
135
+ """Read agent data."""
136
+ agent_file = os.path.join(self._get_agent_path(session_id, agent_id), "agent.json")
137
+ if not os.path.exists(agent_file):
138
+ return None
139
+
140
+ agent_data = self._read_file(agent_file)
141
+ return SessionAgent.from_dict(agent_data)
142
+
143
+ def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
144
+ """Update agent data."""
145
+ agent_id = session_agent.agent_id
146
+ previous_agent = self.read_agent(session_id=session_id, agent_id=agent_id)
147
+ if previous_agent is None:
148
+ raise SessionException(f"Agent {agent_id} in session {session_id} does not exist")
149
+
150
+ session_agent.created_at = previous_agent.created_at
151
+ agent_file = os.path.join(self._get_agent_path(session_id, agent_id), "agent.json")
152
+ self._write_file(agent_file, session_agent.to_dict())
153
+
154
+ def create_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
155
+ """Create a new message for the agent."""
156
+ message_file = self._get_message_path(
157
+ session_id,
158
+ agent_id,
159
+ session_message.message_id,
160
+ )
161
+ session_dict = session_message.to_dict()
162
+ self._write_file(message_file, session_dict)
163
+
164
+ def read_message(self, session_id: str, agent_id: str, message_id: int, **kwargs: Any) -> Optional[SessionMessage]:
165
+ """Read message data."""
166
+ message_path = self._get_message_path(session_id, agent_id, message_id)
167
+ if not os.path.exists(message_path):
168
+ return None
169
+ message_data = self._read_file(message_path)
170
+ return SessionMessage.from_dict(message_data)
171
+
172
+ def update_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
173
+ """Update message data."""
174
+ message_id = session_message.message_id
175
+ previous_message = self.read_message(session_id=session_id, agent_id=agent_id, message_id=message_id)
176
+ if previous_message is None:
177
+ raise SessionException(f"Message {message_id} does not exist")
178
+
179
+ # Preserve the original created_at timestamp
180
+ session_message.created_at = previous_message.created_at
181
+ message_file = self._get_message_path(session_id, agent_id, message_id)
182
+ self._write_file(message_file, session_message.to_dict())
183
+
184
+ def list_messages(
185
+ self, session_id: str, agent_id: str, limit: Optional[int] = None, offset: int = 0, **kwargs: Any
186
+ ) -> list[SessionMessage]:
187
+ """List messages for an agent with pagination."""
188
+ messages_dir = os.path.join(self._get_agent_path(session_id, agent_id), "messages")
189
+ if not os.path.exists(messages_dir):
190
+ raise SessionException(f"Messages directory missing from agent: {agent_id} in session {session_id}")
191
+
192
+ # Read all message files, and record the index
193
+ message_index_files: list[tuple[int, str]] = []
194
+ for filename in os.listdir(messages_dir):
195
+ if filename.startswith(MESSAGE_PREFIX) and filename.endswith(".json"):
196
+ # Extract index from message_<index>.json format
197
+ index = int(filename[len(MESSAGE_PREFIX) : -5]) # Remove prefix and .json suffix
198
+ message_index_files.append((index, filename))
199
+
200
+ # Sort by index and extract just the filenames
201
+ message_files = [f for _, f in sorted(message_index_files)]
202
+
203
+ # Apply pagination to filenames
204
+ if limit is not None:
205
+ message_files = message_files[offset : offset + limit]
206
+ else:
207
+ message_files = message_files[offset:]
208
+
209
+ # Load only the message files
210
+ messages: list[SessionMessage] = []
211
+ for filename in message_files:
212
+ file_path = os.path.join(messages_dir, filename)
213
+ message_data = self._read_file(file_path)
214
+ messages.append(SessionMessage.from_dict(message_data))
215
+
216
+ return messages
@@ -0,0 +1,152 @@
1
+ """Repository session manager implementation."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+ from ..agent.state import AgentState
7
+ from ..types.content import Message
8
+ from ..types.exceptions import SessionException
9
+ from ..types.session import (
10
+ Session,
11
+ SessionAgent,
12
+ SessionMessage,
13
+ SessionType,
14
+ )
15
+ from .session_manager import SessionManager
16
+ from .session_repository import SessionRepository
17
+
18
+ if TYPE_CHECKING:
19
+ from ..agent.agent import Agent
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class RepositorySessionManager(SessionManager):
25
+ """Session manager for persisting agents in a SessionRepository."""
26
+
27
+ def __init__(self, session_id: str, session_repository: SessionRepository, **kwargs: Any):
28
+ """Initialize the RepositorySessionManager.
29
+
30
+ If no session with the specified session_id exists yet, it will be created
31
+ in the session_repository.
32
+
33
+ Args:
34
+ session_id: ID to use for the session. A new session with this id will be created if it does
35
+ not exist in the repository yet
36
+ session_repository: Underlying session repository to use to store the sessions state.
37
+ **kwargs: Additional keyword arguments for future extensibility.
38
+
39
+ """
40
+ self.session_repository = session_repository
41
+ self.session_id = session_id
42
+ session = session_repository.read_session(session_id)
43
+ # Create a session if it does not exist yet
44
+ if session is None:
45
+ logger.debug("session_id=<%s> | session not found, creating new session", self.session_id)
46
+ session = Session(session_id=session_id, session_type=SessionType.AGENT)
47
+ session_repository.create_session(session)
48
+
49
+ self.session = session
50
+
51
+ # Keep track of the latest message of each agent in case we need to redact it.
52
+ self._latest_agent_message: dict[str, Optional[SessionMessage]] = {}
53
+
54
+ def append_message(self, message: Message, agent: "Agent", **kwargs: Any) -> None:
55
+ """Append a message to the agent's session.
56
+
57
+ Args:
58
+ message: Message to add to the agent in the session
59
+ agent: Agent to append the message to
60
+ **kwargs: Additional keyword arguments for future extensibility.
61
+ """
62
+ # Calculate the next index (0 if this is the first message, otherwise increment the previous index)
63
+ latest_agent_message = self._latest_agent_message[agent.agent_id]
64
+ if latest_agent_message:
65
+ next_index = latest_agent_message.message_id + 1
66
+ else:
67
+ next_index = 0
68
+
69
+ session_message = SessionMessage.from_message(message, next_index)
70
+ self._latest_agent_message[agent.agent_id] = session_message
71
+ self.session_repository.create_message(self.session_id, agent.agent_id, session_message)
72
+
73
+ def redact_latest_message(self, redact_message: Message, agent: "Agent", **kwargs: Any) -> None:
74
+ """Redact the latest message appended to the session.
75
+
76
+ Args:
77
+ redact_message: New message to use that contains the redact content
78
+ agent: Agent to apply the message redaction to
79
+ **kwargs: Additional keyword arguments for future extensibility.
80
+ """
81
+ latest_agent_message = self._latest_agent_message[agent.agent_id]
82
+ if latest_agent_message is None:
83
+ raise SessionException("No message to redact.")
84
+ latest_agent_message.redact_message = redact_message
85
+ return self.session_repository.update_message(self.session_id, agent.agent_id, latest_agent_message)
86
+
87
+ def sync_agent(self, agent: "Agent", **kwargs: Any) -> None:
88
+ """Serialize and update the agent into the session repository.
89
+
90
+ Args:
91
+ agent: Agent to sync to the session.
92
+ **kwargs: Additional keyword arguments for future extensibility.
93
+ """
94
+ self.session_repository.update_agent(
95
+ self.session_id,
96
+ SessionAgent.from_agent(agent),
97
+ )
98
+
99
+ def initialize(self, agent: "Agent", **kwargs: Any) -> None:
100
+ """Initialize an agent with a session.
101
+
102
+ Args:
103
+ agent: Agent to initialize from the session
104
+ **kwargs: Additional keyword arguments for future extensibility.
105
+ """
106
+ if agent.agent_id in self._latest_agent_message:
107
+ raise SessionException("The `agent_id` of an agent must be unique in a session.")
108
+ self._latest_agent_message[agent.agent_id] = None
109
+
110
+ session_agent = self.session_repository.read_agent(self.session_id, agent.agent_id)
111
+
112
+ if session_agent is None:
113
+ logger.debug(
114
+ "agent_id=<%s> | session_id=<%s> | creating agent",
115
+ agent.agent_id,
116
+ self.session_id,
117
+ )
118
+
119
+ session_agent = SessionAgent.from_agent(agent)
120
+ self.session_repository.create_agent(self.session_id, session_agent)
121
+ # Initialize messages with sequential indices
122
+ session_message = None
123
+ for i, message in enumerate(agent.messages):
124
+ session_message = SessionMessage.from_message(message, i)
125
+ self.session_repository.create_message(self.session_id, agent.agent_id, session_message)
126
+ self._latest_agent_message[agent.agent_id] = session_message
127
+ else:
128
+ logger.debug(
129
+ "agent_id=<%s> | session_id=<%s> | restoring agent",
130
+ agent.agent_id,
131
+ self.session_id,
132
+ )
133
+ agent.state = AgentState(session_agent.state)
134
+
135
+ # Restore the conversation manager to its previous state, and get the optional prepend messages
136
+ prepend_messages = agent.conversation_manager.restore_from_session(session_agent.conversation_manager_state)
137
+
138
+ if prepend_messages is None:
139
+ prepend_messages = []
140
+
141
+ # List the messages currently in the session, using an offset of the messages previously removed
142
+ # by the conversation manager.
143
+ session_messages = self.session_repository.list_messages(
144
+ session_id=self.session_id,
145
+ agent_id=agent.agent_id,
146
+ offset=agent.conversation_manager.removed_message_count,
147
+ )
148
+ if len(session_messages) > 0:
149
+ self._latest_agent_message[agent.agent_id] = session_messages[-1]
150
+
151
+ # Restore the agents messages array including the optional prepend messages
152
+ agent.messages = prepend_messages + [session_message.to_message() for session_message in session_messages]
@@ -0,0 +1,272 @@
1
+ """S3-based session manager for cloud storage."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any, Dict, List, Optional, cast
6
+
7
+ import boto3
8
+ from botocore.config import Config as BotocoreConfig
9
+ from botocore.exceptions import ClientError
10
+
11
+ from ..types.exceptions import SessionException
12
+ from ..types.session import Session, SessionAgent, SessionMessage
13
+ from .repository_session_manager import RepositorySessionManager
14
+ from .session_repository import SessionRepository
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ SESSION_PREFIX = "session_"
19
+ AGENT_PREFIX = "agent_"
20
+ MESSAGE_PREFIX = "message_"
21
+
22
+
23
+ class S3SessionManager(RepositorySessionManager, SessionRepository):
24
+ """S3-based session manager for cloud storage.
25
+
26
+ Creates the following filesystem structure for the session storage:
27
+ ```bash
28
+ /<sessions_dir>/
29
+ └── session_<session_id>/
30
+ ├── session.json # Session metadata
31
+ └── agents/
32
+ └── agent_<agent_id>/
33
+ ├── agent.json # Agent metadata
34
+ └── messages/
35
+ ├── message_<id1>.json
36
+ └── message_<id2>.json
37
+ ```
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ session_id: str,
43
+ bucket: str,
44
+ prefix: str = "",
45
+ boto_session: Optional[boto3.Session] = None,
46
+ boto_client_config: Optional[BotocoreConfig] = None,
47
+ region_name: Optional[str] = None,
48
+ **kwargs: Any,
49
+ ):
50
+ """Initialize S3SessionManager with S3 storage.
51
+
52
+ Args:
53
+ session_id: ID for the session
54
+ bucket: S3 bucket name (required)
55
+ prefix: S3 key prefix for storage organization
56
+ boto_session: Optional boto3 session
57
+ boto_client_config: Optional boto3 client configuration
58
+ region_name: AWS region for S3 storage
59
+ **kwargs: Additional keyword arguments for future extensibility.
60
+ """
61
+ self.bucket = bucket
62
+ self.prefix = prefix
63
+
64
+ session = boto_session or boto3.Session(region_name=region_name)
65
+
66
+ # Add strands-agents to the request user agent
67
+ if boto_client_config:
68
+ existing_user_agent = getattr(boto_client_config, "user_agent_extra", None)
69
+ # Append 'strands-agents' to existing user_agent_extra or set it if not present
70
+ if existing_user_agent:
71
+ new_user_agent = f"{existing_user_agent} strands-agents"
72
+ else:
73
+ new_user_agent = "strands-agents"
74
+ client_config = boto_client_config.merge(BotocoreConfig(user_agent_extra=new_user_agent))
75
+ else:
76
+ client_config = BotocoreConfig(user_agent_extra="strands-agents")
77
+
78
+ self.client = session.client(service_name="s3", config=client_config)
79
+ super().__init__(session_id=session_id, session_repository=self)
80
+
81
+ def _get_session_path(self, session_id: str) -> str:
82
+ """Get session S3 prefix."""
83
+ return f"{self.prefix}/{SESSION_PREFIX}{session_id}/"
84
+
85
+ def _get_agent_path(self, session_id: str, agent_id: str) -> str:
86
+ """Get agent S3 prefix."""
87
+ session_path = self._get_session_path(session_id)
88
+ return f"{session_path}agents/{AGENT_PREFIX}{agent_id}/"
89
+
90
+ def _get_message_path(self, session_id: str, agent_id: str, message_id: int) -> str:
91
+ """Get message S3 key.
92
+
93
+ Args:
94
+ session_id: ID of the session
95
+ agent_id: ID of the agent
96
+ message_id: Index of the message
97
+ **kwargs: Additional keyword arguments for future extensibility.
98
+
99
+ Returns:
100
+ The key for the message
101
+ """
102
+ agent_path = self._get_agent_path(session_id, agent_id)
103
+ return f"{agent_path}messages/{MESSAGE_PREFIX}{message_id}.json"
104
+
105
+ def _read_s3_object(self, key: str) -> Optional[Dict[str, Any]]:
106
+ """Read JSON object from S3."""
107
+ try:
108
+ response = self.client.get_object(Bucket=self.bucket, Key=key)
109
+ content = response["Body"].read().decode("utf-8")
110
+ return cast(dict[str, Any], json.loads(content))
111
+ except ClientError as e:
112
+ if e.response["Error"]["Code"] == "NoSuchKey":
113
+ return None
114
+ else:
115
+ raise SessionException(f"S3 error reading {key}: {e}") from e
116
+ except json.JSONDecodeError as e:
117
+ raise SessionException(f"Invalid JSON in S3 object {key}: {e}") from e
118
+
119
+ def _write_s3_object(self, key: str, data: Dict[str, Any]) -> None:
120
+ """Write JSON object to S3."""
121
+ try:
122
+ content = json.dumps(data, indent=2, ensure_ascii=False)
123
+ self.client.put_object(
124
+ Bucket=self.bucket, Key=key, Body=content.encode("utf-8"), ContentType="application/json"
125
+ )
126
+ except ClientError as e:
127
+ raise SessionException(f"Failed to write S3 object {key}: {e}") from e
128
+
129
+ def create_session(self, session: Session, **kwargs: Any) -> Session:
130
+ """Create a new session in S3."""
131
+ session_key = f"{self._get_session_path(session.session_id)}session.json"
132
+
133
+ # Check if session already exists
134
+ try:
135
+ self.client.head_object(Bucket=self.bucket, Key=session_key)
136
+ raise SessionException(f"Session {session.session_id} already exists")
137
+ except ClientError as e:
138
+ if e.response["Error"]["Code"] != "404":
139
+ raise SessionException(f"S3 error checking session existence: {e}") from e
140
+
141
+ # Write session object
142
+ session_dict = session.to_dict()
143
+ self._write_s3_object(session_key, session_dict)
144
+ return session
145
+
146
+ def read_session(self, session_id: str, **kwargs: Any) -> Optional[Session]:
147
+ """Read session data from S3."""
148
+ session_key = f"{self._get_session_path(session_id)}session.json"
149
+ session_data = self._read_s3_object(session_key)
150
+ if session_data is None:
151
+ return None
152
+ return Session.from_dict(session_data)
153
+
154
+ def delete_session(self, session_id: str, **kwargs: Any) -> None:
155
+ """Delete session and all associated data from S3."""
156
+ session_prefix = self._get_session_path(session_id)
157
+ try:
158
+ paginator = self.client.get_paginator("list_objects_v2")
159
+ pages = paginator.paginate(Bucket=self.bucket, Prefix=session_prefix)
160
+
161
+ objects_to_delete = []
162
+ for page in pages:
163
+ if "Contents" in page:
164
+ objects_to_delete.extend([{"Key": obj["Key"]} for obj in page["Contents"]])
165
+
166
+ if not objects_to_delete:
167
+ raise SessionException(f"Session {session_id} does not exist")
168
+
169
+ # Delete objects in batches
170
+ for i in range(0, len(objects_to_delete), 1000):
171
+ batch = objects_to_delete[i : i + 1000]
172
+ self.client.delete_objects(Bucket=self.bucket, Delete={"Objects": batch})
173
+
174
+ except ClientError as e:
175
+ raise SessionException(f"S3 error deleting session {session_id}: {e}") from e
176
+
177
+ def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
178
+ """Create a new agent in S3."""
179
+ agent_id = session_agent.agent_id
180
+ agent_dict = session_agent.to_dict()
181
+ agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
182
+ self._write_s3_object(agent_key, agent_dict)
183
+
184
+ def read_agent(self, session_id: str, agent_id: str, **kwargs: Any) -> Optional[SessionAgent]:
185
+ """Read agent data from S3."""
186
+ agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
187
+ agent_data = self._read_s3_object(agent_key)
188
+ if agent_data is None:
189
+ return None
190
+ return SessionAgent.from_dict(agent_data)
191
+
192
+ def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs: Any) -> None:
193
+ """Update agent data in S3."""
194
+ agent_id = session_agent.agent_id
195
+ previous_agent = self.read_agent(session_id=session_id, agent_id=agent_id)
196
+ if previous_agent is None:
197
+ raise SessionException(f"Agent {agent_id} in session {session_id} does not exist")
198
+
199
+ # Preserve creation timestamp
200
+ session_agent.created_at = previous_agent.created_at
201
+ agent_key = f"{self._get_agent_path(session_id, agent_id)}agent.json"
202
+ self._write_s3_object(agent_key, session_agent.to_dict())
203
+
204
+ def create_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
205
+ """Create a new message in S3."""
206
+ message_id = session_message.message_id
207
+ message_dict = session_message.to_dict()
208
+ message_key = self._get_message_path(session_id, agent_id, message_id)
209
+ self._write_s3_object(message_key, message_dict)
210
+
211
+ def read_message(self, session_id: str, agent_id: str, message_id: int, **kwargs: Any) -> Optional[SessionMessage]:
212
+ """Read message data from S3."""
213
+ message_key = self._get_message_path(session_id, agent_id, message_id)
214
+ message_data = self._read_s3_object(message_key)
215
+ if message_data is None:
216
+ return None
217
+ return SessionMessage.from_dict(message_data)
218
+
219
+ def update_message(self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs: Any) -> None:
220
+ """Update message data in S3."""
221
+ message_id = session_message.message_id
222
+ previous_message = self.read_message(session_id=session_id, agent_id=agent_id, message_id=message_id)
223
+ if previous_message is None:
224
+ raise SessionException(f"Message {message_id} does not exist")
225
+
226
+ # Preserve creation timestamp
227
+ session_message.created_at = previous_message.created_at
228
+ message_key = self._get_message_path(session_id, agent_id, message_id)
229
+ self._write_s3_object(message_key, session_message.to_dict())
230
+
231
+ def list_messages(
232
+ self, session_id: str, agent_id: str, limit: Optional[int] = None, offset: int = 0, **kwargs: Any
233
+ ) -> List[SessionMessage]:
234
+ """List messages for an agent with pagination from S3."""
235
+ messages_prefix = f"{self._get_agent_path(session_id, agent_id)}messages/"
236
+ try:
237
+ paginator = self.client.get_paginator("list_objects_v2")
238
+ pages = paginator.paginate(Bucket=self.bucket, Prefix=messages_prefix)
239
+
240
+ # Collect all message keys and extract their indices
241
+ message_index_keys: list[tuple[int, str]] = []
242
+ for page in pages:
243
+ if "Contents" in page:
244
+ for obj in page["Contents"]:
245
+ key = obj["Key"]
246
+ if key.endswith(".json") and MESSAGE_PREFIX in key:
247
+ # Extract the filename part from the full S3 key
248
+ filename = key.split("/")[-1]
249
+ # Extract index from message_<index>.json format
250
+ index = int(filename[len(MESSAGE_PREFIX) : -5]) # Remove prefix and .json suffix
251
+ message_index_keys.append((index, key))
252
+
253
+ # Sort by index and extract just the keys
254
+ message_keys = [k for _, k in sorted(message_index_keys)]
255
+
256
+ # Apply pagination to keys before loading content
257
+ if limit is not None:
258
+ message_keys = message_keys[offset : offset + limit]
259
+ else:
260
+ message_keys = message_keys[offset:]
261
+
262
+ # Load only the required message objects
263
+ messages: List[SessionMessage] = []
264
+ for key in message_keys:
265
+ message_data = self._read_s3_object(key)
266
+ if message_data:
267
+ messages.append(SessionMessage.from_dict(message_data))
268
+
269
+ return messages
270
+
271
+ except ClientError as e:
272
+ raise SessionException(f"S3 error reading messages: {e}") from e