griptape-nodes 0.40.0__py3-none-any.whl → 0.42.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 (49) hide show
  1. griptape_nodes/app/__init__.py +1 -5
  2. griptape_nodes/app/app.py +12 -9
  3. griptape_nodes/app/app_sessions.py +132 -36
  4. griptape_nodes/app/watch.py +3 -1
  5. griptape_nodes/drivers/storage/local_storage_driver.py +3 -2
  6. griptape_nodes/exe_types/flow.py +68 -368
  7. griptape_nodes/machines/control_flow.py +16 -13
  8. griptape_nodes/machines/node_resolution.py +16 -14
  9. griptape_nodes/node_library/workflow_registry.py +2 -2
  10. griptape_nodes/retained_mode/events/agent_events.py +70 -8
  11. griptape_nodes/retained_mode/events/app_events.py +132 -11
  12. griptape_nodes/retained_mode/events/arbitrary_python_events.py +23 -0
  13. griptape_nodes/retained_mode/events/base_events.py +7 -25
  14. griptape_nodes/retained_mode/events/config_events.py +87 -11
  15. griptape_nodes/retained_mode/events/connection_events.py +56 -5
  16. griptape_nodes/retained_mode/events/context_events.py +27 -4
  17. griptape_nodes/retained_mode/events/execution_events.py +99 -14
  18. griptape_nodes/retained_mode/events/flow_events.py +165 -7
  19. griptape_nodes/retained_mode/events/library_events.py +193 -15
  20. griptape_nodes/retained_mode/events/logger_events.py +11 -0
  21. griptape_nodes/retained_mode/events/node_events.py +243 -22
  22. griptape_nodes/retained_mode/events/object_events.py +40 -4
  23. griptape_nodes/retained_mode/events/os_events.py +13 -2
  24. griptape_nodes/retained_mode/events/parameter_events.py +212 -8
  25. griptape_nodes/retained_mode/events/secrets_events.py +59 -7
  26. griptape_nodes/retained_mode/events/static_file_events.py +57 -4
  27. griptape_nodes/retained_mode/events/validation_events.py +39 -4
  28. griptape_nodes/retained_mode/events/workflow_events.py +188 -17
  29. griptape_nodes/retained_mode/griptape_nodes.py +46 -323
  30. griptape_nodes/retained_mode/managers/agent_manager.py +1 -1
  31. griptape_nodes/retained_mode/managers/engine_identity_manager.py +146 -0
  32. griptape_nodes/retained_mode/managers/event_manager.py +14 -2
  33. griptape_nodes/retained_mode/managers/flow_manager.py +749 -64
  34. griptape_nodes/retained_mode/managers/library_manager.py +112 -2
  35. griptape_nodes/retained_mode/managers/node_manager.py +35 -32
  36. griptape_nodes/retained_mode/managers/object_manager.py +11 -3
  37. griptape_nodes/retained_mode/managers/os_manager.py +70 -1
  38. griptape_nodes/retained_mode/managers/secrets_manager.py +4 -0
  39. griptape_nodes/retained_mode/managers/session_manager.py +328 -0
  40. griptape_nodes/retained_mode/managers/settings.py +7 -0
  41. griptape_nodes/retained_mode/managers/workflow_manager.py +523 -454
  42. griptape_nodes/retained_mode/retained_mode.py +44 -0
  43. griptape_nodes/retained_mode/utils/engine_identity.py +141 -27
  44. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/METADATA +2 -2
  45. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/RECORD +48 -47
  46. griptape_nodes/retained_mode/utils/session_persistence.py +0 -105
  47. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/WHEEL +0 -0
  48. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/entry_points.txt +0 -0
  49. {griptape_nodes-0.40.0.dist-info → griptape_nodes-0.42.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ from queue import Queue
4
5
  from typing import TYPE_CHECKING, cast
5
6
 
7
+ from griptape.events import EventBus
8
+
9
+ from griptape_nodes.exe_types.connections import Connections
6
10
  from griptape_nodes.exe_types.core_types import (
11
+ Parameter,
7
12
  ParameterContainer,
8
13
  ParameterMode,
14
+ ParameterTypeBuiltin,
9
15
  )
10
16
  from griptape_nodes.exe_types.flow import ControlFlow
11
- from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
17
+ from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState, StartLoopNode, StartNode
18
+ from griptape_nodes.machines.control_flow import CompleteState, ControlFlowMachine
12
19
  from griptape_nodes.retained_mode.events.base_events import (
20
+ ExecutionEvent,
21
+ ExecutionGriptapeNodeEvent,
13
22
  FlushParameterChangesRequest,
14
23
  FlushParameterChangesResultSuccess,
15
24
  )
@@ -28,6 +37,7 @@ from griptape_nodes.retained_mode.events.execution_events import (
28
37
  ContinueExecutionStepRequest,
29
38
  ContinueExecutionStepResultFailure,
30
39
  ContinueExecutionStepResultSuccess,
40
+ ControlFlowCancelledEvent,
31
41
  GetFlowStateRequest,
32
42
  GetFlowStateResultFailure,
33
43
  GetFlowStateResultSuccess,
@@ -57,6 +67,12 @@ from griptape_nodes.retained_mode.events.flow_events import (
57
67
  DeserializeFlowFromCommandsRequest,
58
68
  DeserializeFlowFromCommandsResultFailure,
59
69
  DeserializeFlowFromCommandsResultSuccess,
70
+ GetFlowDetailsRequest,
71
+ GetFlowDetailsResultFailure,
72
+ GetFlowDetailsResultSuccess,
73
+ GetFlowMetadataRequest,
74
+ GetFlowMetadataResultFailure,
75
+ GetFlowMetadataResultSuccess,
60
76
  GetTopLevelFlowRequest,
61
77
  GetTopLevelFlowResultSuccess,
62
78
  ListFlowsInCurrentContextRequest,
@@ -72,6 +88,9 @@ from griptape_nodes.retained_mode.events.flow_events import (
72
88
  SerializeFlowToCommandsRequest,
73
89
  SerializeFlowToCommandsResultFailure,
74
90
  SerializeFlowToCommandsResultSuccess,
91
+ SetFlowMetadataRequest,
92
+ SetFlowMetadataResultFailure,
93
+ SetFlowMetadataResultSuccess,
75
94
  )
76
95
  from griptape_nodes.retained_mode.events.node_events import (
77
96
  DeleteNodeRequest,
@@ -89,6 +108,10 @@ from griptape_nodes.retained_mode.events.validation_events import (
89
108
  ValidateFlowDependenciesResultFailure,
90
109
  ValidateFlowDependenciesResultSuccess,
91
110
  )
111
+ from griptape_nodes.retained_mode.events.workflow_events import (
112
+ ImportWorkflowAsReferencedSubFlowRequest,
113
+ ImportWorkflowAsReferencedSubFlowResultSuccess,
114
+ )
92
115
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
93
116
 
94
117
  if TYPE_CHECKING:
@@ -100,6 +123,13 @@ logger = logging.getLogger("griptape_nodes")
100
123
 
101
124
  class FlowManager:
102
125
  _name_to_parent_name: dict[str, str | None]
126
+ _flow_to_referenced_workflow_name: dict[ControlFlow, str]
127
+ _connections: Connections
128
+
129
+ # Global execution state (moved from individual ControlFlows)
130
+ _global_flow_queue: Queue[BaseNode]
131
+ _global_control_flow_machine: ControlFlowMachine | None
132
+ _global_single_node_resolution: bool
103
133
 
104
134
  def __init__(self, event_manager: EventManager) -> None:
105
135
  event_manager.assign_manager_to_request_type(CreateFlowRequest, self.on_create_flow_request)
@@ -126,6 +156,9 @@ class FlowManager:
126
156
  ValidateFlowDependenciesRequest, self.on_validate_flow_dependencies_request
127
157
  )
128
158
  event_manager.assign_manager_to_request_type(GetTopLevelFlowRequest, self.on_get_top_level_flow_request)
159
+ event_manager.assign_manager_to_request_type(GetFlowDetailsRequest, self.on_get_flow_details_request)
160
+ event_manager.assign_manager_to_request_type(GetFlowMetadataRequest, self.on_get_flow_metadata_request)
161
+ event_manager.assign_manager_to_request_type(SetFlowMetadataRequest, self.on_set_flow_metadata_request)
129
162
  event_manager.assign_manager_to_request_type(SerializeFlowToCommandsRequest, self.on_serialize_flow_to_commands)
130
163
  event_manager.assign_manager_to_request_type(
131
164
  DeserializeFlowFromCommandsRequest, self.on_deserialize_flow_from_commands
@@ -133,6 +166,53 @@ class FlowManager:
133
166
  event_manager.assign_manager_to_request_type(FlushParameterChangesRequest, self.on_flush_request)
134
167
 
135
168
  self._name_to_parent_name = {}
169
+ self._flow_to_referenced_workflow_name = {}
170
+ self._connections = Connections()
171
+
172
+ # Initialize global execution state
173
+ self._global_flow_queue = Queue[BaseNode]()
174
+ self._global_control_flow_machine = None # Will be initialized when first flow starts
175
+ self._global_single_node_resolution = False
176
+
177
+ def get_connections(self) -> Connections:
178
+ """Get the connections instance."""
179
+ return self._connections
180
+
181
+ def _has_connection(
182
+ self,
183
+ source_node: BaseNode,
184
+ source_parameter: Parameter,
185
+ target_node: BaseNode,
186
+ target_parameter: Parameter,
187
+ ) -> bool:
188
+ """Check if a connection exists."""
189
+ connected_outputs = self.get_connected_output_parameters(source_node, source_parameter)
190
+ for connected_node, connected_param in connected_outputs:
191
+ if connected_node is target_node and connected_param is target_parameter:
192
+ return True
193
+ return False
194
+
195
+ def get_connected_output_parameters(self, node: BaseNode, param: Parameter) -> list[tuple[BaseNode, Parameter]]:
196
+ """Get connected output parameters."""
197
+ connections = []
198
+ if node.name in self._connections.outgoing_index:
199
+ outgoing_params = self._connections.outgoing_index[node.name]
200
+ if param.name in outgoing_params:
201
+ for connection_id in outgoing_params[param.name]:
202
+ connection = self._connections.connections[connection_id]
203
+ connections.append((connection.target_node, connection.target_parameter))
204
+ return connections
205
+
206
+ def _get_connections_for_flow(self, flow: ControlFlow) -> list:
207
+ """Get connections where both nodes are in the specified flow."""
208
+ flow_connections = []
209
+ for connection in self._connections.connections.values():
210
+ source_in_flow = connection.source_node.name in flow.nodes
211
+ target_in_flow = connection.target_node.name in flow.nodes
212
+ # Only include connection if BOTH nodes are in this flow (for serialization)
213
+ if source_in_flow and target_in_flow:
214
+ flow_connections.append(connection)
215
+ return flow_connections
136
216
 
137
217
  def get_parent_flow(self, flow_name: str) -> str | None:
138
218
  if flow_name in self._name_to_parent_name:
@@ -140,6 +220,22 @@ class FlowManager:
140
220
  msg = f"Flow with name {flow_name} doesn't exist"
141
221
  raise ValueError(msg)
142
222
 
223
+ def is_referenced_workflow(self, flow: ControlFlow) -> bool:
224
+ """Check if this flow was created by importing a referenced workflow.
225
+
226
+ Returns True if this flow originated from a workflow import operation,
227
+ False if it was created standalone.
228
+ """
229
+ return flow in self._flow_to_referenced_workflow_name
230
+
231
+ def get_referenced_workflow_name(self, flow: ControlFlow) -> str | None:
232
+ """Get the name of the referenced workflow, if any.
233
+
234
+ Returns the workflow name that was imported to create this flow,
235
+ or None if this flow was created standalone.
236
+ """
237
+ return self._flow_to_referenced_workflow_name.get(flow)
238
+
143
239
  def on_get_top_level_flow_request(self, request: GetTopLevelFlowRequest) -> ResultPayload: # noqa: ARG002 (the request has to be assigned to the method)
144
240
  for flow_name, parent in self._name_to_parent_name.items():
145
241
  if parent is None:
@@ -148,6 +244,103 @@ class FlowManager:
148
244
  logger.debug(msg)
149
245
  return GetTopLevelFlowResultSuccess(flow_name=None)
150
246
 
247
+ def on_get_flow_details_request(self, request: GetFlowDetailsRequest) -> ResultPayload:
248
+ flow_name = request.flow_name
249
+ flow = None
250
+
251
+ if flow_name is None:
252
+ # We want to get details for whatever is at the top of the Current Context.
253
+ if not GriptapeNodes.ContextManager().has_current_flow():
254
+ details = "Attempted to get Flow details from the Current Context. Failed because the Current Context was empty."
255
+ logger.error(details)
256
+ return GetFlowDetailsResultFailure()
257
+ flow = GriptapeNodes.ContextManager().get_current_flow()
258
+ flow_name = flow.name
259
+ else:
260
+ flow = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(flow_name, ControlFlow)
261
+ if flow is None:
262
+ details = (
263
+ f"Attempted to get Flow details for '{flow_name}'. Failed because no Flow with that name exists."
264
+ )
265
+ logger.error(details)
266
+ return GetFlowDetailsResultFailure()
267
+
268
+ try:
269
+ parent_flow_name = self.get_parent_flow(flow_name)
270
+ except ValueError:
271
+ details = f"Attempted to get Flow details for '{flow_name}'. Failed because Flow does not exist in parent mapping."
272
+ logger.error(details)
273
+ return GetFlowDetailsResultFailure()
274
+
275
+ referenced_workflow_name = None
276
+ if self.is_referenced_workflow(flow):
277
+ referenced_workflow_name = self.get_referenced_workflow_name(flow)
278
+
279
+ details = f"Successfully retrieved Flow details for '{flow_name}'."
280
+ logger.debug(details)
281
+ return GetFlowDetailsResultSuccess(
282
+ referenced_workflow_name=referenced_workflow_name,
283
+ parent_flow_name=parent_flow_name,
284
+ )
285
+
286
+ def on_get_flow_metadata_request(self, request: GetFlowMetadataRequest) -> ResultPayload:
287
+ flow_name = request.flow_name
288
+ flow = None
289
+ if flow_name is None:
290
+ # Get from the current context.
291
+ if not GriptapeNodes.ContextManager().has_current_flow():
292
+ details = "Attempted to get metadata for a Flow from the Current Context. Failed because the Current Context is empty."
293
+ logger.error(details)
294
+ return GetFlowMetadataResultFailure()
295
+
296
+ flow = GriptapeNodes.ContextManager().get_current_flow()
297
+ flow_name = flow.name
298
+
299
+ # Does this flow exist?
300
+ if flow is None:
301
+ obj_mgr = GriptapeNodes.ObjectManager()
302
+ flow = obj_mgr.attempt_get_object_by_name_as_type(flow_name, ControlFlow)
303
+ if flow is None:
304
+ details = f"Attempted to get metadata for a Flow '{flow_name}', but no such Flow was found."
305
+ logger.error(details)
306
+ return GetFlowMetadataResultFailure()
307
+
308
+ metadata = flow.metadata
309
+ details = f"Successfully retrieved metadata for a Flow '{flow_name}'."
310
+ logger.debug(details)
311
+
312
+ return GetFlowMetadataResultSuccess(metadata=metadata)
313
+
314
+ def on_set_flow_metadata_request(self, request: SetFlowMetadataRequest) -> ResultPayload:
315
+ flow_name = request.flow_name
316
+ flow = None
317
+ if flow_name is None:
318
+ # Get from the current context.
319
+ if not GriptapeNodes.ContextManager().has_current_flow():
320
+ details = "Attempted to set metadata for a Flow from the Current Context. Failed because the Current Context is empty."
321
+ logger.error(details)
322
+ return SetFlowMetadataResultFailure()
323
+
324
+ flow = GriptapeNodes.ContextManager().get_current_flow()
325
+ flow_name = flow.name
326
+
327
+ # Does this flow exist?
328
+ if flow is None:
329
+ obj_mgr = GriptapeNodes.ObjectManager()
330
+ flow = obj_mgr.attempt_get_object_by_name_as_type(flow_name, ControlFlow)
331
+ if flow is None:
332
+ details = f"Attempted to set metadata for a Flow '{flow_name}', but no such Flow was found."
333
+ logger.error(details)
334
+ return SetFlowMetadataResultFailure()
335
+
336
+ # We can't completely overwrite metadata.
337
+ for key, value in request.metadata.items():
338
+ flow.metadata[key] = value
339
+ details = f"Successfully set metadata for a Flow '{flow_name}'."
340
+ logger.debug(details)
341
+
342
+ return SetFlowMetadataResultSuccess()
343
+
151
344
  def does_canvas_exist(self) -> bool:
152
345
  """Determines if there is already an existing flow with no parent flow.Returns True if there is an existing flow with no parent flow.Return False if there is no existing flow with no parent flow."""
153
346
  return any([parent is None for parent in self._name_to_parent_name.values()]) # noqa: C419
@@ -198,10 +391,19 @@ class FlowManager:
198
391
  final_flow_name = GriptapeNodes.ObjectManager().generate_name_for_object(
199
392
  type_name="ControlFlow", requested_name=request.flow_name
200
393
  )
201
- flow = ControlFlow(name=final_flow_name)
394
+ # Check if we're creating this flow within a referenced workflow context
395
+ # This will inform the engine to maintain a reference to the workflow
396
+ # when serializing it. It may inform the editor to render it differently.
397
+ workflow_manager = GriptapeNodes.WorkflowManager()
398
+ flow = ControlFlow(name=final_flow_name, metadata=request.metadata)
202
399
  GriptapeNodes.ObjectManager().add_object_by_name(name=final_flow_name, obj=flow)
203
400
  self._name_to_parent_name[final_flow_name] = parent_name
204
401
 
402
+ # Track referenced workflow if this flow was created within a referenced workflow context
403
+ if workflow_manager.has_current_referenced_workflow():
404
+ referenced_workflow_name = workflow_manager.get_current_referenced_workflow()
405
+ self._flow_to_referenced_workflow_name[flow] = referenced_workflow_name
406
+
205
407
  # See if we need to push it as the current context.
206
408
  if request.set_as_new_context:
207
409
  GriptapeNodes.ContextManager().push_flow(flow)
@@ -242,7 +444,7 @@ class FlowManager:
242
444
  logger.error(details)
243
445
  result = DeleteFlowResultFailure()
244
446
  return result
245
- if flow.check_for_existing_running_flow():
447
+ if self.check_for_existing_running_flow():
246
448
  result = GriptapeNodes.handle_request(CancelFlowRequest(flow_name=flow.name))
247
449
  if not result.succeeded():
248
450
  details = f"Attempted to delete flow '{flow_name}'. Failed because running flow could not cancel."
@@ -306,6 +508,10 @@ class FlowManager:
306
508
  obj_mgr.del_obj_by_name(flow.name)
307
509
  del self._name_to_parent_name[flow.name]
308
510
 
511
+ # Clean up referenced workflow tracking
512
+ if flow in self._flow_to_referenced_workflow_name:
513
+ del self._flow_to_referenced_workflow_name[flow]
514
+
309
515
  details = f"Successfully deleted Flow '{flow_name}'."
310
516
  logger.debug(details)
311
517
  result = DeleteFlowResultSuccess()
@@ -313,6 +519,10 @@ class FlowManager:
313
519
 
314
520
  def on_get_is_flow_running_request(self, request: GetIsFlowRunningRequest) -> ResultPayload:
315
521
  obj_mgr = GriptapeNodes.ObjectManager()
522
+ if request.flow_name is None:
523
+ details = "Attempted to get Flow, but no flow name was provided."
524
+ logger.error(details)
525
+ return GetIsFlowRunningResultFailure()
316
526
  flow = obj_mgr.attempt_get_object_by_name_as_type(request.flow_name, ControlFlow)
317
527
  if flow is None:
318
528
  details = f"Attempted to get Flow '{request.flow_name}', but no Flow with that name could be found."
@@ -320,7 +530,7 @@ class FlowManager:
320
530
  result = GetIsFlowRunningResultFailure()
321
531
  return result
322
532
  try:
323
- is_running = flow.check_for_existing_running_flow()
533
+ is_running = self.check_for_existing_running_flow()
324
534
  except Exception:
325
535
  details = f"Error while trying to get status of '{request.flow_name}'."
326
536
  logger.error(details)
@@ -452,10 +662,9 @@ class FlowManager:
452
662
  # The two nodes exist.
453
663
  # Get the parent flows.
454
664
  source_flow_name = None
455
- source_flow = None
456
665
  try:
457
666
  source_flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(source_node_name)
458
- source_flow = self.get_flow_by_name(flow_name=source_flow_name)
667
+ self.get_flow_by_name(flow_name=source_flow_name)
459
668
  except KeyError as err:
460
669
  details = f'Connection "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}" failed: {err}.'
461
670
  logger.error(details)
@@ -470,11 +679,7 @@ class FlowManager:
470
679
  logger.error(details)
471
680
  return CreateConnectionResultFailure()
472
681
 
473
- # CURRENT RESTRICTION: Now vet the parents are in the same Flow (yes this sucks)
474
- if target_flow_name != source_flow_name:
475
- details = f'Connection "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}" failed: Different flows.'
476
- logger.error(details)
477
- return CreateConnectionResultFailure()
682
+ # Cross-flow connections are now supported via global connection storage
478
683
 
479
684
  # Now validate the parameters.
480
685
  source_param = source_node.get_parameter_by_name(request.source_parameter_name)
@@ -543,7 +748,7 @@ class FlowManager:
543
748
 
544
749
  # Some scenarios restrict when we can have more than one connection. See if we're in such a scenario and replace the
545
750
  # existing connection instead of adding a new one.
546
- connection_mgr = source_flow.connections
751
+ connection_mgr = self._connections
547
752
  # Try the OUTGOING restricted scenario first.
548
753
  restricted_scenario_connection = connection_mgr.get_existing_connection_for_restricted_scenario(
549
754
  node=source_node, parameter=source_param, is_source=True
@@ -577,7 +782,7 @@ class FlowManager:
577
782
  logger.debug(details)
578
783
  try:
579
784
  # Actually create the Connection.
580
- source_flow.add_connection(
785
+ self._connections.add_connection(
581
786
  source_node=source_node,
582
787
  source_parameter=source_param,
583
788
  target_node=target_node,
@@ -699,10 +904,9 @@ class FlowManager:
699
904
  # The two nodes exist.
700
905
  # Get the parent flows.
701
906
  source_flow_name = None
702
- source_flow = None
703
907
  try:
704
908
  source_flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(source_node_name)
705
- source_flow = self.get_flow_by_name(flow_name=source_flow_name)
909
+ self.get_flow_by_name(flow_name=source_flow_name)
706
910
  except KeyError as err:
707
911
  details = f'Connection not deleted "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}". Error: {err}'
708
912
  logger.error(details)
@@ -719,12 +923,7 @@ class FlowManager:
719
923
 
720
924
  return DeleteConnectionResultFailure()
721
925
 
722
- # CURRENT RESTRICTION: Now vet the parents are in the same Flow (yes this sucks)
723
- if target_flow_name != source_flow_name:
724
- details = f'Connection not deleted "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}". They are in different Flows (TEMPORARY RESTRICTION).'
725
- logger.error(details)
726
-
727
- return DeleteConnectionResultFailure()
926
+ # Cross-flow connections are now supported via global connection storage
728
927
 
729
928
  # Now validate the parameters.
730
929
  source_param = source_node.get_parameter_by_name(request.source_parameter_name)
@@ -742,7 +941,7 @@ class FlowManager:
742
941
  return DeleteConnectionResultFailure()
743
942
 
744
943
  # Vet that a Connection actually exists between them already.
745
- if not source_flow.has_connection(
944
+ if not self._has_connection(
746
945
  source_node=source_node,
747
946
  source_parameter=source_param,
748
947
  target_node=target_node,
@@ -754,11 +953,11 @@ class FlowManager:
754
953
  return DeleteConnectionResultFailure()
755
954
 
756
955
  # Remove the connection.
757
- if not source_flow.remove_connection(
758
- source_node=source_node,
759
- source_parameter=source_param,
760
- target_node=target_node,
761
- target_parameter=target_param,
956
+ if not self._connections.remove_connection(
957
+ source_node=source_node.name,
958
+ source_parameter=source_param.name,
959
+ target_node=target_node.name,
960
+ target_parameter=target_param.name,
762
961
  ):
763
962
  details = f'Connection not deleted "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}". Unknown failure.'
764
963
  logger.error(details)
@@ -773,7 +972,7 @@ class FlowManager:
773
972
  target_node.remove_parameter_value(target_param.name)
774
973
  # It removed it accurately
775
974
  # Unresolve future nodes that depended on that value
776
- source_flow.connections.unresolve_future_nodes(target_node)
975
+ self._connections.unresolve_future_nodes(target_node)
777
976
  target_node.make_node_unresolved(
778
977
  current_states_to_trigger_change_event=set(
779
978
  {NodeResolutionState.RESOLVED, NodeResolutionState.RESOLVING}
@@ -816,7 +1015,7 @@ class FlowManager:
816
1015
  logger.error(details)
817
1016
  return StartFlowResultFailure(validation_exceptions=[err])
818
1017
  # Check to see if the flow is already running.
819
- if flow.check_for_existing_running_flow():
1018
+ if self.check_for_existing_running_flow():
820
1019
  details = "Cannot start flow. Flow is already running."
821
1020
  logger.error(details)
822
1021
  return StartFlowResultFailure(validation_exceptions=[])
@@ -829,7 +1028,7 @@ class FlowManager:
829
1028
  logger.error(details)
830
1029
  return StartFlowResultFailure(validation_exceptions=[])
831
1030
  # lets get the first control node in the flow!
832
- start_node = flow.get_start_node_from_node(flow_node)
1031
+ start_node = self.get_start_node_from_node(flow, flow_node)
833
1032
  # if the start is not the node provided, set a breakpoint at the stop (we're running up until there)
834
1033
  if not start_node:
835
1034
  details = f"Start node for node with name {flow_node_name} does not exist"
@@ -840,7 +1039,7 @@ class FlowManager:
840
1039
  else:
841
1040
  # we wont hit this if we dont have a request id, our requests always have nodes
842
1041
  # If there is a request, reinitialize the queue
843
- flow.get_start_node_queue() # initialize the start flow queue!
1042
+ self.get_start_node_queue() # initialize the start flow queue!
844
1043
  start_node = None
845
1044
  # Run Validation before starting a flow
846
1045
  result = self.on_validate_flow_dependencies_request(
@@ -866,7 +1065,7 @@ class FlowManager:
866
1065
  return StartFlowResultFailure(validation_exceptions=[e])
867
1066
  # By now, it has been validated with no exceptions.
868
1067
  try:
869
- flow.start_flow(start_node, debug_mode)
1068
+ self.start_flow(flow, start_node, debug_mode)
870
1069
  except Exception as e:
871
1070
  details = f"Failed to kick off flow with name {flow_name}. Exception occurred: {e} "
872
1071
  logger.error(details)
@@ -890,7 +1089,7 @@ class FlowManager:
890
1089
  logger.error(details)
891
1090
  return GetFlowStateResultFailure()
892
1091
  try:
893
- control_node, resolving_node = flow.flow_state()
1092
+ control_node, resolving_node = self.flow_state(flow)
894
1093
  except Exception as e:
895
1094
  details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
896
1095
  logger.exception(details)
@@ -907,14 +1106,14 @@ class FlowManager:
907
1106
 
908
1107
  return CancelFlowResultFailure()
909
1108
  try:
910
- flow = self.get_flow_by_name(flow_name)
1109
+ self.get_flow_by_name(flow_name)
911
1110
  except KeyError as err:
912
1111
  details = f"Could not cancel flow execution. Error: {err}"
913
1112
  logger.error(details)
914
1113
 
915
1114
  return CancelFlowResultFailure()
916
1115
  try:
917
- flow.cancel_flow_run()
1116
+ self.cancel_flow_run()
918
1117
  except Exception as e:
919
1118
  details = f"Could not cancel flow execution. Exception: {e}"
920
1119
  logger.error(details)
@@ -933,14 +1132,15 @@ class FlowManager:
933
1132
 
934
1133
  return SingleNodeStepResultFailure(validation_exceptions=[])
935
1134
  try:
936
- flow = self.get_flow_by_name(flow_name)
1135
+ self.get_flow_by_name(flow_name)
937
1136
  except KeyError as err:
938
1137
  details = f"Could not advance to the next step of a running workflow. No flow with name {flow_name} exists. Error: {err}"
939
1138
  logger.error(details)
940
1139
 
941
1140
  return SingleNodeStepResultFailure(validation_exceptions=[err])
942
1141
  try:
943
- flow.single_node_step()
1142
+ flow = self.get_flow_by_name(flow_name)
1143
+ self.single_node_step(flow)
944
1144
  except Exception as e:
945
1145
  details = f"Could not advance to the next step of a running workflow. Exception: {e}"
946
1146
  logger.error(details)
@@ -968,12 +1168,12 @@ class FlowManager:
968
1168
  return SingleExecutionStepResultFailure()
969
1169
  change_debug_mode = request.request_id is not None
970
1170
  try:
971
- flow.single_execution_step(change_debug_mode)
1171
+ self.single_execution_step(flow, change_debug_mode)
972
1172
  except Exception as e:
973
1173
  # We REALLY don't want to fail here, else we'll take the whole engine down
974
1174
  try:
975
- if flow.check_for_existing_running_flow():
976
- flow.cancel_flow_run()
1175
+ if self.check_for_existing_running_flow():
1176
+ self.cancel_flow_run()
977
1177
  except Exception as e_inner:
978
1178
  details = f"Could not cancel flow execution. Exception: {e_inner}"
979
1179
  logger.error(details)
@@ -1001,7 +1201,7 @@ class FlowManager:
1001
1201
 
1002
1202
  return ContinueExecutionStepResultFailure()
1003
1203
  try:
1004
- flow.continue_executing()
1204
+ self.continue_executing(flow)
1005
1205
  except Exception as e:
1006
1206
  details = f"Failed to continue execution step. An exception occurred: {e}."
1007
1207
  logger.error(details)
@@ -1023,7 +1223,7 @@ class FlowManager:
1023
1223
  logger.error(details)
1024
1224
  return UnresolveFlowResultFailure()
1025
1225
  try:
1026
- flow.unresolve_whole_flow()
1226
+ self.unresolve_whole_flow(flow)
1027
1227
  except Exception as e:
1028
1228
  details = f"Failed to unresolve flow. An exception occurred: {e}."
1029
1229
  logger.error(details)
@@ -1108,6 +1308,9 @@ class FlowManager:
1108
1308
  # Track all node libraries that were in use by these Nodes
1109
1309
  node_libraries_in_use = set()
1110
1310
 
1311
+ # Track all referenced workflows used by this flow and its sub-flows
1312
+ referenced_workflows_in_use = set()
1313
+
1111
1314
  # Track all parameter values that were in use by these Nodes (maps UUID to Parameter value)
1112
1315
  unique_parameter_uuid_to_values = {}
1113
1316
  # And track how values map into that map.
@@ -1116,7 +1319,20 @@ class FlowManager:
1116
1319
  with GriptapeNodes.ContextManager().flow(flow):
1117
1320
  # The base flow creation, if desired.
1118
1321
  if request.include_create_flow_command:
1119
- create_flow_request = CreateFlowRequest(parent_flow_name=None)
1322
+ # Check if this flow is a referenced workflow
1323
+ if self.is_referenced_workflow(flow):
1324
+ referenced_workflow_name = self.get_referenced_workflow_name(flow)
1325
+ create_flow_request = ImportWorkflowAsReferencedSubFlowRequest(
1326
+ workflow_name=referenced_workflow_name, # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1327
+ imported_flow_metadata=flow.metadata,
1328
+ )
1329
+ referenced_workflows_in_use.add(referenced_workflow_name) # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1330
+ else:
1331
+ # Always set set_as_new_context=False during serialization - let the workflow manager
1332
+ # that loads this serialized flow decide whether to push it to context or not
1333
+ create_flow_request = CreateFlowRequest(
1334
+ parent_flow_name=None, set_as_new_context=False, metadata=flow.metadata
1335
+ )
1120
1336
  else:
1121
1337
  create_flow_request = None
1122
1338
 
@@ -1169,7 +1385,7 @@ class FlowManager:
1169
1385
  # when we're restored.
1170
1386
  # Create all of the connections
1171
1387
  create_connection_commands = []
1172
- for connection in flow.connections.connections.values():
1388
+ for connection in self._get_connections_for_flow(flow):
1173
1389
  source_node_uuid = node_name_to_uuid[connection.source_node.name]
1174
1390
  target_node_uuid = node_name_to_uuid[connection.target_node.name]
1175
1391
  create_connection_command = SerializedFlowCommands.IndirectConnectionSerialization(
@@ -1197,27 +1413,57 @@ class FlowManager:
1197
1413
  details = f"Attempted to serialize Flow '{flow_name}', but no Flow with that name could be found."
1198
1414
  logger.error(details)
1199
1415
  return SerializeFlowToCommandsResultFailure()
1200
- with GriptapeNodes.ContextManager().flow(flow=flow):
1201
- child_flow_request = SerializeFlowToCommandsRequest()
1202
- child_flow_result = GriptapeNodes().handle_request(child_flow_request)
1203
- if not isinstance(child_flow_result, SerializeFlowToCommandsResultSuccess):
1204
- details = f"Attempted to serialize parent flow '{flow_name}'. Failed while serializing child flow '{child_flow}'."
1205
- logger.error(details)
1206
- return SerializeFlowToCommandsResultFailure()
1207
- serialized_flow = child_flow_result.serialized_flow_commands
1416
+
1417
+ # Check if this is a referenced workflow
1418
+ if self.is_referenced_workflow(flow):
1419
+ # For referenced workflows, create a minimal SerializedFlowCommands with just the import command
1420
+ referenced_workflow_name = self.get_referenced_workflow_name(flow)
1421
+ import_command = ImportWorkflowAsReferencedSubFlowRequest(
1422
+ workflow_name=referenced_workflow_name, # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1423
+ imported_flow_metadata=flow.metadata,
1424
+ )
1425
+
1426
+ serialized_flow = SerializedFlowCommands(
1427
+ node_libraries_used=set(),
1428
+ flow_initialization_command=import_command,
1429
+ serialized_node_commands=[],
1430
+ serialized_connections=[],
1431
+ unique_parameter_uuid_to_values={},
1432
+ set_parameter_value_commands={},
1433
+ sub_flows_commands=[],
1434
+ referenced_workflows={referenced_workflow_name}, # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1435
+ )
1208
1436
  sub_flow_commands.append(serialized_flow)
1209
1437
 
1210
- # Merge in all child flow library details.
1211
- node_libraries_in_use.union(serialized_flow.node_libraries_used)
1438
+ # Add this referenced workflow to our accumulation
1439
+ referenced_workflows_in_use.add(referenced_workflow_name) # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1440
+ else:
1441
+ # For standalone sub-flows, use the existing recursive serialization
1442
+ with GriptapeNodes.ContextManager().flow(flow=flow):
1443
+ child_flow_request = SerializeFlowToCommandsRequest()
1444
+ child_flow_result = GriptapeNodes().handle_request(child_flow_request)
1445
+ if not isinstance(child_flow_result, SerializeFlowToCommandsResultSuccess):
1446
+ details = f"Attempted to serialize parent flow '{flow_name}'. Failed while serializing child flow '{child_flow}'."
1447
+ logger.error(details)
1448
+ return SerializeFlowToCommandsResultFailure()
1449
+ serialized_flow = child_flow_result.serialized_flow_commands
1450
+ sub_flow_commands.append(serialized_flow)
1451
+
1452
+ # Merge in all child flow library details.
1453
+ node_libraries_in_use.union(serialized_flow.node_libraries_used)
1454
+
1455
+ # Merge in all child flow referenced workflows.
1456
+ referenced_workflows_in_use.union(serialized_flow.referenced_workflows)
1212
1457
 
1213
1458
  serialized_flow = SerializedFlowCommands(
1214
- create_flow_command=create_flow_request,
1459
+ flow_initialization_command=create_flow_request,
1215
1460
  serialized_node_commands=serialized_node_commands,
1216
1461
  serialized_connections=create_connection_commands,
1217
1462
  unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1218
1463
  set_parameter_value_commands=set_parameter_value_commands_per_node,
1219
1464
  sub_flows_commands=sub_flow_commands,
1220
1465
  node_libraries_used=node_libraries_in_use,
1466
+ referenced_workflows=referenced_workflows_in_use,
1221
1467
  )
1222
1468
  details = f"Successfully serialized Flow '{flow_name}' into commands."
1223
1469
  result = SerializeFlowToCommandsResultSuccess(serialized_flow_commands=serialized_flow)
@@ -1225,7 +1471,7 @@ class FlowManager:
1225
1471
 
1226
1472
  def on_deserialize_flow_from_commands(self, request: DeserializeFlowFromCommandsRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915 (I am big and complicated and have a lot of negative edge-cases)
1227
1473
  # Do we want to create a NEW Flow to deserialize into, or use the one in the Current Context?
1228
- if request.serialized_flow_commands.create_flow_command is None:
1474
+ if request.serialized_flow_commands.flow_initialization_command is None:
1229
1475
  if GriptapeNodes.ContextManager().has_current_flow():
1230
1476
  flow = GriptapeNodes.ContextManager().get_current_flow()
1231
1477
  flow_name = flow.name
@@ -1235,18 +1481,32 @@ class FlowManager:
1235
1481
  return DeserializeFlowFromCommandsResultFailure()
1236
1482
  else:
1237
1483
  # Issue the creation command first.
1238
- create_flow_request = request.serialized_flow_commands.create_flow_command
1239
- create_flow_result = GriptapeNodes.handle_request(create_flow_request)
1240
- if not isinstance(create_flow_result, CreateFlowResultSuccess):
1241
- details = f"Attempted to deserialize a serialized set of Flow Creation commands. Failed to create flow '{create_flow_request.flow_name}'."
1242
- logger.error(details)
1243
- return DeserializeFlowFromCommandsResultFailure()
1484
+ flow_initialization_command = request.serialized_flow_commands.flow_initialization_command
1485
+ flow_initialization_result = GriptapeNodes.handle_request(flow_initialization_command)
1486
+
1487
+ # Handle different types of creation commands
1488
+ match flow_initialization_command:
1489
+ case CreateFlowRequest():
1490
+ if not isinstance(flow_initialization_result, CreateFlowResultSuccess):
1491
+ details = f"Attempted to deserialize a serialized set of Flow Creation commands. Failed to create flow '{flow_initialization_command.flow_name}'."
1492
+ logger.error(details)
1493
+ return DeserializeFlowFromCommandsResultFailure()
1494
+ flow_name = flow_initialization_result.flow_name
1495
+ case ImportWorkflowAsReferencedSubFlowRequest():
1496
+ if not isinstance(flow_initialization_result, ImportWorkflowAsReferencedSubFlowResultSuccess):
1497
+ details = f"Attempted to deserialize a serialized set of Flow Creation commands. Failed to import workflow '{flow_initialization_command.workflow_name}'."
1498
+ logger.error(details)
1499
+ return DeserializeFlowFromCommandsResultFailure()
1500
+ flow_name = flow_initialization_result.created_flow_name
1501
+ case _:
1502
+ details = f"Attempted to deserialize Flow Creation commands with unknown command type: {type(flow_initialization_command).__name__}."
1503
+ logger.error(details)
1504
+ return DeserializeFlowFromCommandsResultFailure()
1244
1505
 
1245
1506
  # Adopt the newly-created flow as our current context.
1246
- flow_name = create_flow_result.flow_name
1247
1507
  flow = GriptapeNodes.ObjectManager().attempt_get_object_by_name_as_type(flow_name, ControlFlow)
1248
1508
  if flow is None:
1249
- details = f"Attempted to deserialize a serialized set of Flow Creation commands. Failed to create flow '{create_flow_request.flow_name}'."
1509
+ details = f"Attempted to deserialize a serialized set of Flow Creation commands. Failed to find created flow '{flow_name}'."
1250
1510
  logger.error(details)
1251
1511
  return DeserializeFlowFromCommandsResultFailure()
1252
1512
  GriptapeNodes.ContextManager().push_flow(flow=flow)
@@ -1358,3 +1618,428 @@ class FlowManager:
1358
1618
  if node._tracked_parameters:
1359
1619
  node.emit_parameter_changes()
1360
1620
  return FlushParameterChangesResultSuccess()
1621
+
1622
+ def start_flow(self, flow: ControlFlow, start_node: BaseNode | None = None, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1623
+ if self.check_for_existing_running_flow():
1624
+ # If flow already exists, throw an error
1625
+ errormsg = "This workflow is already in progress. Please wait for the current process to finish before starting again."
1626
+ raise RuntimeError(errormsg)
1627
+
1628
+ if start_node is None:
1629
+ if self._global_flow_queue.empty():
1630
+ errormsg = "No Flow exists. You must create at least one control connection."
1631
+ raise RuntimeError(errormsg)
1632
+ start_node = self._global_flow_queue.get()
1633
+ self._global_flow_queue.task_done()
1634
+
1635
+ # Initialize global control flow machine if needed
1636
+ if self._global_control_flow_machine is None:
1637
+ self._global_control_flow_machine = ControlFlowMachine()
1638
+
1639
+ try:
1640
+ self._global_control_flow_machine.start_flow(start_node, debug_mode)
1641
+ except Exception:
1642
+ if self.check_for_existing_running_flow():
1643
+ self.cancel_flow_run()
1644
+ raise
1645
+
1646
+ def check_for_existing_running_flow(self) -> bool:
1647
+ if self._global_control_flow_machine is None:
1648
+ return False
1649
+ if (
1650
+ self._global_control_flow_machine._current_state is not CompleteState
1651
+ and self._global_control_flow_machine._current_state
1652
+ ):
1653
+ # Flow already exists in progress
1654
+ return True
1655
+ return bool(
1656
+ not self._global_control_flow_machine._context.resolution_machine.is_complete()
1657
+ and self._global_control_flow_machine._context.resolution_machine.is_started()
1658
+ )
1659
+
1660
+ def cancel_flow_run(self) -> None:
1661
+ if not self.check_for_existing_running_flow():
1662
+ errormsg = "Flow has not yet been started. Cannot cancel flow that hasn't begun."
1663
+ raise RuntimeError(errormsg)
1664
+ self._global_flow_queue.queue.clear()
1665
+ if self._global_control_flow_machine is not None:
1666
+ self._global_control_flow_machine.reset_machine()
1667
+ # Reset control flow machine
1668
+ self._global_single_node_resolution = False
1669
+ logger.debug("Cancelling flow run")
1670
+
1671
+ EventBus.publish_event(
1672
+ ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=ControlFlowCancelledEvent()))
1673
+ )
1674
+
1675
+ def reset_global_execution_state(self) -> None:
1676
+ """Reset all global execution state - useful when clearing all workflows."""
1677
+ self._global_flow_queue.queue.clear()
1678
+ if self._global_control_flow_machine is not None:
1679
+ self._global_control_flow_machine.reset_machine()
1680
+ self._global_control_flow_machine = None
1681
+ self._global_single_node_resolution = False
1682
+
1683
+ # Clear all connections to prevent memory leaks and stale references
1684
+ self._connections.connections.clear()
1685
+ self._connections.outgoing_index.clear()
1686
+ self._connections.incoming_index.clear()
1687
+
1688
+ logger.debug("Reset global execution state")
1689
+
1690
+ # Public methods to replace private variable access from external classes
1691
+ def is_execution_queue_empty(self) -> bool:
1692
+ """Check if the global execution queue is empty."""
1693
+ return self._global_flow_queue.empty()
1694
+
1695
+ def get_next_node_from_execution_queue(self) -> BaseNode | None:
1696
+ """Get the next node from the global execution queue, or None if empty."""
1697
+ if self._global_flow_queue.empty():
1698
+ return None
1699
+ node = self._global_flow_queue.get()
1700
+ self._global_flow_queue.task_done()
1701
+ return node
1702
+
1703
+ def clear_execution_queue(self) -> None:
1704
+ """Clear all nodes from the global execution queue."""
1705
+ self._global_flow_queue.queue.clear()
1706
+
1707
+ def has_connection(
1708
+ self,
1709
+ source_node: BaseNode,
1710
+ source_parameter: Parameter,
1711
+ target_node: BaseNode,
1712
+ target_parameter: Parameter,
1713
+ ) -> bool:
1714
+ """Check if a connection exists between the specified nodes and parameters."""
1715
+ return self._has_connection(source_node, source_parameter, target_node, target_parameter)
1716
+
1717
+ # Internal execution queue helper methods to consolidate redundant operations
1718
+ def _handle_flow_start_if_not_running(self, flow: ControlFlow, *, debug_mode: bool, error_message: str) -> None: # noqa: ARG002
1719
+ """Common logic for starting flow execution if not already running."""
1720
+ if not self.check_for_existing_running_flow():
1721
+ if self._global_flow_queue.empty():
1722
+ raise RuntimeError(error_message)
1723
+ start_node = self._global_flow_queue.get()
1724
+ self._global_flow_queue.task_done()
1725
+ if self._global_control_flow_machine is None:
1726
+ self._global_control_flow_machine = ControlFlowMachine()
1727
+ self._global_control_flow_machine.start_flow(start_node, debug_mode)
1728
+
1729
+ def _handle_post_execution_queue_processing(self, *, debug_mode: bool) -> None:
1730
+ """Handle execution queue processing after execution completes."""
1731
+ if not self.check_for_existing_running_flow() and not self._global_flow_queue.empty():
1732
+ start_node = self._global_flow_queue.get()
1733
+ self._global_flow_queue.task_done()
1734
+ if self._global_control_flow_machine is not None:
1735
+ self._global_control_flow_machine.start_flow(start_node, debug_mode)
1736
+
1737
+ def resolve_singular_node(self, flow: ControlFlow, node: BaseNode, debug_mode: bool = False) -> None: # noqa: FBT001, FBT002, ARG002
1738
+ # Set that we are only working on one node right now! no other stepping allowed
1739
+ if self.check_for_existing_running_flow():
1740
+ # If flow already exists, throw an error
1741
+ errormsg = f"This workflow is already in progress. Please wait for the current process to finish before starting {node.name} again."
1742
+ raise RuntimeError(errormsg)
1743
+ self._global_single_node_resolution = True
1744
+ # Initialize global control flow machine if needed
1745
+ if self._global_control_flow_machine is None:
1746
+ self._global_control_flow_machine = ControlFlowMachine()
1747
+ # Get the node resolution machine for the current flow!
1748
+ self._global_control_flow_machine._context.current_node = node
1749
+ resolution_machine = self._global_control_flow_machine._context.resolution_machine
1750
+ # Set debug mode
1751
+ resolution_machine.change_debug_mode(debug_mode)
1752
+ # Resolve the node.
1753
+ node.state = NodeResolutionState.UNRESOLVED
1754
+ resolution_machine.resolve_node(node)
1755
+ # decide if we can change it back to normal flow mode!
1756
+ if resolution_machine.is_complete():
1757
+ self._global_single_node_resolution = False
1758
+ self._global_control_flow_machine._context.current_node = None
1759
+
1760
+ def single_execution_step(self, flow: ControlFlow, change_debug_mode: bool) -> None: # noqa: FBT001
1761
+ # do a granular step
1762
+ self._handle_flow_start_if_not_running(
1763
+ flow, debug_mode=True, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1764
+ )
1765
+ if not self.check_for_existing_running_flow():
1766
+ return
1767
+ if self._global_control_flow_machine is not None:
1768
+ self._global_control_flow_machine.granular_step(change_debug_mode)
1769
+ resolution_machine = self._global_control_flow_machine._context.resolution_machine
1770
+ if self._global_single_node_resolution:
1771
+ resolution_machine = self._global_control_flow_machine._context.resolution_machine
1772
+ if resolution_machine.is_complete():
1773
+ self._global_single_node_resolution = False
1774
+
1775
+ def single_node_step(self, flow: ControlFlow) -> None:
1776
+ # It won't call single_node_step without an existing flow running from US.
1777
+ self._handle_flow_start_if_not_running(
1778
+ flow, debug_mode=True, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1779
+ )
1780
+ if not self.check_for_existing_running_flow():
1781
+ return
1782
+ # Step over a whole node
1783
+ if self._global_single_node_resolution:
1784
+ msg = "Cannot step through the Control Flow in Single Node Execution"
1785
+ raise RuntimeError(msg)
1786
+ if self._global_control_flow_machine is not None:
1787
+ self._global_control_flow_machine.node_step()
1788
+ # Start the next resolution step now please.
1789
+ self._handle_post_execution_queue_processing(debug_mode=True)
1790
+
1791
+ def continue_executing(self, flow: ControlFlow) -> None:
1792
+ self._handle_flow_start_if_not_running(
1793
+ flow, debug_mode=False, error_message="Flow has not yet been started. Cannot step while no flow has begun."
1794
+ )
1795
+ if not self.check_for_existing_running_flow():
1796
+ return
1797
+ # Turn all debugging to false and continue on
1798
+ if self._global_control_flow_machine is not None:
1799
+ self._global_control_flow_machine.change_debug_mode(False)
1800
+ if self._global_single_node_resolution:
1801
+ if self._global_control_flow_machine._context.resolution_machine.is_complete():
1802
+ self._global_single_node_resolution = False
1803
+ else:
1804
+ self._global_control_flow_machine._context.resolution_machine.update()
1805
+ else:
1806
+ self._global_control_flow_machine.node_step()
1807
+ # Now it is done executing. make sure it's actually done?
1808
+ self._handle_post_execution_queue_processing(debug_mode=False)
1809
+
1810
+ def unresolve_whole_flow(self, flow: ControlFlow) -> None:
1811
+ for node in flow.nodes.values():
1812
+ node.make_node_unresolved(current_states_to_trigger_change_event=None)
1813
+
1814
+ def flow_state(self, flow: ControlFlow) -> tuple[str | None, str | None]: # noqa: ARG002
1815
+ if not self.check_for_existing_running_flow():
1816
+ msg = "Flow hasn't started."
1817
+ raise RuntimeError(msg)
1818
+ if self._global_control_flow_machine is None:
1819
+ return None, None
1820
+ current_control_node = (
1821
+ self._global_control_flow_machine._context.current_node.name
1822
+ if self._global_control_flow_machine._context.current_node is not None
1823
+ else None
1824
+ )
1825
+ focus_stack_for_node = self._global_control_flow_machine._context.resolution_machine._context.focus_stack
1826
+ current_resolving_node = focus_stack_for_node[-1].node.name if len(focus_stack_for_node) else None
1827
+ return current_control_node, current_resolving_node
1828
+
1829
+ def get_start_node_from_node(self, flow: ControlFlow, node: BaseNode) -> BaseNode | None:
1830
+ # backwards chain in control outputs.
1831
+ if node not in flow.nodes.values():
1832
+ return None
1833
+ # Go back through incoming control connections to get the start node
1834
+ curr_node = node
1835
+ prev_node = self.get_prev_node(flow, curr_node)
1836
+ # Fencepost loop - get the first previous node name and then we go
1837
+ while prev_node:
1838
+ curr_node = prev_node
1839
+ prev_node = self.get_prev_node(flow, prev_node)
1840
+ return curr_node
1841
+
1842
+ def get_prev_node(self, flow: ControlFlow, node: BaseNode) -> BaseNode | None: # noqa: ARG002
1843
+ connections = self.get_connections()
1844
+ if node.name in connections.incoming_index:
1845
+ parameters = connections.incoming_index[node.name]
1846
+ for parameter_name in parameters:
1847
+ parameter = node.get_parameter_by_name(parameter_name)
1848
+ if parameter and ParameterTypeBuiltin.CONTROL_TYPE.value == parameter.output_type:
1849
+ # this is a control connection
1850
+ connection_ids = connections.incoming_index[node.name][parameter_name]
1851
+ for connection_id in connection_ids:
1852
+ connection = connections.connections[connection_id]
1853
+ return connection.get_source_node()
1854
+ return None
1855
+
1856
+ def get_start_node_queue(self) -> Queue | None: # noqa: C901, PLR0912
1857
+ # For cross-flow execution, we need to consider ALL nodes across ALL flows
1858
+ # Clear and use the global execution queue
1859
+ self._global_flow_queue.queue.clear()
1860
+
1861
+ # Get all flows and collect all nodes across all flows
1862
+ all_flows = GriptapeNodes.ObjectManager().get_filtered_subset(type=ControlFlow)
1863
+ all_nodes = []
1864
+ for current_flow in all_flows.values():
1865
+ all_nodes.extend(current_flow.nodes.values())
1866
+
1867
+ # if no nodes across all flows, no execution possible
1868
+ if not all_nodes:
1869
+ return None
1870
+
1871
+ data_nodes = []
1872
+ valid_data_nodes = []
1873
+ start_nodes = []
1874
+ control_nodes = []
1875
+ for node in all_nodes:
1876
+ # if it's a start node, start here! Return the first one!
1877
+ if isinstance(node, StartNode):
1878
+ start_nodes.append(node)
1879
+ continue
1880
+ # no start nodes. let's find the first control node.
1881
+ # if it's a control node, there could be a flow.
1882
+ control_param = False
1883
+ for parameter in node.parameters:
1884
+ if ParameterTypeBuiltin.CONTROL_TYPE.value == parameter.output_type:
1885
+ control_param = True
1886
+ break
1887
+ if not control_param:
1888
+ # saving this for later
1889
+ data_nodes.append(node)
1890
+ # If this node doesn't have a control connection..
1891
+ continue
1892
+
1893
+ cn_mgr = self.get_connections()
1894
+ # check if it has an incoming connection. If it does, it's not a start node
1895
+ has_control_connection = False
1896
+ if node.name in cn_mgr.incoming_index:
1897
+ for param_name in cn_mgr.incoming_index[node.name]:
1898
+ param = node.get_parameter_by_name(param_name)
1899
+ if param and ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type:
1900
+ # there is a control connection coming in
1901
+ has_control_connection = True
1902
+ break
1903
+ # if there is a connection coming in, isn't a start.
1904
+ if has_control_connection and not isinstance(node, StartLoopNode):
1905
+ continue
1906
+ # Does it have an outgoing connection?
1907
+ if node.name in cn_mgr.outgoing_index:
1908
+ # If one of the outgoing connections is control, add it. otherwise don't.
1909
+ for param_name in cn_mgr.outgoing_index[node.name]:
1910
+ param = node.get_parameter_by_name(param_name)
1911
+ if param and ParameterTypeBuiltin.CONTROL_TYPE.value == param.output_type:
1912
+ control_nodes.append(node)
1913
+ break
1914
+ else:
1915
+ control_nodes.append(node)
1916
+
1917
+ # If we've gotten to this point, there are no control parameters
1918
+ # Let's return a data node that has no OUTGOING data connections!
1919
+ for node in data_nodes:
1920
+ cn_mgr = self.get_connections()
1921
+ # check if it has an outgoing connection. We don't want it to (that means we get the most resolution)
1922
+ if node.name not in cn_mgr.outgoing_index:
1923
+ valid_data_nodes.append(node)
1924
+ # ok now - populate the global flow queue
1925
+ for node in start_nodes:
1926
+ self._global_flow_queue.put(node)
1927
+ for node in control_nodes:
1928
+ self._global_flow_queue.put(node)
1929
+ for node in valid_data_nodes:
1930
+ self._global_flow_queue.put(node)
1931
+
1932
+ return self._global_flow_queue
1933
+
1934
+ def get_connected_input_from_node(self, flow: ControlFlow, node: BaseNode) -> list[tuple[BaseNode, Parameter]]: # noqa: ARG002
1935
+ global_connections = self.get_connections()
1936
+ connections = []
1937
+ if node.name in global_connections.incoming_index:
1938
+ connection_ids = [
1939
+ item for value_list in global_connections.incoming_index[node.name].values() for item in value_list
1940
+ ]
1941
+ for connection_id in connection_ids:
1942
+ connection = global_connections.connections[connection_id]
1943
+ connections.append((connection.source_node, connection.source_parameter))
1944
+ return connections
1945
+
1946
+ def get_connected_output_from_node(self, flow: ControlFlow, node: BaseNode) -> list[tuple[BaseNode, Parameter]]: # noqa: ARG002
1947
+ global_connections = self.get_connections()
1948
+ connections = []
1949
+ if node.name in global_connections.outgoing_index:
1950
+ connection_ids = [
1951
+ item for value_list in global_connections.outgoing_index[node.name].values() for item in value_list
1952
+ ]
1953
+ for connection_id in connection_ids:
1954
+ connection = global_connections.connections[connection_id]
1955
+ connections.append((connection.target_node, connection.target_parameter))
1956
+ return connections
1957
+
1958
+ def get_connected_input_parameters(
1959
+ self,
1960
+ flow: ControlFlow, # noqa: ARG002
1961
+ node: BaseNode,
1962
+ param: Parameter,
1963
+ ) -> list[tuple[BaseNode, Parameter]]:
1964
+ global_connections = self.get_connections()
1965
+ connections = []
1966
+ if node.name in global_connections.incoming_index:
1967
+ incoming_params = global_connections.incoming_index[node.name]
1968
+ if param.name in incoming_params:
1969
+ for connection_id in incoming_params[param.name]:
1970
+ connection = global_connections.connections[connection_id]
1971
+ connections.append((connection.source_node, connection.source_parameter))
1972
+ return connections
1973
+
1974
+ def get_connections_on_node(self, flow: ControlFlow, node: BaseNode) -> list[BaseNode] | None: # noqa: ARG002
1975
+ connections = self.get_connections()
1976
+ # get all of the connection ids
1977
+ connected_nodes = []
1978
+ # Handle outgoing connections
1979
+ if node.name in connections.outgoing_index:
1980
+ outgoing_params = connections.outgoing_index[node.name]
1981
+ outgoing_connection_ids = []
1982
+ for connection_ids in outgoing_params.values():
1983
+ outgoing_connection_ids = outgoing_connection_ids + connection_ids
1984
+ for connection_id in outgoing_connection_ids:
1985
+ connection = connections.connections[connection_id]
1986
+ if connection.source_node not in connected_nodes:
1987
+ connected_nodes.append(connection.target_node)
1988
+ # Handle incoming connections
1989
+ if node.name in connections.incoming_index:
1990
+ incoming_params = connections.incoming_index[node.name]
1991
+ incoming_connection_ids = []
1992
+ for connection_ids in incoming_params.values():
1993
+ incoming_connection_ids = incoming_connection_ids + connection_ids
1994
+ for connection_id in incoming_connection_ids:
1995
+ connection = connections.connections[connection_id]
1996
+ if connection.source_node not in connected_nodes:
1997
+ connected_nodes.append(connection.source_node)
1998
+ # Return all connected nodes. No duplicates
1999
+ return connected_nodes
2000
+
2001
+ def get_all_connected_nodes(self, flow: ControlFlow, node: BaseNode) -> list[BaseNode]:
2002
+ discovered = {}
2003
+ processed = {}
2004
+ queue = Queue()
2005
+ queue.put(node)
2006
+ discovered[node] = True
2007
+ while not queue.empty():
2008
+ curr_node = queue.get()
2009
+ processed[curr_node] = True
2010
+ next_nodes = self.get_connections_on_node(flow, curr_node)
2011
+ if next_nodes:
2012
+ for next_node in next_nodes:
2013
+ if next_node not in discovered:
2014
+ discovered[next_node] = True
2015
+ queue.put(next_node)
2016
+ return list(processed.keys())
2017
+
2018
+ def get_node_dependencies(self, flow: ControlFlow, node: BaseNode) -> list[BaseNode]:
2019
+ """Get all upstream nodes that the given node depends on.
2020
+
2021
+ This method performs a breadth-first search starting from the given node and working backwards through its non-control input connections to identify all nodes that must run before this node can be resolved.
2022
+ It ignores control connections, since we're only focusing on node dependencies.
2023
+
2024
+ Args:
2025
+ flow (ControlFlow): The flow containing the node
2026
+ node (BaseNode): The node to find dependencies for
2027
+
2028
+ Returns:
2029
+ list[BaseNode]: A list of all nodes that the given node depends on, including the node itself (as the first element)
2030
+ """
2031
+ node_list = [node]
2032
+ node_queue = Queue()
2033
+ node_queue.put(node)
2034
+ while not node_queue.empty():
2035
+ curr_node = node_queue.get()
2036
+ input_connections = self.get_connected_input_from_node(flow, curr_node)
2037
+ if input_connections:
2038
+ for input_node, input_parameter in input_connections:
2039
+ if (
2040
+ ParameterTypeBuiltin.CONTROL_TYPE.value != input_parameter.output_type
2041
+ and input_node not in node_list
2042
+ ):
2043
+ node_list.append(input_node)
2044
+ node_queue.put(input_node)
2045
+ return node_list