griptape-nodes 0.57.1__py3-none-any.whl → 0.58.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.
Files changed (51) hide show
  1. griptape_nodes/api_client/__init__.py +9 -0
  2. griptape_nodes/api_client/client.py +279 -0
  3. griptape_nodes/api_client/request_client.py +273 -0
  4. griptape_nodes/app/app.py +57 -150
  5. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +1 -1
  6. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +22 -50
  7. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +6 -1
  8. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +27 -46
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +7 -0
  10. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +3 -1
  11. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +3 -1
  12. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +16 -1
  13. griptape_nodes/common/node_executor.py +466 -0
  14. griptape_nodes/drivers/storage/base_storage_driver.py +0 -11
  15. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +7 -25
  16. griptape_nodes/drivers/storage/local_storage_driver.py +2 -2
  17. griptape_nodes/exe_types/connections.py +37 -9
  18. griptape_nodes/exe_types/core_types.py +1 -1
  19. griptape_nodes/exe_types/node_types.py +115 -22
  20. griptape_nodes/machines/control_flow.py +48 -7
  21. griptape_nodes/machines/parallel_resolution.py +98 -29
  22. griptape_nodes/machines/sequential_resolution.py +61 -22
  23. griptape_nodes/node_library/library_registry.py +24 -1
  24. griptape_nodes/node_library/workflow_registry.py +38 -2
  25. griptape_nodes/retained_mode/events/execution_events.py +8 -1
  26. griptape_nodes/retained_mode/events/flow_events.py +90 -3
  27. griptape_nodes/retained_mode/events/node_events.py +17 -10
  28. griptape_nodes/retained_mode/events/workflow_events.py +5 -0
  29. griptape_nodes/retained_mode/griptape_nodes.py +16 -219
  30. griptape_nodes/retained_mode/managers/config_manager.py +0 -46
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +225 -74
  32. griptape_nodes/retained_mode/managers/flow_manager.py +1276 -230
  33. griptape_nodes/retained_mode/managers/library_manager.py +7 -8
  34. griptape_nodes/retained_mode/managers/node_manager.py +197 -9
  35. griptape_nodes/retained_mode/managers/secrets_manager.py +26 -0
  36. griptape_nodes/retained_mode/managers/session_manager.py +264 -227
  37. griptape_nodes/retained_mode/managers/settings.py +4 -38
  38. griptape_nodes/retained_mode/managers/static_files_manager.py +3 -3
  39. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +135 -6
  40. griptape_nodes/retained_mode/managers/workflow_manager.py +206 -78
  41. griptape_nodes/servers/mcp.py +23 -15
  42. griptape_nodes/utils/async_utils.py +36 -0
  43. griptape_nodes/utils/dict_utils.py +8 -2
  44. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +11 -6
  45. griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +12 -5
  46. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/METADATA +4 -3
  47. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/RECORD +49 -47
  48. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/WHEEL +1 -1
  49. griptape_nodes/retained_mode/utils/engine_identity.py +0 -245
  50. griptape_nodes/servers/ws_request_manager.py +0 -268
  51. {griptape_nodes-0.57.1.dist-info → griptape_nodes-0.58.0.dist-info}/entry_points.txt +0 -0
@@ -6,323 +6,360 @@ Supports multiple concurrent sessions per engine with one active session managed
6
6
  Storage structure: ~/.local/state/griptape_nodes/engines/{engine_id}/sessions.json
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import json
10
12
  import logging
13
+ import uuid
11
14
  from datetime import UTC, datetime
12
- from pathlib import Path
15
+ from typing import TYPE_CHECKING
13
16
 
17
+ from pydantic import BaseModel
14
18
  from xdg_base_dirs import xdg_state_home
15
19
 
16
- from griptape_nodes.retained_mode.events.base_events import BaseEvent
17
- from griptape_nodes.retained_mode.managers.event_manager import EventManager
20
+ from griptape_nodes.retained_mode.events.app_events import (
21
+ AppEndSessionRequest,
22
+ AppEndSessionResultFailure,
23
+ AppEndSessionResultSuccess,
24
+ AppGetSessionRequest,
25
+ AppGetSessionResultSuccess,
26
+ AppStartSessionRequest,
27
+ AppStartSessionResultSuccess,
28
+ SessionHeartbeatRequest,
29
+ SessionHeartbeatResultFailure,
30
+ SessionHeartbeatResultSuccess,
31
+ )
32
+ from griptape_nodes.retained_mode.events.base_events import BaseEvent, ResultPayload
33
+
34
+ if TYPE_CHECKING:
35
+ from pathlib import Path
36
+
37
+ from griptape_nodes.retained_mode.managers.engine_identity_manager import EngineIdentityManager
38
+ from griptape_nodes.retained_mode.managers.event_manager import EventManager
18
39
 
19
40
  logger = logging.getLogger("griptape_nodes")
20
41
 
21
42
 
43
+ class SessionData(BaseModel):
44
+ """Represents a single session's data."""
45
+
46
+ session_id: str
47
+ engine_id: str | None = None
48
+ started_at: str
49
+ last_updated: str
50
+
51
+
52
+ class SessionsStorage(BaseModel):
53
+ """Represents the sessions storage structure."""
54
+
55
+ sessions: list[SessionData]
56
+
57
+
22
58
  class SessionManager:
23
59
  """Manages session saving and active session state."""
24
60
 
25
- _active_session_id: str | None = None
26
-
27
61
  _SESSION_STATE_FILE = "sessions.json"
28
62
 
29
- def __init__(self, event_manager: EventManager | None = None) -> None:
63
+ def __init__(
64
+ self,
65
+ engine_identity_manager: EngineIdentityManager,
66
+ event_manager: EventManager | None = None,
67
+ ) -> None:
30
68
  """Initialize the SessionManager.
31
69
 
32
70
  Args:
71
+ engine_identity_manager: The EngineIdentityManager instance to use for engine ID operations.
33
72
  event_manager: The EventManager instance to use for event handling.
34
73
  """
74
+ self._engine_identity_manager = engine_identity_manager
75
+ self._sessions_data = self._load_sessions_data()
76
+ self._active_session_id = self._get_or_initialize_active_session()
35
77
  BaseEvent._session_id = self._active_session_id
36
78
  if event_manager is not None:
37
- # Register event handlers here when session events are defined
38
- pass
79
+ event_manager.assign_manager_to_request_type(AppStartSessionRequest, self.handle_session_start_request)
80
+ event_manager.assign_manager_to_request_type(AppEndSessionRequest, self.handle_session_end_request)
81
+ event_manager.assign_manager_to_request_type(AppGetSessionRequest, self.handle_get_session_request)
82
+ event_manager.assign_manager_to_request_type(SessionHeartbeatRequest, self.handle_session_heartbeat_request)
39
83
 
40
- @classmethod
41
- def _get_session_state_dir(cls, engine_id: str | None = None) -> Path:
42
- """Get the XDG state directory for session storage.
84
+ @property
85
+ def active_session_id(self) -> str | None:
86
+ """Get the active session ID.
43
87
 
44
- Args:
45
- engine_id: Optional engine ID to create engine-specific directory
88
+ Returns:
89
+ str | None: The active session ID or None if not set
46
90
  """
47
- base_dir = xdg_state_home() / "griptape_nodes"
48
- if engine_id:
49
- return base_dir / "engines" / engine_id
50
- return base_dir
91
+ return self._active_session_id
51
92
 
52
- @classmethod
53
- def _get_session_state_file(cls, engine_id: str | None = None) -> Path:
54
- """Get the path to the session state storage file.
93
+ @active_session_id.setter
94
+ def active_session_id(self, session_id: str) -> None:
95
+ """Set the active session ID.
55
96
 
56
97
  Args:
57
- engine_id: Optional engine ID to get engine-specific session file
98
+ session_id: The session ID to set as active
58
99
  """
59
- return cls._get_session_state_dir(engine_id) / cls._SESSION_STATE_FILE
60
-
61
- @classmethod
62
- def _load_sessions_data(cls, engine_id: str | None = None) -> dict:
63
- """Load sessions data from storage.
100
+ self._active_session_id = session_id
101
+ BaseEvent._session_id = session_id
102
+ logger.debug("Set active session ID to: %s", session_id)
64
103
 
65
- Args:
66
- engine_id: Optional engine ID to load engine-specific sessions
104
+ @property
105
+ def all_sessions(self) -> list[SessionData]:
106
+ """Get all registered sessions for the current engine.
67
107
 
68
108
  Returns:
69
- dict: Sessions data structure with sessions array
109
+ list[SessionData]: List of all session data for the current engine
70
110
  """
71
- session_state_file = cls._get_session_state_file(engine_id)
111
+ return self._sessions_data.sessions
72
112
 
73
- if session_state_file.exists():
74
- try:
75
- with session_state_file.open("r") as f:
76
- data = json.load(f)
77
- if isinstance(data, dict) and "sessions" in data:
78
- return {"sessions": data["sessions"]}
79
- except (json.JSONDecodeError, OSError):
80
- pass
113
+ def save_session(self, session_id: str) -> None:
114
+ """Save a session and make it the active session.
81
115
 
82
- return {"sessions": []}
116
+ Args:
117
+ session_id: The session ID to save
118
+ """
119
+ engine_id = self._get_current_engine_id()
120
+ session_data = SessionData(
121
+ session_id=session_id,
122
+ engine_id=engine_id,
123
+ started_at=datetime.now(tz=UTC).isoformat(),
124
+ last_updated=datetime.now(tz=UTC).isoformat(),
125
+ )
83
126
 
84
- @classmethod
85
- def _save_sessions_data(cls, sessions_data: dict, engine_id: str | None = None) -> None:
86
- """Save sessions data to storage.
127
+ # Add or update the session
128
+ self._add_or_update_session(session_data)
129
+
130
+ # Set as active session
131
+ self._active_session_id = session_id
132
+ BaseEvent._session_id = session_id
133
+ logger.info("Saved and activated session: %s for engine: %s", session_id, engine_id)
134
+
135
+ def remove_session(self, session_id: str) -> None:
136
+ """Remove a session from the sessions data for the current engine.
87
137
 
88
138
  Args:
89
- sessions_data: Sessions data structure to save
90
- engine_id: Optional engine ID to save engine-specific sessions
139
+ session_id: The session ID to remove
91
140
  """
92
- session_state_dir = cls._get_session_state_dir(engine_id)
93
- session_state_dir.mkdir(parents=True, exist_ok=True)
141
+ engine_id = self._get_current_engine_id()
94
142
 
95
- session_state_file = cls._get_session_state_file(engine_id)
96
- with session_state_file.open("w") as f:
97
- json.dump(sessions_data, f, indent=2)
143
+ # Remove the session
144
+ self._sessions_data.sessions = [
145
+ session for session in self._sessions_data.sessions if session.session_id != session_id
146
+ ]
98
147
 
99
- @classmethod
100
- def _get_current_engine_id(cls) -> str | None:
101
- """Get the current engine ID from EngineIdentityManager.
148
+ # Clear active session if it was the removed session
149
+ if self._active_session_id == session_id:
150
+ # Set to first remaining session or None
151
+ self._active_session_id = (
152
+ self._sessions_data.sessions[0].session_id if self._sessions_data.sessions else None
153
+ )
154
+ BaseEvent._session_id = self._active_session_id
155
+ logger.info(
156
+ "Removed active session %s for engine %s, set new active session to: %s",
157
+ session_id,
158
+ engine_id,
159
+ self._active_session_id,
160
+ )
102
161
 
103
- Returns:
104
- str | None: The current engine ID or None if not set
105
- """
106
- # Import here to avoid circular imports
107
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
162
+ self._save_sessions_data(self._sessions_data, engine_id)
163
+ logger.info("Removed session: %s from engine: %s", session_id, engine_id)
108
164
 
109
- return GriptapeNodes.EngineIdentityManager().get_active_engine_id()
165
+ def clear_saved_session(self) -> None:
166
+ """Clear all saved session data for the current engine."""
167
+ # Clear active session
168
+ self._active_session_id = None
169
+ BaseEvent._session_id = None
110
170
 
111
- @classmethod
112
- def _find_session_by_id(cls, sessions_data: dict, session_id: str) -> dict | None:
113
- """Find a session by ID in the sessions data.
171
+ # Clear in-memory session data
172
+ self._sessions_data = SessionsStorage(sessions=[])
114
173
 
115
- Args:
116
- sessions_data: The sessions data structure
117
- session_id: The session ID to find
174
+ engine_id = self._get_current_engine_id()
175
+ session_state_file = self._get_session_state_file(engine_id)
176
+ if session_state_file.exists():
177
+ try:
178
+ session_state_file.unlink()
179
+ logger.info("Cleared all saved session data for engine: %s", engine_id)
180
+ except OSError:
181
+ # If we can't delete the file, just clear its contents
182
+ self._save_sessions_data(self._sessions_data, engine_id)
183
+ logger.warning("Could not delete session file for engine %s, cleared contents instead", engine_id)
184
+
185
+ def _get_or_initialize_active_session(self) -> str | None:
186
+ """Get or initialize the active session ID.
187
+
188
+ Falls back to first available session if no active session is set.
118
189
 
119
190
  Returns:
120
- dict | None: The session data if found, None otherwise
191
+ str | None: The active session ID or None if no sessions exist
121
192
  """
122
- for session in sessions_data.get("sessions", []):
123
- if session.get("session_id") == session_id:
124
- return session
193
+ # Fall back to first session if available
194
+ if self._sessions_data.sessions:
195
+ first_session = self._sessions_data.sessions[0]
196
+ logger.debug(
197
+ "Initialized active session to first saved session: %s for engine: %s",
198
+ first_session.session_id,
199
+ first_session.engine_id,
200
+ )
201
+ return first_session.session_id
202
+
125
203
  return None
126
204
 
127
- @classmethod
128
- def _add_or_update_session(cls, session_data: dict) -> None:
205
+ def _add_or_update_session(self, session_data: SessionData) -> None:
129
206
  """Add or update a session in the sessions data structure.
130
207
 
131
208
  Args:
132
209
  session_data: The session data to add or update
133
210
  """
134
- engine_id = cls._get_current_engine_id()
135
- sessions_data = cls._load_sessions_data(engine_id)
211
+ engine_id = self._get_current_engine_id()
136
212
 
137
213
  # Find existing session
138
- session_id = session_data["session_id"]
139
- existing_session = cls._find_session_by_id(sessions_data, session_id)
214
+ existing_session = self._find_session_by_id(self._sessions_data, session_data.session_id)
140
215
 
141
216
  if existing_session:
142
217
  # Update existing session
143
- existing_session.update(session_data)
144
- existing_session["last_updated"] = datetime.now(tz=UTC).isoformat()
218
+ existing_session.session_id = session_data.session_id
219
+ existing_session.engine_id = session_data.engine_id
220
+ existing_session.started_at = session_data.started_at
221
+ existing_session.last_updated = datetime.now(tz=UTC).isoformat()
145
222
  else:
146
223
  # Add new session
147
- session_data["engine_id"] = engine_id
148
- sessions_data.setdefault("sessions", []).append(session_data)
224
+ self._sessions_data.sessions.append(session_data)
149
225
 
150
- cls._save_sessions_data(sessions_data, engine_id)
226
+ self._save_sessions_data(self._sessions_data, engine_id)
151
227
 
152
- @classmethod
153
- def get_active_session_id(cls) -> str | None:
154
- """Get the active session ID.
228
+ def _get_current_engine_id(self) -> str | None:
229
+ """Get the current engine ID from EngineIdentityManager.
155
230
 
156
231
  Returns:
157
- str | None: The active session ID or None if not set
158
- """
159
- return cls._active_session_id
160
-
161
- @classmethod
162
- def set_active_session_id(cls, session_id: str) -> None:
163
- """Set the active session ID.
164
-
165
- Args:
166
- session_id: The session ID to set as active
167
-
168
- Raises:
169
- ValueError: If session_id is not found in persisted sessions
170
- """
171
- engine_id = cls._get_current_engine_id()
172
- sessions_data = cls._load_sessions_data(engine_id)
173
-
174
- # Verify the session exists
175
- if cls._find_session_by_id(sessions_data, session_id):
176
- cls._active_session_id = session_id
177
- logger.debug("Set active session ID to: %s", session_id)
178
- else:
179
- msg = f"Session with ID {session_id} not found for engine {engine_id}"
180
- raise ValueError(msg)
181
-
182
- @classmethod
183
- def save_session(cls, session_id: str) -> None:
184
- """Save a session and make it the active session.
185
-
186
- Args:
187
- session_id: The session ID to save
232
+ str | None: The current engine ID or None if not set
188
233
  """
189
- engine_id = cls._get_current_engine_id()
190
- session_data = {
191
- "session_id": session_id,
192
- "engine_id": engine_id,
193
- "started_at": datetime.now(tz=UTC).isoformat(),
194
- "last_updated": datetime.now(tz=UTC).isoformat(),
195
- }
196
-
197
- # Add or update the session
198
- cls._add_or_update_session(session_data)
199
-
200
- # Set as active session
201
- cls._active_session_id = session_id
202
- BaseEvent._session_id = session_id
203
- logger.info("Saved and activated session: %s for engine: %s", session_id, engine_id)
234
+ return self._engine_identity_manager.active_engine_id
204
235
 
205
- @classmethod
206
- def get_saved_session_id(cls) -> str | None:
207
- """Get the active session ID if it exists.
236
+ def _load_sessions_data(self) -> SessionsStorage:
237
+ """Load sessions data from storage.
208
238
 
209
239
  Returns:
210
- str | None: The active session ID or None if no active session
240
+ SessionsStorage: Sessions data structure with sessions array
211
241
  """
212
- # Return active session if set
213
- if cls._active_session_id:
214
- return cls._active_session_id
215
-
216
- # Fall back to first session if available
217
- engine_id = cls._get_current_engine_id()
218
- sessions_data = cls._load_sessions_data(engine_id)
219
- sessions = sessions_data.get("sessions", [])
220
- if sessions:
221
- first_session_id = sessions[0].get("session_id")
222
- # Set as active for future calls
223
- BaseEvent._session_id = first_session_id
224
- cls._active_session_id = first_session_id
225
- logger.debug("Retrieved first saved session as active: %s for engine: %s", first_session_id, engine_id)
226
- return first_session_id
227
-
228
- return None
242
+ engine_id = self._get_current_engine_id()
243
+ session_state_file = self._get_session_state_file(engine_id)
229
244
 
230
- @classmethod
231
- def clear_saved_session(cls) -> None:
232
- """Clear all saved session data for the current engine."""
233
- # Clear active session
234
- cls._active_session_id = None
235
- BaseEvent._session_id = None
236
-
237
- engine_id = cls._get_current_engine_id()
238
- session_state_file = cls._get_session_state_file(engine_id)
239
245
  if session_state_file.exists():
240
246
  try:
241
- session_state_file.unlink()
242
- logger.info("Cleared all saved session data for engine: %s", engine_id)
243
- except OSError:
244
- # If we can't delete the file, just clear its contents
245
- cls._save_sessions_data({"sessions": []}, engine_id)
246
- logger.warning("Could not delete session file for engine %s, cleared contents instead", engine_id)
247
-
248
- @classmethod
249
- def has_saved_session(cls) -> bool:
250
- """Check if there is a saved session.
251
-
252
- Returns:
253
- bool: True if there is a saved session, False otherwise
254
- """
255
- return cls.get_saved_session_id() is not None
256
-
257
- @classmethod
258
- def get_all_sessions(cls) -> list[dict]:
259
- """Get all registered sessions for the current engine.
247
+ with session_state_file.open("r") as f:
248
+ data = json.load(f)
249
+ if isinstance(data, dict) and "sessions" in data:
250
+ return SessionsStorage.model_validate(data)
251
+ except (json.JSONDecodeError, OSError):
252
+ pass
260
253
 
261
- Returns:
262
- list[dict]: List of all session data for the current engine
263
- """
264
- engine_id = cls._get_current_engine_id()
265
- sessions_data = cls._load_sessions_data(engine_id)
266
- return sessions_data.get("sessions", [])
254
+ return SessionsStorage(sessions=[])
267
255
 
268
- @classmethod
269
- def remove_session(cls, session_id: str) -> None:
270
- """Remove a session from the sessions data for the current engine.
256
+ def _save_sessions_data(self, sessions_data: SessionsStorage, engine_id: str | None = None) -> None:
257
+ """Save sessions data to storage.
271
258
 
272
259
  Args:
273
- session_id: The session ID to remove
260
+ sessions_data: Sessions data structure to save
261
+ engine_id: Optional engine ID to save engine-specific sessions
274
262
  """
275
- engine_id = cls._get_current_engine_id()
276
- sessions_data = cls._load_sessions_data(engine_id)
277
-
278
- # Remove the session
279
- sessions_data["sessions"] = [
280
- session for session in sessions_data.get("sessions", []) if session.get("session_id") != session_id
281
- ]
263
+ session_state_dir = self._get_session_state_dir(engine_id)
264
+ session_state_dir.mkdir(parents=True, exist_ok=True)
282
265
 
283
- # Clear active session if it was the removed session
284
- if cls._active_session_id == session_id:
285
- # Set to first remaining session or None
286
- remaining_sessions = sessions_data.get("sessions", [])
287
- cls._active_session_id = remaining_sessions[0].get("session_id") if remaining_sessions else None
288
- logger.info(
289
- "Removed active session %s for engine %s, set new active session to: %s",
290
- session_id,
291
- engine_id,
292
- cls._active_session_id,
266
+ session_state_file = self._get_session_state_file(engine_id)
267
+ with session_state_file.open("w") as f:
268
+ json.dump(sessions_data.model_dump(exclude_none=True), f, indent=2)
269
+
270
+ # Update in-memory copy
271
+ self._sessions_data = sessions_data
272
+
273
+ async def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload: # noqa: ARG002
274
+ current_session_id = self.active_session_id
275
+ if current_session_id is None:
276
+ # Client wants a new session
277
+ current_session_id = uuid.uuid4().hex
278
+ self.save_session(current_session_id)
279
+ details = f"New session '{current_session_id}' started at {datetime.now(tz=UTC)}."
280
+ logger.info(details)
281
+ else:
282
+ details = f"Session '{current_session_id}' already active. Joining..."
283
+
284
+ return AppStartSessionResultSuccess(current_session_id, result_details="Session started successfully.")
285
+
286
+ async def handle_session_end_request(self, _: AppEndSessionRequest) -> ResultPayload:
287
+ try:
288
+ previous_session_id = self.active_session_id
289
+ if previous_session_id is None:
290
+ details = "No active session to end."
291
+ logger.info(details)
292
+ else:
293
+ details = f"Session '{previous_session_id}' ended at {datetime.now(tz=UTC)}."
294
+ logger.info(details)
295
+ self.clear_saved_session()
296
+
297
+ return AppEndSessionResultSuccess(
298
+ session_id=previous_session_id, result_details="Session ended successfully."
293
299
  )
300
+ except Exception as err:
301
+ details = f"Failed to end session due to '{err}'."
302
+ logger.error(details)
303
+ return AppEndSessionResultFailure(result_details=details)
294
304
 
295
- cls._save_sessions_data(sessions_data, engine_id)
296
- logger.info("Removed session: %s from engine: %s", session_id, engine_id)
305
+ def handle_get_session_request(self, _: AppGetSessionRequest) -> ResultPayload:
306
+ return AppGetSessionResultSuccess(
307
+ session_id=self.active_session_id,
308
+ result_details="Session ID retrieved successfully.",
309
+ )
310
+
311
+ def handle_session_heartbeat_request(self, request: SessionHeartbeatRequest) -> ResultPayload: # noqa: ARG002
312
+ """Handle session heartbeat requests.
297
313
 
298
- @classmethod
299
- def get_sessions_for_engine(cls, engine_id: str) -> list[dict]:
300
- """Get all sessions for a specific engine.
314
+ Simply verifies that the session is active and responds with success.
315
+ """
316
+ try:
317
+ active_session_id = self.active_session_id
318
+ if active_session_id is None:
319
+ details = "Session heartbeat received but no active session found"
320
+ logger.warning(details)
321
+ return SessionHeartbeatResultFailure(result_details=details)
322
+
323
+ details = f"Session heartbeat successful for session: {active_session_id}"
324
+ return SessionHeartbeatResultSuccess(result_details=details)
325
+ except Exception as err:
326
+ details = f"Failed to handle session heartbeat: {err}"
327
+ logger.error(details)
328
+ return SessionHeartbeatResultFailure(result_details=details)
329
+
330
+ @staticmethod
331
+ def _find_session_by_id(sessions_data: SessionsStorage, session_id: str) -> SessionData | None:
332
+ """Find a session by ID in the sessions data.
301
333
 
302
334
  Args:
303
- engine_id: The engine ID to get sessions for
335
+ sessions_data: The sessions data structure
336
+ session_id: The session ID to find
304
337
 
305
338
  Returns:
306
- list[dict]: List of session data for the specified engine
339
+ SessionData | None: The session data if found, None otherwise
307
340
  """
308
- sessions_data = cls._load_sessions_data(engine_id)
309
- return sessions_data.get("sessions", [])
341
+ for session in sessions_data.sessions:
342
+ if session.session_id == session_id:
343
+ return session
344
+ return None
310
345
 
311
- @classmethod
312
- def get_all_sessions_across_engines(cls) -> dict[str, list[dict]]:
313
- """Get all sessions across all engines.
346
+ @staticmethod
347
+ def _get_session_state_file(engine_id: str | None = None) -> Path:
348
+ """Get the path to the session state storage file.
314
349
 
315
- Returns:
316
- dict[str, list[dict]]: Dictionary mapping engine IDs to their session lists
350
+ Args:
351
+ engine_id: Optional engine ID to get engine-specific session file
317
352
  """
318
- from griptape_nodes.retained_mode.utils.engine_identity import EngineIdentity
319
-
320
- all_engines = EngineIdentity.get_all_engines()
321
- result = {}
353
+ return SessionManager._get_session_state_dir(engine_id) / SessionManager._SESSION_STATE_FILE
322
354
 
323
- for engine in all_engines:
324
- engine_id = engine.get("engine_id")
325
- if engine_id:
326
- result[engine_id] = cls.get_sessions_for_engine(engine_id)
355
+ @staticmethod
356
+ def _get_session_state_dir(engine_id: str | None = None) -> Path:
357
+ """Get the XDG state directory for session storage.
327
358
 
328
- return result
359
+ Args:
360
+ engine_id: Optional engine ID to create engine-specific directory
361
+ """
362
+ base_dir = xdg_state_home() / "griptape_nodes"
363
+ if engine_id:
364
+ return base_dir / "engines" / engine_id
365
+ return base_dir
@@ -94,6 +94,10 @@ class MCPServerConfig(BaseModel):
94
94
  class AppInitializationComplete(BaseModel):
95
95
  libraries_to_register: list[str] = Field(default_factory=list)
96
96
  workflows_to_register: list[str] = Field(default_factory=list)
97
+ secrets_to_register: list[str] = Field(
98
+ default_factory=lambda: ["HF_TOKEN", "GT_CLOUD_API_KEY"],
99
+ description="Core secrets to register in the secrets manager. Library-specific secrets are registered automatically from library settings.",
100
+ )
97
101
  models_to_download: list[str] = Field(default_factory=list)
98
102
 
99
103
 
@@ -147,44 +151,6 @@ class Settings(BaseModel):
147
151
  category=APPLICATION_EVENTS,
148
152
  default_factory=AppEvents,
149
153
  )
150
- nodes: dict[str, Any] = Field(
151
- category=API_KEYS,
152
- default_factory=lambda: {
153
- "Griptape": {"GT_CLOUD_API_KEY": "$GT_CLOUD_API_KEY"},
154
- "OpenAI": {"OPENAI_API_KEY": "$OPENAI_API_KEY"},
155
- "Amazon": {
156
- "AWS_ACCESS_KEY_ID": "$AWS_ACCESS_KEY_ID",
157
- "AWS_SECRET_ACCESS_KEY": "$AWS_SECRET_ACCESS_KEY",
158
- "AWS_DEFAULT_REGION": "$AWS_DEFAULT_REGION",
159
- "AMAZON_OPENSEARCH_HOST": "$AMAZON_OPENSEARCH_HOST",
160
- "AMAZON_OPENSEARCH_INDEX_NAME": "$AMAZON_OPENSEARCH_INDEX_NAME",
161
- },
162
- "Anthropic": {"ANTHROPIC_API_KEY": "$ANTHROPIC_API_KEY"},
163
- "BlackForest Labs": {"BFL_API_KEY": "$BFL_API_KEY"},
164
- "Microsoft Azure": {
165
- "AZURE_OPENAI_ENDPOINT": "$AZURE_OPENAI_ENDPOINT",
166
- "AZURE_OPENAI_DALL_E_3_ENDPOINT": "$AZURE_OPENAI_DALL_E_3_ENDPOINT",
167
- "AZURE_OPENAI_DALL_E_3_API_KEY": "$AZURE_OPENAI_DALL_E_3_API_KEY",
168
- "AZURE_OPENAI_API_KEY": "$AZURE_OPENAI_API_KEY",
169
- },
170
- "Cohere": {"COHERE_API_KEY": "$COHERE_API_KEY"},
171
- "Eleven Labs": {"ELEVEN_LABS_API_KEY": "$ELEVEN_LABS_API_KEY"},
172
- "Exa": {"EXA_API_KEY": "$EXA_API_KEY"},
173
- "Grok": {"GROK_API_KEY": "$GROK_API_KEY"},
174
- "Groq": {"GROQ_API_KEY": "$GROQ_API_KEY"},
175
- "Nvidia": {"NVIDIA_API_KEY": "$NVIDIA_API_KEY"},
176
- "Google": {"GOOGLE_API_KEY": "$GOOGLE_API_KEY", "GOOGLE_API_SEARCH_ID": "$GOOGLE_API_SEARCH_ID"},
177
- "Huggingface": {"HUGGINGFACE_HUB_ACCESS_TOKEN": "$HUGGINGFACE_HUB_ACCESS_TOKEN"},
178
- "LeonardoAI": {"LEONARDO_API_KEY": "$LEONARDO_API_KEY"},
179
- "Pinecone": {
180
- "PINECONE_API_KEY": "$PINECONE_API_KEY",
181
- "PINECONE_ENVIRONMENT": "$PINECONE_ENVIRONMENT",
182
- "PINECONE_INDEX_NAME": "$PINECONE_INDEX_NAME",
183
- },
184
- "Tavily": {"TAVILY_API_KEY": "$TAVILY_API_KEY"},
185
- "Serper": {"SERPER_API_KEY": "$SERPER_API_KEY"},
186
- },
187
- )
188
154
  log_level: LogLevel = Field(category=EXECUTION, default=LogLevel.INFO)
189
155
  workflow_execution_mode: WorkflowExecutionMode = Field(
190
156
  category=EXECUTION,