griptape-nodes 0.55.1__py3-none-any.whl → 0.56.1__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 (60) hide show
  1. griptape_nodes/app/app.py +10 -15
  2. griptape_nodes/app/watch.py +35 -67
  3. griptape_nodes/bootstrap/utils/__init__.py +1 -0
  4. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +122 -0
  5. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +418 -0
  6. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +37 -8
  7. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +326 -0
  8. griptape_nodes/bootstrap/workflow_executors/utils/__init__.py +1 -0
  9. griptape_nodes/bootstrap/workflow_executors/utils/subprocess_script.py +51 -0
  10. griptape_nodes/bootstrap/workflow_publishers/__init__.py +1 -0
  11. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +43 -0
  12. griptape_nodes/bootstrap/workflow_publishers/subprocess_workflow_publisher.py +84 -0
  13. griptape_nodes/bootstrap/workflow_publishers/utils/__init__.py +1 -0
  14. griptape_nodes/bootstrap/workflow_publishers/utils/subprocess_script.py +54 -0
  15. griptape_nodes/cli/commands/engine.py +4 -15
  16. griptape_nodes/cli/commands/init.py +88 -0
  17. griptape_nodes/cli/commands/models.py +2 -0
  18. griptape_nodes/cli/main.py +6 -1
  19. griptape_nodes/cli/shared.py +1 -0
  20. griptape_nodes/exe_types/core_types.py +130 -0
  21. griptape_nodes/exe_types/node_types.py +125 -13
  22. griptape_nodes/machines/control_flow.py +10 -0
  23. griptape_nodes/machines/dag_builder.py +21 -2
  24. griptape_nodes/machines/parallel_resolution.py +25 -10
  25. griptape_nodes/node_library/workflow_registry.py +73 -3
  26. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  27. griptape_nodes/retained_mode/events/base_events.py +18 -17
  28. griptape_nodes/retained_mode/events/execution_events.py +15 -3
  29. griptape_nodes/retained_mode/events/flow_events.py +63 -7
  30. griptape_nodes/retained_mode/events/mcp_events.py +363 -0
  31. griptape_nodes/retained_mode/events/node_events.py +3 -4
  32. griptape_nodes/retained_mode/events/resource_events.py +290 -0
  33. griptape_nodes/retained_mode/events/workflow_events.py +57 -2
  34. griptape_nodes/retained_mode/griptape_nodes.py +17 -1
  35. griptape_nodes/retained_mode/managers/agent_manager.py +67 -4
  36. griptape_nodes/retained_mode/managers/event_manager.py +31 -13
  37. griptape_nodes/retained_mode/managers/flow_manager.py +731 -33
  38. griptape_nodes/retained_mode/managers/library_manager.py +15 -23
  39. griptape_nodes/retained_mode/managers/mcp_manager.py +364 -0
  40. griptape_nodes/retained_mode/managers/model_manager.py +184 -83
  41. griptape_nodes/retained_mode/managers/node_manager.py +15 -4
  42. griptape_nodes/retained_mode/managers/os_manager.py +118 -1
  43. griptape_nodes/retained_mode/managers/resource_components/__init__.py +1 -0
  44. griptape_nodes/retained_mode/managers/resource_components/capability_field.py +41 -0
  45. griptape_nodes/retained_mode/managers/resource_components/comparator.py +18 -0
  46. griptape_nodes/retained_mode/managers/resource_components/resource_instance.py +236 -0
  47. griptape_nodes/retained_mode/managers/resource_components/resource_type.py +79 -0
  48. griptape_nodes/retained_mode/managers/resource_manager.py +306 -0
  49. griptape_nodes/retained_mode/managers/resource_types/__init__.py +1 -0
  50. griptape_nodes/retained_mode/managers/resource_types/cpu_resource.py +108 -0
  51. griptape_nodes/retained_mode/managers/resource_types/os_resource.py +87 -0
  52. griptape_nodes/retained_mode/managers/settings.py +45 -0
  53. griptape_nodes/retained_mode/managers/sync_manager.py +10 -3
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +447 -263
  55. griptape_nodes/traits/multi_options.py +5 -1
  56. griptape_nodes/traits/options.py +10 -2
  57. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/METADATA +2 -2
  58. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/RECORD +60 -37
  59. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/WHEEL +1 -1
  60. {griptape_nodes-0.55.1.dist-info → griptape_nodes-0.56.1.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,8 @@ from __future__ import annotations
3
3
  import logging
4
4
  from enum import StrEnum
5
5
  from queue import Queue
6
- from typing import TYPE_CHECKING, NamedTuple, cast
6
+ from typing import TYPE_CHECKING, Any, NamedTuple, cast
7
+ from uuid import uuid4
7
8
 
8
9
  from griptape_nodes.exe_types.connections import Connections
9
10
  from griptape_nodes.exe_types.core_types import (
@@ -14,11 +15,20 @@ from griptape_nodes.exe_types.core_types import (
14
15
  ParameterTypeBuiltin,
15
16
  )
16
17
  from griptape_nodes.exe_types.flow import ControlFlow
17
- from griptape_nodes.exe_types.node_types import BaseNode, ErrorProxyNode, NodeResolutionState, StartLoopNode, StartNode
18
+ from griptape_nodes.exe_types.node_types import (
19
+ LOCAL_EXECUTION,
20
+ BaseNode,
21
+ ErrorProxyNode,
22
+ NodeDependencies,
23
+ NodeResolutionState,
24
+ StartLoopNode,
25
+ StartNode,
26
+ )
18
27
  from griptape_nodes.machines.control_flow import CompleteState, ControlFlowMachine
19
28
  from griptape_nodes.machines.dag_builder import DagBuilder
20
29
  from griptape_nodes.machines.parallel_resolution import ParallelResolutionMachine
21
30
  from griptape_nodes.machines.sequential_resolution import SequentialResolutionMachine
31
+ from griptape_nodes.node_library.library_registry import LibraryNameAndVersion, LibraryRegistry
22
32
  from griptape_nodes.retained_mode.events.base_events import (
23
33
  ExecutionEvent,
24
34
  ExecutionGriptapeNodeEvent,
@@ -33,6 +43,10 @@ from griptape_nodes.retained_mode.events.connection_events import (
33
43
  DeleteConnectionRequest,
34
44
  DeleteConnectionResultFailure,
35
45
  DeleteConnectionResultSuccess,
46
+ IncomingConnection,
47
+ ListConnectionsForNodeRequest,
48
+ ListConnectionsForNodeResultSuccess,
49
+ OutgoingConnection,
36
50
  )
37
51
  from griptape_nodes.retained_mode.events.execution_events import (
38
52
  CancelFlowRequest,
@@ -48,6 +62,7 @@ from griptape_nodes.retained_mode.events.execution_events import (
48
62
  GetIsFlowRunningRequest,
49
63
  GetIsFlowRunningResultFailure,
50
64
  GetIsFlowRunningResultSuccess,
65
+ InvolvedNodesEvent,
51
66
  SingleExecutionStepRequest,
52
67
  SingleExecutionStepResultFailure,
53
68
  SingleExecutionStepResultSuccess,
@@ -88,6 +103,9 @@ from griptape_nodes.retained_mode.events.flow_events import (
88
103
  ListNodesInFlowRequest,
89
104
  ListNodesInFlowResultFailure,
90
105
  ListNodesInFlowResultSuccess,
106
+ PackageNodeAsSerializedFlowRequest,
107
+ PackageNodeAsSerializedFlowResultFailure,
108
+ PackageNodeAsSerializedFlowResultSuccess,
91
109
  SerializedFlowCommands,
92
110
  SerializeFlowToCommandsRequest,
93
111
  SerializeFlowToCommandsResultFailure,
@@ -97,14 +115,18 @@ from griptape_nodes.retained_mode.events.flow_events import (
97
115
  SetFlowMetadataResultSuccess,
98
116
  )
99
117
  from griptape_nodes.retained_mode.events.node_events import (
118
+ CreateNodeRequest,
100
119
  DeleteNodeRequest,
101
120
  DeleteNodeResultFailure,
102
121
  DeserializeNodeFromCommandsRequest,
122
+ SerializedNodeCommands,
103
123
  SerializedParameterValueTracker,
104
124
  SerializeNodeToCommandsRequest,
105
125
  SerializeNodeToCommandsResultSuccess,
106
126
  )
107
127
  from griptape_nodes.retained_mode.events.parameter_events import (
128
+ AddParameterToNodeRequest,
129
+ AlterParameterDetailsRequest,
108
130
  SetParameterValueRequest,
109
131
  )
110
132
  from griptape_nodes.retained_mode.events.validation_events import (
@@ -121,6 +143,7 @@ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
121
143
  if TYPE_CHECKING:
122
144
  from griptape_nodes.retained_mode.events.base_events import ResultPayload
123
145
  from griptape_nodes.retained_mode.managers.event_manager import EventManager
146
+ from griptape_nodes.retained_mode.managers.workflow_manager import WorkflowShapeNodes
124
147
 
125
148
  logger = logging.getLogger("griptape_nodes")
126
149
 
@@ -138,6 +161,39 @@ class QueueItem(NamedTuple):
138
161
  dag_execution_type: DagExecutionType
139
162
 
140
163
 
164
+ class ConnectionAnalysis(NamedTuple):
165
+ """Analysis of connections separated by type (data vs control) when packaging nodes."""
166
+
167
+ incoming_data_connections: list[IncomingConnection]
168
+ incoming_control_connections: list[IncomingConnection]
169
+ outgoing_data_connections: list[OutgoingConnection]
170
+ outgoing_control_connections: list[OutgoingConnection]
171
+
172
+
173
+ class PackageNodeInfo(NamedTuple):
174
+ """Information about the node being packaged."""
175
+
176
+ package_node: BaseNode
177
+ package_flow_name: str
178
+
179
+
180
+ class PackagingStartNodeResult(NamedTuple):
181
+ """Result of creating start node commands and data connections for flow packaging."""
182
+
183
+ start_node_commands: SerializedNodeCommands
184
+ start_to_package_data_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
185
+ input_shape_data: WorkflowShapeNodes
186
+ start_node_parameter_value_commands: list[SerializedNodeCommands.IndirectSetParameterValueCommand]
187
+
188
+
189
+ class PackagingEndNodeResult(NamedTuple):
190
+ """Result of creating end node commands and data connections for flow packaging."""
191
+
192
+ end_node_commands: SerializedNodeCommands
193
+ package_to_end_data_connections: list[SerializedFlowCommands.IndirectConnectionSerialization]
194
+ output_shape_data: WorkflowShapeNodes
195
+
196
+
141
197
  class FlowManager:
142
198
  _name_to_parent_name: dict[str, str | None]
143
199
  _flow_to_referenced_workflow_name: dict[ControlFlow, str]
@@ -181,6 +237,9 @@ class FlowManager:
181
237
  event_manager.assign_manager_to_request_type(
182
238
  DeserializeFlowFromCommandsRequest, self.on_deserialize_flow_from_commands
183
239
  )
240
+ event_manager.assign_manager_to_request_type(
241
+ PackageNodeAsSerializedFlowRequest, self.on_package_node_as_serialized_flow_request
242
+ )
184
243
  event_manager.assign_manager_to_request_type(FlushParameterChangesRequest, self.on_flush_request)
185
244
 
186
245
  self._name_to_parent_name = {}
@@ -1012,6 +1071,600 @@ class FlowManager:
1012
1071
  result = DeleteConnectionResultSuccess(result_details=details)
1013
1072
  return result
1014
1073
 
1074
+ def on_package_node_as_serialized_flow_request(self, request: PackageNodeAsSerializedFlowRequest) -> ResultPayload: # noqa: PLR0911
1075
+ """Handle request to package a node as a serialized flow.
1076
+
1077
+ Creates a self-contained flow with Start node -> Package node -> End node structure,
1078
+ where artificial start/end nodes match the package node's connections.
1079
+ """
1080
+ # Step 1: Validate package node and flow
1081
+ package_node_info = self._validate_package_node_and_flow(request=request)
1082
+ if isinstance(package_node_info, PackageNodeAsSerializedFlowResultFailure):
1083
+ return package_node_info
1084
+
1085
+ # Step 2: Validate library and get version
1086
+ library_version = self._validate_and_get_library_info(request=request)
1087
+ if isinstance(library_version, PackageNodeAsSerializedFlowResultFailure):
1088
+ return library_version
1089
+
1090
+ # Step 3: Analyze package node connections
1091
+ connection_analysis = self._analyze_package_node_connections(
1092
+ package_node=package_node_info.package_node,
1093
+ node_name=package_node_info.package_node.name,
1094
+ )
1095
+ if isinstance(connection_analysis, PackageNodeAsSerializedFlowResultFailure):
1096
+ return connection_analysis
1097
+
1098
+ # Step 4: Serialize the package node
1099
+ unique_parameter_uuid_to_values = {}
1100
+ serialized_parameter_value_tracker = SerializedParameterValueTracker()
1101
+ package_node = package_node_info.package_node
1102
+ # Set to LOCAL_EXECUTION before packaging to prevent recursive loop.
1103
+ previous_value = package_node.get_parameter_value("execution_environment")
1104
+ package_node.set_parameter_value("execution_environment", LOCAL_EXECUTION)
1105
+ serialized_package_result = self._serialize_package_node(
1106
+ node_name=package_node_info.package_node.name,
1107
+ package_node=package_node_info.package_node,
1108
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1109
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1110
+ )
1111
+ # Now that we've serialized the value as LOCAL_EXECUTION, we need to restore it to whatever it was before
1112
+ package_node.set_parameter_value("execution_environment", previous_value)
1113
+ if isinstance(serialized_package_result, PackageNodeAsSerializedFlowResultFailure):
1114
+ return serialized_package_result
1115
+ # Step 5: Create start node commands and data connections
1116
+ start_node_result = self._create_start_node_commands(
1117
+ request=request,
1118
+ incoming_data_connections=connection_analysis.incoming_data_connections,
1119
+ package_node=package_node_info.package_node,
1120
+ package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1121
+ library_version=library_version,
1122
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1123
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1124
+ )
1125
+ if isinstance(start_node_result, PackageNodeAsSerializedFlowResultFailure):
1126
+ return start_node_result
1127
+
1128
+ # Step 6: Create end node commands and data connections
1129
+ end_node_result = self._create_end_node_commands(
1130
+ request=request,
1131
+ package_node=package_node_info.package_node,
1132
+ package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1133
+ library_version=library_version,
1134
+ )
1135
+ if isinstance(end_node_result, PackageNodeAsSerializedFlowResultFailure):
1136
+ return end_node_result
1137
+
1138
+ # Step 7a: Create start node control flow connection
1139
+ start_control_connection_result = self._create_start_node_control_connection(
1140
+ entry_control_parameter_name=request.entry_control_parameter_name,
1141
+ start_node_uuid=start_node_result.start_node_commands.node_uuid,
1142
+ package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1143
+ package_node=package_node_info.package_node,
1144
+ )
1145
+ if isinstance(start_control_connection_result, PackageNodeAsSerializedFlowResultFailure):
1146
+ return start_control_connection_result
1147
+
1148
+ start_control_connections = [start_control_connection_result]
1149
+
1150
+ # Use only start control connections for now (end node control connections not implemented yet)
1151
+ control_flow_connections = start_control_connections
1152
+
1153
+ # Step 8: Assemble the complete serialized flow
1154
+ packaged_flow = self._assemble_serialized_flow(
1155
+ serialized_package_result=serialized_package_result,
1156
+ start_node_result=start_node_result,
1157
+ end_node_result=end_node_result,
1158
+ control_flow_connections=control_flow_connections,
1159
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1160
+ library_version=library_version,
1161
+ request=request,
1162
+ )
1163
+
1164
+ # Step 9: Build WorkflowShape from collected parameter shape data
1165
+ workflow_shape = GriptapeNodes.WorkflowManager().build_workflow_shape_from_parameter_info(
1166
+ input_node_params=start_node_result.input_shape_data, output_node_params=end_node_result.output_shape_data
1167
+ )
1168
+ # Return success result
1169
+ return PackageNodeAsSerializedFlowResultSuccess(
1170
+ result_details=f'Successfully packaged node "{package_node_info.package_node.name}" from flow "{package_node_info.package_flow_name}" as serialized flow with start node type "{request.start_node_type}" and end node type "{request.end_node_type}" from library "{request.start_end_specific_library_name}".',
1171
+ serialized_flow_commands=packaged_flow,
1172
+ workflow_shape=workflow_shape,
1173
+ )
1174
+
1175
+ def _validate_package_node_and_flow( # noqa: PLR0911
1176
+ self, request: PackageNodeAsSerializedFlowRequest
1177
+ ) -> PackageNodeInfo | PackageNodeAsSerializedFlowResultFailure:
1178
+ """Validate and retrieve the package node and its parent flow."""
1179
+ node_name = request.node_name
1180
+ package_node = None
1181
+
1182
+ if node_name is None:
1183
+ # First check if we have a current node
1184
+ if not GriptapeNodes.ContextManager().has_current_node():
1185
+ details = (
1186
+ "Attempted to package node from Current Context. Failed because the Current Context was empty."
1187
+ )
1188
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1189
+
1190
+ # Get the current node from context
1191
+ package_node = GriptapeNodes.ContextManager().get_current_node()
1192
+ node_name = package_node.name
1193
+
1194
+ if package_node is None:
1195
+ try:
1196
+ package_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1197
+ except ValueError as err:
1198
+ details = f"Attempted to package node '{node_name}'. Failed because node does not exist. Error: {err}."
1199
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1200
+
1201
+ # Get the flow containing this node using the same pattern
1202
+ package_flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
1203
+ if package_flow_name is None:
1204
+ details = f"Attempted to package node '{node_name}'. Failed because node is not assigned to any flow."
1205
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1206
+
1207
+ try:
1208
+ self.get_flow_by_name(flow_name=package_flow_name)
1209
+ except KeyError as err:
1210
+ details = f"Attempted to package node '{node_name}' from flow '{package_flow_name}'. Failed because flow does not exist. Error: {err}."
1211
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1212
+
1213
+ # Validate entry control parameter if specified
1214
+ if request.entry_control_parameter_name is not None:
1215
+ entry_param = package_node.get_parameter_by_name(request.entry_control_parameter_name)
1216
+ if entry_param is None:
1217
+ details = f"Attempted to package node '{node_name}' with entry control parameter '{request.entry_control_parameter_name}'. Failed because the parameter does not exist on the node."
1218
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1219
+
1220
+ # Verify it's actually a control parameter
1221
+ if ParameterTypeBuiltin.CONTROL_TYPE.value not in entry_param.input_types:
1222
+ details = f"Attempted to package node '{node_name}' with entry control parameter '{request.entry_control_parameter_name}'. Failed because the parameter is not a control type parameter."
1223
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1224
+
1225
+ return PackageNodeInfo(package_node=package_node, package_flow_name=package_flow_name)
1226
+
1227
+ def _validate_and_get_library_info(
1228
+ self, request: PackageNodeAsSerializedFlowRequest
1229
+ ) -> str | PackageNodeAsSerializedFlowResultFailure:
1230
+ """Validate start/end node types exist in library and return library version."""
1231
+ # Early validation - ensure both start and end node types exist in the specified library
1232
+ try:
1233
+ start_end_library = LibraryRegistry.get_library_for_node_type(
1234
+ node_type=request.start_node_type, specific_library_name=request.start_end_specific_library_name
1235
+ )
1236
+ except KeyError as err:
1237
+ details = f"Attempted to package node with start node type '{request.start_node_type}' from library '{request.start_end_specific_library_name}'. Failed because start node type was not found in library. Error: {err}."
1238
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1239
+
1240
+ try:
1241
+ LibraryRegistry.get_library_for_node_type(
1242
+ node_type=request.end_node_type, specific_library_name=request.start_end_specific_library_name
1243
+ )
1244
+ except KeyError as err:
1245
+ details = f"Attempted to package node with end node type '{request.end_node_type}' from library '{request.start_end_specific_library_name}'. Failed because end node type was not found in library. Error: {err}."
1246
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1247
+
1248
+ # Get the actual library version
1249
+ start_end_library_metadata = start_end_library.get_metadata()
1250
+ return start_end_library_metadata.library_version
1251
+
1252
+ def _analyze_package_node_connections(
1253
+ self, package_node: BaseNode, node_name: str
1254
+ ) -> ConnectionAnalysis | PackageNodeAsSerializedFlowResultFailure:
1255
+ """Analyze package node connections and separate control from data connections."""
1256
+ # Get connection details using the efficient approach
1257
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
1258
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1259
+ list_connections_request
1260
+ )
1261
+
1262
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1263
+ details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
1264
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1265
+
1266
+ # Separate control connections from data connections based on package node's parameter types
1267
+ incoming_data_connections = []
1268
+ incoming_control_connections = []
1269
+ for incoming_conn in list_connections_result.incoming_connections:
1270
+ # Get the package node's parameter to check if it's a control type
1271
+ package_param = package_node.get_parameter_by_name(incoming_conn.target_parameter_name)
1272
+ if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value in package_param.input_types:
1273
+ incoming_control_connections.append(incoming_conn)
1274
+ else:
1275
+ incoming_data_connections.append(incoming_conn)
1276
+
1277
+ outgoing_data_connections = []
1278
+ outgoing_control_connections = []
1279
+ for outgoing_conn in list_connections_result.outgoing_connections:
1280
+ # Get the package node's parameter to check if it's a control type
1281
+ package_param = package_node.get_parameter_by_name(outgoing_conn.source_parameter_name)
1282
+ if package_param and ParameterTypeBuiltin.CONTROL_TYPE.value == package_param.output_type:
1283
+ outgoing_control_connections.append(outgoing_conn)
1284
+ else:
1285
+ outgoing_data_connections.append(outgoing_conn)
1286
+
1287
+ return ConnectionAnalysis(
1288
+ incoming_data_connections=incoming_data_connections,
1289
+ incoming_control_connections=incoming_control_connections,
1290
+ outgoing_data_connections=outgoing_data_connections,
1291
+ outgoing_control_connections=outgoing_control_connections,
1292
+ )
1293
+
1294
+ def _serialize_package_node(
1295
+ self,
1296
+ node_name: str,
1297
+ package_node: BaseNode,
1298
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1299
+ serialized_parameter_value_tracker: SerializedParameterValueTracker,
1300
+ ) -> SerializeNodeToCommandsResultSuccess | PackageNodeAsSerializedFlowResultFailure:
1301
+ """Serialize the package node to commands, adding OUTPUT mode to PROPERTY-only parameters."""
1302
+ # Use the provided parameter tracking structures
1303
+
1304
+ # Create serialization request for the package node
1305
+ serialize_node_request = SerializeNodeToCommandsRequest(
1306
+ node_name=node_name,
1307
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1308
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1309
+ )
1310
+
1311
+ # Execute the serialization
1312
+ serialize_node_result = GriptapeNodes.handle_request(serialize_node_request)
1313
+ if not isinstance(serialize_node_result, SerializeNodeToCommandsResultSuccess):
1314
+ details = f"Attempted to serialize package node '{node_name}'. Failed because node serialization failed."
1315
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1316
+
1317
+ # Add ALTER parameter commands for PROPERTY-only parameters to enable OUTPUT mode
1318
+ # We need these to emit their values back so that the orchestrator/caller
1319
+ # can reconcile the packaged node's values after it is executed.
1320
+ package_alter_parameter_commands = []
1321
+ for package_param in package_node.parameters:
1322
+ has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
1323
+ has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
1324
+
1325
+ # If has PROPERTY but not OUTPUT, add ALTER command to enable OUTPUT
1326
+ if has_property_mode and not has_output_mode:
1327
+ alter_param_request = AlterParameterDetailsRequest(
1328
+ parameter_name=package_param.name,
1329
+ node_name=package_node.name,
1330
+ mode_allowed_output=True,
1331
+ )
1332
+ package_alter_parameter_commands.append(alter_param_request)
1333
+
1334
+ # If we have alter parameter commands, append them to the existing element_modification_commands
1335
+ if package_alter_parameter_commands:
1336
+ serialize_node_result.serialized_node_commands.element_modification_commands.extend(
1337
+ package_alter_parameter_commands
1338
+ )
1339
+
1340
+ return serialize_node_result
1341
+
1342
+ def _create_start_node_commands( # noqa: PLR0913
1343
+ self,
1344
+ request: PackageNodeAsSerializedFlowRequest,
1345
+ incoming_data_connections: list[IncomingConnection],
1346
+ package_node: BaseNode,
1347
+ package_node_uuid: SerializedNodeCommands.NodeUUID,
1348
+ library_version: str,
1349
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1350
+ serialized_parameter_value_tracker: SerializedParameterValueTracker,
1351
+ ) -> PackagingStartNodeResult | PackageNodeAsSerializedFlowResultFailure:
1352
+ """Create start node commands and connections for incoming data connections."""
1353
+ # Generate UUID and name for start node
1354
+ start_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
1355
+ start_node_name = f"Start_Package_{package_node.name}"
1356
+
1357
+ # Build start node CreateNodeRequest
1358
+ start_create_node_command = CreateNodeRequest(
1359
+ node_type=request.start_node_type,
1360
+ specific_library_name=request.start_end_specific_library_name,
1361
+ node_name=start_node_name,
1362
+ metadata={},
1363
+ initial_setup=True,
1364
+ create_error_proxy_on_failure=False,
1365
+ )
1366
+
1367
+ # Create library details
1368
+ start_node_library_details = LibraryNameAndVersion(
1369
+ library_name=request.start_end_specific_library_name,
1370
+ library_version=library_version,
1371
+ )
1372
+
1373
+ # Create parameter modification commands and connection mappings for the start node based on incoming DATA connections
1374
+ start_node_parameter_commands = []
1375
+ start_to_package_data_connections = []
1376
+ start_node_parameter_value_commands = []
1377
+ input_shape_data: WorkflowShapeNodes = {}
1378
+
1379
+ for incoming_conn in incoming_data_connections:
1380
+ # Parameter name: use the package node's parameter name
1381
+ param_name = incoming_conn.target_parameter_name
1382
+
1383
+ # Get the source node to determine parameter type
1384
+ try:
1385
+ source_node = GriptapeNodes.NodeManager().get_node_by_name(incoming_conn.source_node_name)
1386
+ except ValueError as err:
1387
+ details = f"Attempted to package node '{package_node.name}'. Failed because source node '{incoming_conn.source_node_name}' from incoming connection could not be found. Error: {err}."
1388
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1389
+
1390
+ # Get the source parameter
1391
+ source_param = source_node.get_parameter_by_name(incoming_conn.source_parameter_name)
1392
+ if not source_param:
1393
+ details = f"Attempted to package node '{package_node.name}'. Failed because source parameter '{incoming_conn.source_parameter_name}' on node '{incoming_conn.source_node_name}' from incoming connection could not be found."
1394
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1395
+
1396
+ # Extract parameter shape info for workflow shape (inputs from external sources)
1397
+ param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
1398
+ source_param, include_control_params=True
1399
+ )
1400
+ if param_shape_info is not None:
1401
+ if start_node_name not in input_shape_data:
1402
+ input_shape_data[start_node_name] = {}
1403
+ input_shape_data[start_node_name][param_name] = param_shape_info
1404
+
1405
+ # Extract parameter value from source node to set on start node
1406
+ param_value_commands = GriptapeNodes.NodeManager().handle_parameter_value_saving(
1407
+ parameter=source_param,
1408
+ node=source_node,
1409
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1410
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1411
+ create_node_request=start_create_node_command,
1412
+ )
1413
+ if param_value_commands is not None:
1414
+ # Modify each command to target the start node parameter instead
1415
+ for param_value_command in param_value_commands:
1416
+ param_value_command.set_parameter_value_command.node_name = start_node_name
1417
+ param_value_command.set_parameter_value_command.parameter_name = param_name
1418
+ start_node_parameter_value_commands.append(param_value_command)
1419
+
1420
+ # Create parameter command for start node
1421
+ add_param_request = AddParameterToNodeRequest(
1422
+ node_name=start_node_name,
1423
+ parameter_name=param_name,
1424
+ # Use the source parameter's output_type as the type for our start node parameter
1425
+ # since we want to match what the original connection was providing
1426
+ type=source_param.output_type,
1427
+ default_value=None,
1428
+ tooltip=f"Parameter {param_name} from packaged flow",
1429
+ initial_setup=True,
1430
+ )
1431
+ start_node_parameter_commands.append(add_param_request)
1432
+
1433
+ # Create connection from start node to package node
1434
+ start_to_package_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1435
+ source_node_uuid=start_node_uuid,
1436
+ source_parameter_name=param_name,
1437
+ target_node_uuid=package_node_uuid,
1438
+ target_parameter_name=param_name,
1439
+ )
1440
+ start_to_package_data_connections.append(start_to_package_connection)
1441
+
1442
+ # Build complete SerializedNodeCommands for start node
1443
+ start_node_dependencies = NodeDependencies()
1444
+ start_node_dependencies.libraries.add(start_node_library_details)
1445
+
1446
+ start_node_commands = SerializedNodeCommands(
1447
+ create_node_command=start_create_node_command,
1448
+ element_modification_commands=start_node_parameter_commands,
1449
+ node_dependencies=start_node_dependencies,
1450
+ node_uuid=start_node_uuid,
1451
+ )
1452
+
1453
+ return PackagingStartNodeResult(
1454
+ start_node_commands=start_node_commands,
1455
+ start_to_package_data_connections=start_to_package_data_connections,
1456
+ input_shape_data=input_shape_data,
1457
+ start_node_parameter_value_commands=start_node_parameter_value_commands,
1458
+ )
1459
+
1460
+ def _create_end_node_commands(
1461
+ self,
1462
+ request: PackageNodeAsSerializedFlowRequest,
1463
+ package_node: BaseNode,
1464
+ package_node_uuid: SerializedNodeCommands.NodeUUID,
1465
+ library_version: str,
1466
+ ) -> PackagingEndNodeResult | PackageNodeAsSerializedFlowResultFailure:
1467
+ """Create end node commands and connections for ALL package parameters that meet criteria."""
1468
+ # Generate UUID and name for end node
1469
+ end_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
1470
+ end_node_name = f"End_Package_{package_node.name}"
1471
+
1472
+ # Build end node CreateNodeRequest
1473
+ end_create_node_command = CreateNodeRequest(
1474
+ node_type=request.end_node_type,
1475
+ specific_library_name=request.start_end_specific_library_name,
1476
+ node_name=end_node_name,
1477
+ metadata={},
1478
+ initial_setup=True,
1479
+ create_error_proxy_on_failure=False,
1480
+ )
1481
+
1482
+ # Create library details
1483
+ end_node_library_details = LibraryNameAndVersion(
1484
+ library_name=request.start_end_specific_library_name,
1485
+ library_version=library_version,
1486
+ )
1487
+
1488
+ # Process ALL package node parameters to create end node parameters and connections
1489
+ # Note: PROPERTY-only parameters are guaranteed to have OUTPUT mode after serialization
1490
+ end_node_parameter_commands = []
1491
+ package_to_end_data_connections = []
1492
+ output_shape_data: WorkflowShapeNodes = {}
1493
+
1494
+ for package_param in package_node.parameters:
1495
+ # Only ignore parameters that have ONLY INPUT mode (no OUTPUT or PROPERTY)
1496
+ has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
1497
+ has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
1498
+
1499
+ # Skip parameters that only have INPUT mode
1500
+ if not has_output_mode and not has_property_mode:
1501
+ continue
1502
+
1503
+ # Create prefixed parameter name for end node to avoid collisions
1504
+ end_param_name = f"{request.output_parameter_prefix}{package_param.name}"
1505
+
1506
+ # Extract parameter shape info for workflow shape (outputs to external consumers)
1507
+ param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
1508
+ package_param, include_control_params=True
1509
+ )
1510
+ if param_shape_info is not None:
1511
+ if end_node_name not in output_shape_data:
1512
+ output_shape_data[end_node_name] = {}
1513
+ output_shape_data[end_node_name][end_param_name] = param_shape_info
1514
+
1515
+ # Create parameter command for end node
1516
+ add_param_request = AddParameterToNodeRequest(
1517
+ node_name=end_node_name,
1518
+ parameter_name=end_param_name,
1519
+ # Use the package node's output_type as the type for our end node parameter
1520
+ type=package_param.output_type,
1521
+ default_value=None,
1522
+ tooltip=f"Output parameter {package_param.name} from packaged node {package_node.name}",
1523
+ initial_setup=True,
1524
+ )
1525
+ end_node_parameter_commands.append(add_param_request)
1526
+
1527
+ # Create connection from package node to end node
1528
+ package_to_end_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1529
+ source_node_uuid=package_node_uuid,
1530
+ source_parameter_name=package_param.name,
1531
+ target_node_uuid=end_node_uuid,
1532
+ target_parameter_name=end_param_name,
1533
+ )
1534
+ package_to_end_data_connections.append(package_to_end_connection)
1535
+
1536
+ # Build complete SerializedNodeCommands for end node
1537
+ end_node_dependencies = NodeDependencies()
1538
+ end_node_dependencies.libraries.add(end_node_library_details)
1539
+
1540
+ end_node_commands = SerializedNodeCommands(
1541
+ create_node_command=end_create_node_command,
1542
+ element_modification_commands=end_node_parameter_commands,
1543
+ node_dependencies=end_node_dependencies,
1544
+ node_uuid=end_node_uuid,
1545
+ )
1546
+
1547
+ return PackagingEndNodeResult(
1548
+ end_node_commands=end_node_commands,
1549
+ package_to_end_data_connections=package_to_end_data_connections,
1550
+ output_shape_data=output_shape_data,
1551
+ )
1552
+
1553
+ def _create_start_node_control_connection(
1554
+ self,
1555
+ entry_control_parameter_name: str | None,
1556
+ start_node_uuid: SerializedNodeCommands.NodeUUID,
1557
+ package_node_uuid: SerializedNodeCommands.NodeUUID,
1558
+ package_node: BaseNode,
1559
+ ) -> SerializedFlowCommands.IndirectConnectionSerialization | PackageNodeAsSerializedFlowResultFailure:
1560
+ """Create control flow connection from start node to package node.
1561
+
1562
+ Connects the start node's first control output to the specified or first available package node control input.
1563
+ """
1564
+ if entry_control_parameter_name is not None:
1565
+ # Case 1: Specific entry parameter name provided
1566
+ package_control_input_name = entry_control_parameter_name
1567
+ else:
1568
+ # Case 2: Find the first available control input parameter
1569
+ package_control_input_name = None
1570
+ for param in package_node.parameters:
1571
+ if ParameterTypeBuiltin.CONTROL_TYPE.value in param.input_types:
1572
+ package_control_input_name = param.name
1573
+ logger.warning(
1574
+ "No entry_control_parameter_name specified for packaging node '%s'. "
1575
+ "Using first available control input parameter: '%s'",
1576
+ package_node.name,
1577
+ package_control_input_name,
1578
+ )
1579
+ break
1580
+
1581
+ if package_control_input_name is None:
1582
+ details = f"Attempted to package node '{package_node.name}'. Failed because no control input parameters found on the node, so cannot create control flow connection."
1583
+ return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1584
+
1585
+ # StartNode always has a control output parameter with name "exec_out"
1586
+ source_control_parameter_name = "exec_out"
1587
+
1588
+ # Create the connection
1589
+ control_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1590
+ source_node_uuid=start_node_uuid,
1591
+ source_parameter_name=source_control_parameter_name,
1592
+ target_node_uuid=package_node_uuid,
1593
+ target_parameter_name=package_control_input_name,
1594
+ )
1595
+ return control_connection
1596
+
1597
+ def _assemble_serialized_flow( # noqa: PLR0913
1598
+ self,
1599
+ serialized_package_result: SerializeNodeToCommandsResultSuccess,
1600
+ start_node_result: PackagingStartNodeResult,
1601
+ end_node_result: PackagingEndNodeResult,
1602
+ control_flow_connections: list[SerializedFlowCommands.IndirectConnectionSerialization],
1603
+ unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1604
+ library_version: str,
1605
+ request: PackageNodeAsSerializedFlowRequest,
1606
+ ) -> SerializedFlowCommands:
1607
+ """Assemble the complete SerializedFlowCommands from all components."""
1608
+ # Combine all connections: Start->Package + Package->End + Control Flow
1609
+ all_connections = (
1610
+ start_node_result.start_to_package_data_connections
1611
+ + end_node_result.package_to_end_data_connections
1612
+ + control_flow_connections
1613
+ )
1614
+
1615
+ # Set up lock commands if needed
1616
+ set_lock_commands_per_node = {}
1617
+ if serialized_package_result.serialized_node_commands.lock_node_command:
1618
+ set_lock_commands_per_node[serialized_package_result.serialized_node_commands.node_uuid] = (
1619
+ serialized_package_result.serialized_node_commands.lock_node_command
1620
+ )
1621
+
1622
+ # Include all three nodes in the flow
1623
+ all_serialized_nodes = [
1624
+ start_node_result.start_node_commands,
1625
+ serialized_package_result.serialized_node_commands,
1626
+ end_node_result.end_node_commands,
1627
+ ]
1628
+
1629
+ # Create a CreateFlowRequest for the packaged flow so that it can
1630
+ # run as a standalone workflow.
1631
+ package_flow_name = (
1632
+ f"Packaged_{serialized_package_result.serialized_node_commands.create_node_command.node_name}"
1633
+ )
1634
+ packaged_flow_metadata = {} # Keep it simple until we have reason to populate it
1635
+
1636
+ create_packaged_flow_request = CreateFlowRequest(
1637
+ parent_flow_name=None, # Standalone flow
1638
+ flow_name=package_flow_name,
1639
+ set_as_new_context=False, # Let deserializer decide
1640
+ metadata=packaged_flow_metadata,
1641
+ )
1642
+
1643
+ # Aggregate dependencies from the packaged nodes
1644
+ packaged_dependencies = self._aggregate_flow_dependencies(all_serialized_nodes, [])
1645
+
1646
+ # Add the start/end specific library dependency
1647
+ start_end_library_dependency = LibraryNameAndVersion(
1648
+ library_name=request.start_end_specific_library_name,
1649
+ library_version=library_version,
1650
+ )
1651
+ packaged_dependencies.libraries.add(start_end_library_dependency)
1652
+
1653
+ # Build the complete SerializedFlowCommands
1654
+ return SerializedFlowCommands(
1655
+ flow_initialization_command=create_packaged_flow_request,
1656
+ serialized_node_commands=all_serialized_nodes,
1657
+ serialized_connections=all_connections,
1658
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1659
+ set_parameter_value_commands={
1660
+ serialized_package_result.serialized_node_commands.node_uuid: serialized_package_result.set_parameter_value_commands,
1661
+ start_node_result.start_node_commands.node_uuid: start_node_result.start_node_parameter_value_commands,
1662
+ },
1663
+ set_lock_commands_per_node=set_lock_commands_per_node,
1664
+ sub_flows_commands=[],
1665
+ node_dependencies=packaged_dependencies,
1666
+ )
1667
+
1015
1668
  async def on_start_flow_request(self, request: StartFlowRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912
1016
1669
  # which flow
1017
1670
  flow_name = request.flow_name
@@ -1090,7 +1743,7 @@ class FlowManager:
1090
1743
  details = f"Could not get flow state. Error: {err}"
1091
1744
  return GetFlowStateResultFailure(result_details=details)
1092
1745
  try:
1093
- control_nodes, resolving_nodes, involved_nodes = self.flow_state(flow)
1746
+ control_nodes, resolving_nodes = self.flow_state(flow)
1094
1747
  except Exception as e:
1095
1748
  details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
1096
1749
  logger.exception(details)
@@ -1100,7 +1753,6 @@ class FlowManager:
1100
1753
  control_nodes=control_nodes,
1101
1754
  resolving_node=resolving_nodes,
1102
1755
  result_details=details,
1103
- involved_nodes=involved_nodes,
1104
1756
  )
1105
1757
 
1106
1758
  def on_cancel_flow_request(self, request: CancelFlowRequest) -> ResultPayload:
@@ -1264,6 +1916,31 @@ class FlowManager:
1264
1916
 
1265
1917
  return ListFlowsInCurrentContextResultSuccess(flow_names=ret_list, result_details=details)
1266
1918
 
1919
+ def _aggregate_flow_dependencies(
1920
+ self, serialized_node_commands: list[SerializedNodeCommands], sub_flows_commands: list[SerializedFlowCommands]
1921
+ ) -> NodeDependencies:
1922
+ """Aggregate dependencies from nodes and sub-flows into a single NodeDependencies object.
1923
+
1924
+ Args:
1925
+ serialized_node_commands: List of serialized node commands to aggregate from
1926
+ sub_flows_commands: List of sub-flow commands to aggregate from
1927
+
1928
+ Returns:
1929
+ NodeDependencies object with all dependencies merged
1930
+ """
1931
+ # Start with empty dependencies and aggregate into it
1932
+ aggregated_deps = NodeDependencies()
1933
+
1934
+ # Aggregate dependencies from all nodes
1935
+ for node_cmd in serialized_node_commands:
1936
+ aggregated_deps.aggregate_from(node_cmd.node_dependencies)
1937
+
1938
+ # Aggregate dependencies from all sub-flows
1939
+ for sub_flow_cmd in sub_flows_commands:
1940
+ aggregated_deps.aggregate_from(sub_flow_cmd.node_dependencies)
1941
+
1942
+ return aggregated_deps
1943
+
1267
1944
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/861
1268
1945
  # similar manager refactors: https://github.com/griptape-ai/griptape-nodes/issues/806
1269
1946
  def on_serialize_flow_to_commands(self, request: SerializeFlowToCommandsRequest) -> ResultPayload: # noqa: C901, PLR0911, PLR0912, PLR0915
@@ -1285,12 +1962,6 @@ class FlowManager:
1285
1962
  )
1286
1963
  return SerializeFlowToCommandsResultFailure(result_details=details)
1287
1964
 
1288
- # Track all node libraries that were in use by these Nodes
1289
- node_libraries_in_use = set()
1290
-
1291
- # Track all referenced workflows used by this flow and its sub-flows
1292
- referenced_workflows_in_use = set()
1293
-
1294
1965
  # Track all parameter values that were in use by these Nodes (maps UUID to Parameter value)
1295
1966
  unique_parameter_uuid_to_values = {}
1296
1967
  # And track how values map into that map.
@@ -1306,7 +1977,6 @@ class FlowManager:
1306
1977
  workflow_name=referenced_workflow_name, # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1307
1978
  imported_flow_metadata=flow.metadata,
1308
1979
  )
1309
- referenced_workflows_in_use.add(referenced_workflow_name) # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1310
1980
  else:
1311
1981
  # Always set set_as_new_context=False during serialization - let the workflow manager
1312
1982
  # that loads this serialized flow decide whether to push it to context or not
@@ -1354,7 +2024,6 @@ class FlowManager:
1354
2024
  node_name_to_uuid[node_name] = serialized_node.node_uuid
1355
2025
 
1356
2026
  serialized_node_commands.append(serialized_node)
1357
- node_libraries_in_use.add(serialized_node.node_library_details)
1358
2027
  # Get the list of set value commands for THIS node.
1359
2028
  set_value_commands_list = serialize_node_result.set_parameter_value_commands
1360
2029
  if serialize_node_result.serialized_node_commands.lock_node_command is not None:
@@ -1403,20 +2072,22 @@ class FlowManager:
1403
2072
  imported_flow_metadata=flow.metadata,
1404
2073
  )
1405
2074
 
2075
+ # Create NodeDependencies with just the referenced workflow
2076
+ sub_flow_dependencies = NodeDependencies(
2077
+ referenced_workflows={referenced_workflow_name} # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
2078
+ )
2079
+
1406
2080
  serialized_flow = SerializedFlowCommands(
1407
- node_libraries_used=set(),
1408
2081
  flow_initialization_command=import_command,
1409
2082
  serialized_node_commands=[],
1410
2083
  serialized_connections=[],
1411
2084
  unique_parameter_uuid_to_values={},
1412
2085
  set_parameter_value_commands={},
2086
+ set_lock_commands_per_node={},
1413
2087
  sub_flows_commands=[],
1414
- referenced_workflows={referenced_workflow_name}, # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
2088
+ node_dependencies=sub_flow_dependencies,
1415
2089
  )
1416
2090
  sub_flow_commands.append(serialized_flow)
1417
-
1418
- # Add this referenced workflow to our accumulation
1419
- referenced_workflows_in_use.add(referenced_workflow_name) # type: ignore[arg-type] # is_referenced_workflow() guarantees this is not None
1420
2091
  else:
1421
2092
  # For standalone sub-flows, use the existing recursive serialization
1422
2093
  with GriptapeNodes.ContextManager().flow(flow=flow):
@@ -1428,11 +2099,8 @@ class FlowManager:
1428
2099
  serialized_flow = child_flow_result.serialized_flow_commands
1429
2100
  sub_flow_commands.append(serialized_flow)
1430
2101
 
1431
- # Merge in all child flow library details.
1432
- node_libraries_in_use.union(serialized_flow.node_libraries_used)
1433
-
1434
- # Merge in all child flow referenced workflows.
1435
- referenced_workflows_in_use.union(serialized_flow.referenced_workflows)
2102
+ # Aggregate all dependencies from nodes and sub-flows
2103
+ aggregated_dependencies = self._aggregate_flow_dependencies(serialized_node_commands, sub_flow_commands)
1436
2104
 
1437
2105
  serialized_flow = SerializedFlowCommands(
1438
2106
  flow_initialization_command=create_flow_request,
@@ -1442,8 +2110,7 @@ class FlowManager:
1442
2110
  set_parameter_value_commands=set_parameter_value_commands_per_node,
1443
2111
  set_lock_commands_per_node=set_lock_commands_per_node,
1444
2112
  sub_flows_commands=sub_flow_commands,
1445
- node_libraries_used=node_libraries_in_use,
1446
- referenced_workflows=referenced_workflows_in_use,
2113
+ node_dependencies=aggregated_dependencies,
1447
2114
  )
1448
2115
  details = f"Successfully serialized Flow '{flow_name}' into commands."
1449
2116
  result = SerializeFlowToCommandsResultSuccess(serialized_flow_commands=serialized_flow, result_details=details)
@@ -1608,12 +2275,16 @@ class FlowManager:
1608
2275
  # Initialize global control flow machine and DAG builder
1609
2276
 
1610
2277
  self._global_control_flow_machine = ControlFlowMachine(flow.name)
2278
+ # Set off the request here.
1611
2279
  try:
1612
2280
  await self._global_control_flow_machine.start_flow(start_node, debug_mode)
1613
2281
  except Exception:
1614
2282
  if self.check_for_existing_running_flow():
1615
2283
  self.cancel_flow_run()
1616
2284
  raise
2285
+ GriptapeNodes.EventManager().put_event(
2286
+ ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=[])))
2287
+ )
1617
2288
 
1618
2289
  def check_for_existing_running_flow(self) -> bool:
1619
2290
  if self._global_control_flow_machine is None:
@@ -1639,6 +2310,9 @@ class FlowManager:
1639
2310
  self._global_dag_builder.clear()
1640
2311
  logger.debug("Cancelling flow run")
1641
2312
 
2313
+ GriptapeNodes.EventManager().put_event(
2314
+ ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=[])))
2315
+ )
1642
2316
  GriptapeNodes.EventManager().put_event(
1643
2317
  ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=ControlFlowCancelledEvent()))
1644
2318
  )
@@ -1720,6 +2394,13 @@ class FlowManager:
1720
2394
  if self.check_for_existing_running_flow():
1721
2395
  # Now we know something is running, it's ParallelResolutionMachine, and that we are in single_node_resolution.
1722
2396
  self._global_dag_builder.add_node_with_dependencies(node, node.name)
2397
+ # Emit involved nodes update after adding node to DAG
2398
+ involved_nodes = list(self._global_dag_builder.node_to_reference.keys())
2399
+ GriptapeNodes.EventManager().put_event(
2400
+ ExecutionGriptapeNodeEvent(
2401
+ wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=involved_nodes))
2402
+ )
2403
+ )
1723
2404
  else:
1724
2405
  # Set that we are only working on one node right now!
1725
2406
  self._global_single_node_resolution = True
@@ -1733,10 +2414,29 @@ class FlowManager:
1733
2414
  if isinstance(resolution_machine, ParallelResolutionMachine):
1734
2415
  self._global_dag_builder.add_node_with_dependencies(node)
1735
2416
  resolution_machine.context.dag_builder = self._global_dag_builder
1736
- await resolution_machine.resolve_node(node)
2417
+ involved_nodes = list(self._global_dag_builder.node_to_reference.keys())
2418
+ else:
2419
+ involved_nodes = list(flow.nodes.keys())
2420
+ # Send a InvolvedNodesRequest
2421
+
2422
+ GriptapeNodes.EventManager().put_event(
2423
+ ExecutionGriptapeNodeEvent(
2424
+ wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=involved_nodes))
2425
+ )
2426
+ )
2427
+ try:
2428
+ await resolution_machine.resolve_node(node)
2429
+ except Exception as e:
2430
+ logger.exception("Exception during single node resolution")
2431
+ if self.check_for_existing_running_flow():
2432
+ self.cancel_flow_run()
2433
+ raise RuntimeError(e) from e
1737
2434
  if resolution_machine.is_complete():
1738
2435
  self._global_single_node_resolution = False
1739
2436
  self._global_control_flow_machine.context.current_nodes = []
2437
+ GriptapeNodes.EventManager().put_event(
2438
+ ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=InvolvedNodesEvent(involved_nodes=[])))
2439
+ )
1740
2440
 
1741
2441
  async def single_execution_step(self, flow: ControlFlow, change_debug_mode: bool) -> None: # noqa: FBT001
1742
2442
  # do a granular step
@@ -1794,12 +2494,11 @@ class FlowManager:
1794
2494
  # Clear entry control parameter for new execution
1795
2495
  node.set_entry_control_parameter(None)
1796
2496
 
1797
- def flow_state(self, flow: ControlFlow) -> tuple[list[str] | None, list[str] | None, list[str] | None]: # noqa: ARG002
2497
+ def flow_state(self, flow: ControlFlow) -> tuple[list[str] | None, list[str] | None]: # noqa: ARG002
1798
2498
  if not self.check_for_existing_running_flow():
1799
- msg = "Flow hasn't started."
1800
- raise RuntimeError(msg)
2499
+ return None, None
1801
2500
  if self._global_control_flow_machine is None:
1802
- return None, None, None
2501
+ return None, None
1803
2502
  control_flow_context = self._global_control_flow_machine.context
1804
2503
  current_control_nodes = (
1805
2504
  [control_flow_node.name for control_flow_node in control_flow_context.current_nodes]
@@ -1812,13 +2511,12 @@ class FlowManager:
1812
2511
  node.node_reference.name
1813
2512
  for node in control_flow_context.resolution_machine.context.task_to_node.values()
1814
2513
  ]
1815
- involved_nodes = list(self._global_dag_builder.node_to_reference.keys())
1816
- return current_control_nodes, current_resolving_nodes, involved_nodes if len(involved_nodes) != 0 else None
2514
+ return current_control_nodes, current_resolving_nodes
1817
2515
  if isinstance(control_flow_context.resolution_machine, SequentialResolutionMachine):
1818
2516
  focus_stack_for_node = control_flow_context.resolution_machine.context.focus_stack
1819
2517
  current_resolving_node = focus_stack_for_node[-1].node.name if len(focus_stack_for_node) else None
1820
- return current_control_nodes, [current_resolving_node] if current_resolving_node else None, None
1821
- return current_control_nodes, None, None
2518
+ return current_control_nodes, [current_resolving_node] if current_resolving_node else None
2519
+ return current_control_nodes, None
1822
2520
 
1823
2521
  def get_start_node_from_node(self, flow: ControlFlow, node: BaseNode) -> BaseNode | None:
1824
2522
  # backwards chain in control outputs.