griptape-nodes 0.51.2__py3-none-any.whl → 0.52.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 (46) hide show
  1. griptape_nodes/__init__.py +5 -4
  2. griptape_nodes/app/api.py +27 -24
  3. griptape_nodes/app/app.py +243 -221
  4. griptape_nodes/app/watch.py +17 -2
  5. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
  6. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
  7. griptape_nodes/exe_types/core_types.py +16 -4
  8. griptape_nodes/exe_types/node_types.py +74 -16
  9. griptape_nodes/machines/control_flow.py +21 -26
  10. griptape_nodes/machines/fsm.py +16 -16
  11. griptape_nodes/machines/node_resolution.py +30 -119
  12. griptape_nodes/mcp_server/server.py +14 -10
  13. griptape_nodes/mcp_server/ws_request_manager.py +2 -2
  14. griptape_nodes/node_library/workflow_registry.py +5 -0
  15. griptape_nodes/retained_mode/events/base_events.py +12 -7
  16. griptape_nodes/retained_mode/events/execution_events.py +0 -6
  17. griptape_nodes/retained_mode/events/node_events.py +38 -0
  18. griptape_nodes/retained_mode/events/parameter_events.py +11 -0
  19. griptape_nodes/retained_mode/events/variable_events.py +361 -0
  20. griptape_nodes/retained_mode/events/workflow_events.py +35 -0
  21. griptape_nodes/retained_mode/griptape_nodes.py +61 -26
  22. griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
  23. griptape_nodes/retained_mode/managers/event_manager.py +215 -74
  24. griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
  25. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
  26. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
  27. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
  28. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
  30. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +20 -19
  33. griptape_nodes/retained_mode/managers/node_manager.py +83 -8
  34. griptape_nodes/retained_mode/managers/object_manager.py +4 -0
  35. griptape_nodes/retained_mode/managers/settings.py +1 -0
  36. griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
  37. griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
  39. griptape_nodes/retained_mode/variable_types.py +18 -0
  40. griptape_nodes/utils/__init__.py +4 -0
  41. griptape_nodes/utils/async_utils.py +89 -0
  42. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/METADATA +2 -3
  43. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/RECORD +45 -42
  44. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/WHEEL +1 -1
  45. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
  46. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/entry_points.txt +0 -0
@@ -4,8 +4,6 @@ import logging
4
4
  from queue import Queue
5
5
  from typing import TYPE_CHECKING, cast
6
6
 
7
- from griptape.events import EventBus
8
-
9
7
  from griptape_nodes.exe_types.connections import Connections
10
8
  from griptape_nodes.exe_types.core_types import (
11
9
  Parameter,
@@ -869,6 +867,8 @@ class FlowManager:
869
867
  node_name=target_node.name,
870
868
  value=value,
871
869
  data_type=source_param.type,
870
+ incoming_connection_source_node_name=source_node.name,
871
+ incoming_connection_source_parameter_name=source_param.name,
872
872
  )
873
873
  )
874
874
 
@@ -1031,7 +1031,7 @@ class FlowManager:
1031
1031
  result = DeleteConnectionResultSuccess()
1032
1032
  return result
1033
1033
 
1034
- def on_start_flow_request(self, request: StartFlowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1034
+ async def on_start_flow_request(self, request: StartFlowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1035
1035
  # which flow
1036
1036
  flow_name = request.flow_name
1037
1037
  debug_mode = request.debug_mode
@@ -1075,7 +1075,7 @@ class FlowManager:
1075
1075
  self.get_start_node_queue() # initialize the start flow queue!
1076
1076
  start_node = None
1077
1077
  # Run Validation before starting a flow
1078
- result = self.on_validate_flow_dependencies_request(
1078
+ result = await self.on_validate_flow_dependencies_request(
1079
1079
  ValidateFlowDependenciesRequest(flow_name=flow_name, flow_node_name=start_node.name if start_node else None)
1080
1080
  )
1081
1081
  try:
@@ -1098,7 +1098,7 @@ class FlowManager:
1098
1098
  return StartFlowResultFailure(validation_exceptions=[e], result_details=details)
1099
1099
  # By now, it has been validated with no exceptions.
1100
1100
  try:
1101
- self.start_flow(flow, start_node, debug_mode)
1101
+ await self.start_flow(flow, start_node, debug_mode)
1102
1102
  except Exception as e:
1103
1103
  details = f"Failed to kick off flow with name {flow_name}. Exception occurred: {e} "
1104
1104
  logger.error(details)
@@ -1157,7 +1157,7 @@ class FlowManager:
1157
1157
 
1158
1158
  return CancelFlowResultSuccess()
1159
1159
 
1160
- def on_single_node_step_request(self, request: SingleNodeStepRequest) -> ResultPayload:
1160
+ async def on_single_node_step_request(self, request: SingleNodeStepRequest) -> ResultPayload:
1161
1161
  flow_name = request.flow_name
1162
1162
  if not flow_name:
1163
1163
  details = "Could not advance to the next step of a running workflow. No flow name was provided."
@@ -1173,7 +1173,7 @@ class FlowManager:
1173
1173
  return SingleNodeStepResultFailure(validation_exceptions=[err], result_details=details)
1174
1174
  try:
1175
1175
  flow = self.get_flow_by_name(flow_name)
1176
- self.single_node_step(flow)
1176
+ await self.single_node_step(flow)
1177
1177
  except Exception as e:
1178
1178
  details = f"Could not advance to the next step of a running workflow. Exception: {e}"
1179
1179
  logger.error(details)
@@ -1185,7 +1185,7 @@ class FlowManager:
1185
1185
 
1186
1186
  return SingleNodeStepResultSuccess()
1187
1187
 
1188
- def on_single_execution_step_request(self, request: SingleExecutionStepRequest) -> ResultPayload:
1188
+ async def on_single_execution_step_request(self, request: SingleExecutionStepRequest) -> ResultPayload:
1189
1189
  flow_name = request.flow_name
1190
1190
  if not flow_name:
1191
1191
  details = "Could not advance to the next step of a running workflow. No flow name was provided."
@@ -1201,7 +1201,7 @@ class FlowManager:
1201
1201
  return SingleExecutionStepResultFailure(result_details=details)
1202
1202
  change_debug_mode = request.request_id is not None
1203
1203
  try:
1204
- self.single_execution_step(flow, change_debug_mode)
1204
+ await self.single_execution_step(flow, change_debug_mode)
1205
1205
  except Exception as e:
1206
1206
  # We REALLY don't want to fail here, else we'll take the whole engine down
1207
1207
  try:
@@ -1219,7 +1219,7 @@ class FlowManager:
1219
1219
 
1220
1220
  return SingleExecutionStepResultSuccess()
1221
1221
 
1222
- def on_continue_execution_step_request(self, request: ContinueExecutionStepRequest) -> ResultPayload:
1222
+ async def on_continue_execution_step_request(self, request: ContinueExecutionStepRequest) -> ResultPayload:
1223
1223
  flow_name = request.flow_name
1224
1224
  if not flow_name:
1225
1225
  details = "Failed to continue execution step because no flow name was provided"
@@ -1234,7 +1234,7 @@ class FlowManager:
1234
1234
 
1235
1235
  return ContinueExecutionStepResultFailure(result_details=details)
1236
1236
  try:
1237
- self.continue_executing(flow)
1237
+ await self.continue_executing(flow)
1238
1238
  except Exception as e:
1239
1239
  details = f"Failed to continue execution step. An exception occurred: {e}."
1240
1240
  logger.error(details)
@@ -1265,7 +1265,7 @@ class FlowManager:
1265
1265
  logger.debug(details)
1266
1266
  return UnresolveFlowResultSuccess()
1267
1267
 
1268
- def on_validate_flow_dependencies_request(self, request: ValidateFlowDependenciesRequest) -> ResultPayload:
1268
+ async def on_validate_flow_dependencies_request(self, request: ValidateFlowDependenciesRequest) -> ResultPayload:
1269
1269
  flow_name = request.flow_name
1270
1270
  # get the flow name
1271
1271
  try:
@@ -1658,7 +1658,7 @@ class FlowManager:
1658
1658
  node.emit_parameter_changes()
1659
1659
  return FlushParameterChangesResultSuccess()
1660
1660
 
1661
- def start_flow(self, flow: ControlFlow, start_node: BaseNode | None = None, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1661
+ async def start_flow(self, flow: ControlFlow, start_node: BaseNode | None = None, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1662
1662
  if self.check_for_existing_running_flow():
1663
1663
  # If flow already exists, throw an error
1664
1664
  errormsg = "This workflow is already in progress. Please wait for the current process to finish before starting again."
@@ -1676,7 +1676,7 @@ class FlowManager:
1676
1676
  self._global_control_flow_machine = ControlFlowMachine()
1677
1677
 
1678
1678
  try:
1679
- self._global_control_flow_machine.start_flow(start_node, debug_mode)
1679
+ await self._global_control_flow_machine.start_flow(start_node, debug_mode)
1680
1680
  except Exception:
1681
1681
  if self.check_for_existing_running_flow():
1682
1682
  self.cancel_flow_run()
@@ -1707,7 +1707,7 @@ class FlowManager:
1707
1707
  self._global_single_node_resolution = False
1708
1708
  logger.debug("Cancelling flow run")
1709
1709
 
1710
- EventBus.publish_event(
1710
+ GriptapeNodes.EventManager().put_event(
1711
1711
  ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=ControlFlowCancelledEvent()))
1712
1712
  )
1713
1713
 
@@ -1754,7 +1754,13 @@ class FlowManager:
1754
1754
  return self._has_connection(source_node, source_parameter, target_node, target_parameter)
1755
1755
 
1756
1756
  # Internal execution queue helper methods to consolidate redundant operations
1757
- def _handle_flow_start_if_not_running(self, flow: ControlFlow, *, debug_mode: bool, error_message: str) -> None: # noqa: ARG002
1757
+ async def _handle_flow_start_if_not_running(
1758
+ self,
1759
+ flow: ControlFlow, # noqa: ARG002
1760
+ *,
1761
+ debug_mode: bool,
1762
+ error_message: str,
1763
+ ) -> None:
1758
1764
  """Common logic for starting flow execution if not already running."""
1759
1765
  if not self.check_for_existing_running_flow():
1760
1766
  if self._global_flow_queue.empty():
@@ -1763,17 +1769,17 @@ class FlowManager:
1763
1769
  self._global_flow_queue.task_done()
1764
1770
  if self._global_control_flow_machine is None:
1765
1771
  self._global_control_flow_machine = ControlFlowMachine()
1766
- self._global_control_flow_machine.start_flow(start_node, debug_mode)
1772
+ await self._global_control_flow_machine.start_flow(start_node, debug_mode)
1767
1773
 
1768
- def _handle_post_execution_queue_processing(self, *, debug_mode: bool) -> None:
1774
+ async def _handle_post_execution_queue_processing(self, *, debug_mode: bool) -> None:
1769
1775
  """Handle execution queue processing after execution completes."""
1770
1776
  if not self.check_for_existing_running_flow() and not self._global_flow_queue.empty():
1771
1777
  start_node = self._global_flow_queue.get()
1772
1778
  self._global_flow_queue.task_done()
1773
1779
  if self._global_control_flow_machine is not None:
1774
- self._global_control_flow_machine.start_flow(start_node, debug_mode)
1780
+ await self._global_control_flow_machine.start_flow(start_node, debug_mode)
1775
1781
 
1776
- def resolve_singular_node(self, flow: ControlFlow, node: BaseNode, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1782
+ async def resolve_singular_node(self, flow: ControlFlow, node: BaseNode, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1777
1783
  # Set that we are only working on one node right now! no other stepping allowed
1778
1784
  if self.check_for_existing_running_flow():
1779
1785
  # If flow already exists, throw an error
@@ -1790,30 +1796,30 @@ class FlowManager:
1790
1796
  resolution_machine.change_debug_mode(debug_mode)
1791
1797
  # Resolve the node.
1792
1798
  node.state = NodeResolutionState.UNRESOLVED
1793
- resolution_machine.resolve_node(node)
1799
+ await resolution_machine.resolve_node(node)
1794
1800
  # decide if we can change it back to normal flow mode!
1795
1801
  if resolution_machine.is_complete():
1796
1802
  self._global_single_node_resolution = False
1797
1803
  self._global_control_flow_machine._context.current_node = None
1798
1804
 
1799
- def single_execution_step(self, flow: ControlFlow, change_debug_mode: bool) -> None: # noqa: FBT001
1805
+ async def single_execution_step(self, flow: ControlFlow, change_debug_mode: bool) -> None: # noqa: FBT001
1800
1806
  # do a granular step
1801
- self._handle_flow_start_if_not_running(
1807
+ await self._handle_flow_start_if_not_running(
1802
1808
  flow, debug_mode=True, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1803
1809
  )
1804
1810
  if not self.check_for_existing_running_flow():
1805
1811
  return
1806
1812
  if self._global_control_flow_machine is not None:
1807
- self._global_control_flow_machine.granular_step(change_debug_mode)
1813
+ await self._global_control_flow_machine.granular_step(change_debug_mode)
1808
1814
  resolution_machine = self._global_control_flow_machine._context.resolution_machine
1809
1815
  if self._global_single_node_resolution:
1810
1816
  resolution_machine = self._global_control_flow_machine._context.resolution_machine
1811
1817
  if resolution_machine.is_complete():
1812
1818
  self._global_single_node_resolution = False
1813
1819
 
1814
- def single_node_step(self, flow: ControlFlow) -> None:
1820
+ async def single_node_step(self, flow: ControlFlow) -> None:
1815
1821
  # It won't call single_node_step without an existing flow running from US.
1816
- self._handle_flow_start_if_not_running(
1822
+ await self._handle_flow_start_if_not_running(
1817
1823
  flow, debug_mode=True, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1818
1824
  )
1819
1825
  if not self.check_for_existing_running_flow():
@@ -1823,12 +1829,12 @@ class FlowManager:
1823
1829
  msg = "Cannot step through the Control Flow in Single Node Execution"
1824
1830
  raise RuntimeError(msg)
1825
1831
  if self._global_control_flow_machine is not None:
1826
- self._global_control_flow_machine.node_step()
1832
+ await self._global_control_flow_machine.node_step()
1827
1833
  # Start the next resolution step now please.
1828
- self._handle_post_execution_queue_processing(debug_mode=True)
1834
+ await self._handle_post_execution_queue_processing(debug_mode=True)
1829
1835
 
1830
- def continue_executing(self, flow: ControlFlow) -> None:
1831
- self._handle_flow_start_if_not_running(
1836
+ async def continue_executing(self, flow: ControlFlow) -> None:
1837
+ await self._handle_flow_start_if_not_running(
1832
1838
  flow, debug_mode=False, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1833
1839
  )
1834
1840
  if not self.check_for_existing_running_flow():
@@ -1840,11 +1846,11 @@ class FlowManager:
1840
1846
  if self._global_control_flow_machine._context.resolution_machine.is_complete():
1841
1847
  self._global_single_node_resolution = False
1842
1848
  else:
1843
- self._global_control_flow_machine._context.resolution_machine.update()
1849
+ await self._global_control_flow_machine._context.resolution_machine.update()
1844
1850
  else:
1845
- self._global_control_flow_machine.node_step()
1851
+ await self._global_control_flow_machine.node_step()
1846
1852
  # Now it is done executing. make sure it's actually done?
1847
- self._handle_post_execution_queue_processing(debug_mode=False)
1853
+ await self._handle_post_execution_queue_processing(debug_mode=False)
1848
1854
 
1849
1855
  def unresolve_whole_flow(self, flow: ControlFlow) -> None:
1850
1856
  for node in flow.nodes.values():
@@ -35,7 +35,7 @@ class LibraryDirectory:
35
35
  # Own all FSM instances for library lifecycle management
36
36
  self._provenance_to_fsm: dict[LibraryProvenance, LibraryLifecycleFSM] = {}
37
37
 
38
- def discover_library(self, provenance: LibraryProvenance) -> None:
38
+ async def discover_library(self, provenance: LibraryProvenance) -> None:
39
39
  """Discover a library and its provenance.
40
40
 
41
41
  Discovery is purely about cataloging - activation state is handled separately.
@@ -49,26 +49,26 @@ class LibraryDirectory:
49
49
  self._discovered_libraries[provenance] = entry
50
50
 
51
51
  # Create FSM and run evaluation automatically
52
- self._create_fsm_and_evaluate(provenance)
52
+ await self._create_fsm_and_evaluate(provenance)
53
53
 
54
- def add_curated_candidate(self, provenance: LibraryProvenance) -> None:
54
+ async def add_curated_candidate(self, provenance: LibraryProvenance) -> None:
55
55
  """Add a curated library candidate.
56
56
 
57
57
  Curated libraries default to inactive and need to be activated by user.
58
58
  """
59
- self.discover_library(provenance)
59
+ await self.discover_library(provenance)
60
60
 
61
61
  # Set curated library as inactive by default
62
62
  if provenance in self._discovered_libraries:
63
63
  entry = self._discovered_libraries[provenance]
64
64
  entry.active = False
65
65
 
66
- def add_user_candidate(self, provenance: LibraryProvenance) -> None:
66
+ async def add_user_candidate(self, provenance: LibraryProvenance) -> None:
67
67
  """Add a user-supplied library candidate.
68
68
 
69
69
  User libraries default to active.
70
70
  """
71
- self.discover_library(provenance)
71
+ await self.discover_library(provenance)
72
72
 
73
73
  # Set user library as active by default
74
74
  if provenance in self._discovered_libraries:
@@ -202,7 +202,7 @@ class LibraryDirectory:
202
202
 
203
203
  return blockers
204
204
 
205
- def _create_fsm_and_evaluate(self, provenance: LibraryProvenance) -> None:
205
+ async def _create_fsm_and_evaluate(self, provenance: LibraryProvenance) -> None:
206
206
  """Create FSM for provenance and run through evaluation phase.
207
207
 
208
208
  This method is called automatically when a library is discovered.
@@ -214,11 +214,11 @@ class LibraryDirectory:
214
214
  self._provenance_to_fsm[provenance] = fsm
215
215
 
216
216
  # Start the lifecycle and run through evaluation
217
- fsm.start_lifecycle()
217
+ await fsm.start_lifecycle()
218
218
 
219
219
  # Progress through inspection
220
220
  if fsm.can_begin_inspection():
221
- fsm.begin_inspection()
221
+ await fsm.begin_inspection()
222
222
  else:
223
223
  logger.error(
224
224
  "Cannot inspect library '%s' - inspection step cannot proceed",
@@ -228,7 +228,7 @@ class LibraryDirectory:
228
228
 
229
229
  # Progress through evaluation
230
230
  if fsm.can_begin_evaluation():
231
- fsm.begin_evaluation()
231
+ await fsm.begin_evaluation()
232
232
  else:
233
233
  logger.error(
234
234
  "Cannot evaluate library '%s' - evaluation step cannot proceed",
@@ -247,7 +247,7 @@ class LibraryDirectory:
247
247
 
248
248
  logger.debug("Completed FSM evaluation for library: %s", provenance.get_display_name())
249
249
 
250
- def install_library(self, provenance: LibraryProvenance) -> bool:
250
+ async def install_library(self, provenance: LibraryProvenance) -> bool:
251
251
  """Install a library by running its FSM through the installation phase.
252
252
 
253
253
  Returns True if installation was successful, False otherwise.
@@ -272,7 +272,7 @@ class LibraryDirectory:
272
272
 
273
273
  # Proceed with installation
274
274
  if fsm.can_begin_installation():
275
- fsm.begin_installation()
275
+ await fsm.begin_installation()
276
276
  logger.info("Installation completed for library: %s", provenance.get_display_name())
277
277
  return True
278
278
  logger.error(
@@ -281,7 +281,7 @@ class LibraryDirectory:
281
281
  )
282
282
  return False
283
283
 
284
- def load_library(self, provenance: LibraryProvenance) -> bool:
284
+ async def load_library(self, provenance: LibraryProvenance) -> bool:
285
285
  """Load a library by running its FSM through the loading phase.
286
286
 
287
287
  Returns True if loading was successful, False otherwise.
@@ -299,7 +299,7 @@ class LibraryDirectory:
299
299
  return False
300
300
 
301
301
  # Proceed with loading
302
- fsm.begin_loading()
302
+ await fsm.begin_loading()
303
303
 
304
304
  if not fsm.is_loaded():
305
305
  logger.error(
@@ -87,7 +87,7 @@ class CandidateState(State):
87
87
  """Initial state where we have a library candidate ready for processing."""
88
88
 
89
89
  @staticmethod
90
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
90
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
91
91
  logger.info("Library %s is now a candidate for processing", context.provenance.get_display_name())
92
92
  return None # Wait for explicit transition to InspectingState
93
93
 
@@ -106,7 +106,7 @@ class InspectingState(State):
106
106
  return {InspectedState}
107
107
 
108
108
  @staticmethod
109
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
109
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
110
110
  logger.info("Inspecting library %s", context.provenance.get_display_name())
111
111
 
112
112
  # Store inspection result directly
@@ -145,7 +145,7 @@ class InspectedState(State):
145
145
  return {EvaluatingState}
146
146
 
147
147
  @staticmethod
148
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
148
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
149
149
  if context.inspection_result and context.inspection_result.issues:
150
150
  logger.warning(
151
151
  "Library %s inspection completed with problems: %s",
@@ -167,7 +167,7 @@ class EvaluatingState(State):
167
167
  return {EvaluatedState}
168
168
 
169
169
  @staticmethod
170
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
170
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
171
171
  logger.info("Evaluating library %s", context.provenance.get_display_name())
172
172
 
173
173
  context.evaluation_result = context.provenance.evaluate(context)
@@ -185,7 +185,7 @@ class EvaluatedState(State):
185
185
  return {InstallingState}
186
186
 
187
187
  @staticmethod
188
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
188
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
189
189
  evaluation_issues = context.get_evaluation_issues()
190
190
  if evaluation_issues:
191
191
  logger.warning(
@@ -208,7 +208,7 @@ class InstallingState(State):
208
208
  return {InstalledState}
209
209
 
210
210
  @staticmethod
211
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
211
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
212
212
  logger.info("Installing library %s", context.provenance.get_display_name())
213
213
 
214
214
  # Check if user has disabled this library
@@ -219,7 +219,7 @@ class InstallingState(State):
219
219
  return InstalledState
220
220
 
221
221
  # Perform installation using delegation
222
- context.installation_result = context.provenance.install(context)
222
+ context.installation_result = await context.provenance.install(context)
223
223
 
224
224
  # Auto-transition to InstalledState
225
225
  return InstalledState
@@ -234,7 +234,7 @@ class InstalledState(State):
234
234
  return {LoadingState}
235
235
 
236
236
  @staticmethod
237
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
237
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
238
238
  installation_issues = context.get_installation_issues()
239
239
  if installation_issues:
240
240
  logger.warning(
@@ -257,7 +257,7 @@ class LoadingState(State):
257
257
  return {LoadedState}
258
258
 
259
259
  @staticmethod
260
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
260
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
261
261
  logger.info("Loading library %s", context.provenance.get_display_name())
262
262
 
263
263
  # Check if user has disabled this library
@@ -292,7 +292,7 @@ class LoadedState(State):
292
292
  return set() # Terminal state
293
293
 
294
294
  @staticmethod
295
- def on_enter(context: LibraryLifecycleContext) -> StateType | None:
295
+ async def on_enter(context: LibraryLifecycleContext) -> StateType | None:
296
296
  library_loaded_issues = context.get_library_loaded_issues()
297
297
  if library_loaded_issues:
298
298
  logger.warning(
@@ -313,31 +313,31 @@ class LibraryLifecycleFSM(FSM[LibraryLifecycleContext]):
313
313
  context = LibraryLifecycleContext(provenance=provenance)
314
314
  super().__init__(context)
315
315
 
316
- def start_lifecycle(self) -> None:
316
+ async def start_lifecycle(self) -> None:
317
317
  """Start the library lifecycle from CandidateState."""
318
318
  if self._current_state is not None:
319
319
  raise InvalidStateTransitionError(self._current_state, CandidateState, "Lifecycle has already been started")
320
- self.start(CandidateState)
320
+ await self.start(CandidateState)
321
321
 
322
- def begin_inspection(self) -> None:
322
+ async def begin_inspection(self) -> None:
323
323
  """Explicitly transition from Candidate to Inspecting."""
324
324
  self._validate_state_transition(InspectingState)
325
- self.transition_state(InspectingState)
325
+ await self.transition_state(InspectingState)
326
326
 
327
- def begin_evaluation(self) -> None:
327
+ async def begin_evaluation(self) -> None:
328
328
  """Explicitly transition from Inspected to Evaluating."""
329
329
  self._validate_state_transition(EvaluatingState)
330
- self.transition_state(EvaluatingState)
330
+ await self.transition_state(EvaluatingState)
331
331
 
332
- def begin_installation(self) -> None:
332
+ async def begin_installation(self) -> None:
333
333
  """Explicitly transition from Evaluated to Installing."""
334
334
  self._validate_state_transition(InstallingState)
335
- self.transition_state(InstallingState)
335
+ await self.transition_state(InstallingState)
336
336
 
337
- def begin_loading(self) -> None:
337
+ async def begin_loading(self) -> None:
338
338
  """Explicitly transition from Installed to Loading."""
339
339
  self._validate_state_transition(LoadingState)
340
- self.transition_state(LoadingState)
340
+ await self.transition_state(LoadingState)
341
341
 
342
342
  def get_context(self) -> LibraryLifecycleContext:
343
343
  """Get the current context."""
@@ -60,7 +60,7 @@ class LibraryProvenance(ABC):
60
60
  """
61
61
 
62
62
  @abstractmethod
63
- def install(self, context: LibraryLifecycleContext) -> InstallationResult:
63
+ async def install(self, context: LibraryLifecycleContext) -> InstallationResult:
64
64
  """Install this provenance.
65
65
 
66
66
  Args:
@@ -72,7 +72,7 @@ class LibraryProvenanceGitHub(LibraryProvenance):
72
72
  )
73
73
  return EvaluationResult(issues=issues)
74
74
 
75
- def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
75
+ async def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
76
76
  """Install this GitHub repository library."""
77
77
  issues = []
78
78
  issues.append(
@@ -31,6 +31,7 @@ from griptape_nodes.retained_mode.managers.library_lifecycle.data_models import
31
31
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_provenance.base import LibraryProvenance
32
32
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_status import LibraryStatus
33
33
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
34
+ from griptape_nodes.utils.async_utils import subprocess_run
34
35
 
35
36
  if TYPE_CHECKING:
36
37
  from griptape_nodes.retained_mode.managers.library_lifecycle.library_fsm import LibraryLifecycleContext
@@ -114,7 +115,7 @@ class LibraryProvenanceLocalFile(LibraryProvenance):
114
115
 
115
116
  return EvaluationResult(issues=issues)
116
117
 
117
- def install(self, context: LibraryLifecycleContext) -> InstallationResult:
118
+ async def install(self, context: LibraryLifecycleContext) -> InstallationResult:
118
119
  """Install this local file library."""
119
120
  problems = []
120
121
  venv_path = ""
@@ -150,7 +151,7 @@ class LibraryProvenanceLocalFile(LibraryProvenance):
150
151
  # Only install dependencies if conditions are met
151
152
  library_venv_python_path = None
152
153
  try:
153
- library_venv_python_path = library_manager._init_library_venv(venv_path)
154
+ library_venv_python_path = await library_manager._init_library_venv(venv_path)
154
155
  except RuntimeError as e:
155
156
  problems.append(
156
157
  LifecycleIssue(
@@ -187,7 +188,7 @@ class LibraryProvenanceLocalFile(LibraryProvenance):
187
188
  # Grab the python executable from the virtual environment so that we can pip install there
188
189
  logger.info("Installing dependencies for library '%s' with pip in venv at %s", library_data.name, venv_path)
189
190
  try:
190
- subprocess.run( # noqa: S603
191
+ await subprocess_run(
191
192
  [
192
193
  sys.executable,
193
194
  "-m",
@@ -61,7 +61,7 @@ class LibraryProvenancePackage(LibraryProvenance):
61
61
  )
62
62
  return EvaluationResult(issues=issues)
63
63
 
64
- def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
64
+ async def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
65
65
  """Install this package library."""
66
66
  issues = []
67
67
  issues.append(
@@ -110,7 +110,7 @@ class LibraryProvenanceSandbox(LibraryProvenance):
110
110
 
111
111
  return EvaluationResult(issues=issues)
112
112
 
113
- def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
113
+ async def install(self, context: LibraryLifecycleContext) -> InstallationResult: # noqa: ARG002
114
114
  """Install this sandbox library."""
115
115
  issues = []
116
116