griptape-nodes 0.58.1__py3-none-any.whl → 0.59.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 (30) hide show
  1. griptape_nodes/bootstrap/utils/python_subprocess_executor.py +2 -2
  2. griptape_nodes/bootstrap/workflow_executors/local_session_workflow_executor.py +0 -5
  3. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +9 -5
  4. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -1
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +1 -3
  6. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +1 -1
  7. griptape_nodes/cli/commands/init.py +53 -7
  8. griptape_nodes/cli/shared.py +1 -0
  9. griptape_nodes/common/node_executor.py +218 -42
  10. griptape_nodes/exe_types/core_types.py +46 -0
  11. griptape_nodes/exe_types/node_types.py +272 -0
  12. griptape_nodes/machines/control_flow.py +222 -16
  13. griptape_nodes/machines/dag_builder.py +212 -1
  14. griptape_nodes/machines/parallel_resolution.py +237 -4
  15. griptape_nodes/node_library/workflow_registry.py +1 -1
  16. griptape_nodes/retained_mode/events/execution_events.py +5 -4
  17. griptape_nodes/retained_mode/events/flow_events.py +17 -67
  18. griptape_nodes/retained_mode/events/parameter_events.py +122 -1
  19. griptape_nodes/retained_mode/managers/event_manager.py +17 -13
  20. griptape_nodes/retained_mode/managers/flow_manager.py +316 -573
  21. griptape_nodes/retained_mode/managers/library_manager.py +32 -20
  22. griptape_nodes/retained_mode/managers/model_manager.py +19 -8
  23. griptape_nodes/retained_mode/managers/node_manager.py +463 -3
  24. griptape_nodes/retained_mode/managers/object_manager.py +2 -2
  25. griptape_nodes/retained_mode/managers/workflow_manager.py +37 -46
  26. griptape_nodes/retained_mode/retained_mode.py +297 -3
  27. {griptape_nodes-0.58.1.dist-info → griptape_nodes-0.59.1.dist-info}/METADATA +3 -2
  28. {griptape_nodes-0.58.1.dist-info → griptape_nodes-0.59.1.dist-info}/RECORD +30 -30
  29. {griptape_nodes-0.58.1.dist-info → griptape_nodes-0.59.1.dist-info}/WHEEL +1 -1
  30. {griptape_nodes-0.58.1.dist-info → griptape_nodes-0.59.1.dist-info}/entry_points.txt +0 -0
@@ -21,6 +21,7 @@ from griptape_nodes.exe_types.node_types import (
21
21
  BaseNode,
22
22
  ErrorProxyNode,
23
23
  NodeDependencies,
24
+ NodeGroupProxyNode,
24
25
  NodeResolutionState,
25
26
  StartLoopNode,
26
27
  StartNode,
@@ -106,9 +107,6 @@ from griptape_nodes.retained_mode.events.flow_events import (
106
107
  ListNodesInFlowResultFailure,
107
108
  ListNodesInFlowResultSuccess,
108
109
  OriginalNodeParameter,
109
- PackageNodeAsSerializedFlowRequest,
110
- PackageNodeAsSerializedFlowResultFailure,
111
- PackageNodeAsSerializedFlowResultSuccess,
112
110
  PackageNodesAsSerializedFlowRequest,
113
111
  PackageNodesAsSerializedFlowResultFailure,
114
112
  PackageNodesAsSerializedFlowResultSuccess,
@@ -262,9 +260,6 @@ class FlowManager:
262
260
  event_manager.assign_manager_to_request_type(
263
261
  DeserializeFlowFromCommandsRequest, self.on_deserialize_flow_from_commands
264
262
  )
265
- event_manager.assign_manager_to_request_type(
266
- PackageNodeAsSerializedFlowRequest, self.on_package_node_as_serialized_flow_request
267
- )
268
263
  event_manager.assign_manager_to_request_type(
269
264
  PackageNodesAsSerializedFlowRequest, self.on_package_nodes_as_serialized_flow_request
270
265
  )
@@ -937,11 +932,15 @@ class FlowManager:
937
932
  if isinstance(target_param, ParameterContainer):
938
933
  target_node.kill_parameter_children(target_param)
939
934
  # Set the parameter value (including None/empty values) unless we're in initial setup
940
- # Skip propagation for Control Parameters as they should not receive values
941
- if (
942
- request.initial_setup is False
943
- and ParameterType.attempt_get_builtin(source_param.output_type) != ParameterTypeBuiltin.CONTROL_TYPE
944
- ):
935
+ # Skip propagation for:
936
+ # 1. Control Parameters as they should not receive values
937
+ # 2. Locked nodes
938
+ # 3. Initial Setup (this is used during deserialization; the downstream node may not be created yet)
939
+ is_control_parameter = (
940
+ ParameterType.attempt_get_builtin(source_param.output_type) == ParameterTypeBuiltin.CONTROL_TYPE
941
+ )
942
+ is_dest_node_locked = target_node.lock
943
+ if (not is_control_parameter) and (not is_dest_node_locked) and (not request.initial_setup):
945
944
  # When creating a connection, pass the initial value from source to target parameter
946
945
  # Set incoming_connection_source fields to identify this as legitimate connection value passing
947
946
  # (not manual property setting) so it bypasses the INPUT+PROPERTY connection blocking logic
@@ -1104,521 +1103,7 @@ class FlowManager:
1104
1103
  result = DeleteConnectionResultSuccess(result_details=details)
1105
1104
  return result
1106
1105
 
1107
- def on_package_node_as_serialized_flow_request(self, request: PackageNodeAsSerializedFlowRequest) -> ResultPayload: # noqa: PLR0911
1108
- """Handle request to package a node as a serialized flow.
1109
-
1110
- Creates a self-contained flow with Start node -> Package node -> End node structure,
1111
- where artificial start/end nodes match the package node's connections.
1112
- """
1113
- # Step 1: Validate package node and flow
1114
- package_node_info = self._validate_package_node_and_flow(request=request)
1115
- if isinstance(package_node_info, PackageNodeAsSerializedFlowResultFailure):
1116
- return package_node_info
1117
-
1118
- # Step 2: Validate library and get version
1119
- library_version = self._validate_and_get_library_info(request=request)
1120
- if isinstance(library_version, PackageNodeAsSerializedFlowResultFailure):
1121
- return library_version
1122
-
1123
- # Step 3: Analyze package node connections
1124
- connection_analysis = self._analyze_package_node_connections(
1125
- package_node=package_node_info.package_node,
1126
- node_name=package_node_info.package_node.name,
1127
- )
1128
- if isinstance(connection_analysis, PackageNodeAsSerializedFlowResultFailure):
1129
- return connection_analysis
1130
-
1131
- # Step 4: Serialize the package node
1132
- unique_parameter_uuid_to_values = {}
1133
- serialized_parameter_value_tracker = SerializedParameterValueTracker()
1134
- package_node = package_node_info.package_node
1135
- # Set to LOCAL_EXECUTION before packaging to prevent recursive loop.
1136
- previous_value = package_node.get_parameter_value("execution_environment")
1137
- package_node.set_parameter_value("execution_environment", LOCAL_EXECUTION)
1138
- serialized_package_result = self._serialize_package_node(
1139
- node_name=package_node_info.package_node.name,
1140
- package_node=package_node_info.package_node,
1141
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1142
- serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1143
- )
1144
- # Now that we've serialized the value as LOCAL_EXECUTION, we need to restore it to whatever it was before
1145
- package_node.set_parameter_value("execution_environment", previous_value)
1146
- if isinstance(serialized_package_result, PackageNodeAsSerializedFlowResultFailure):
1147
- return serialized_package_result
1148
- # Step 5: Create start node commands and data connections
1149
- start_node_result = self._create_start_node_commands(
1150
- request=request,
1151
- incoming_data_connections=connection_analysis.incoming_data_connections,
1152
- package_node=package_node_info.package_node,
1153
- package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1154
- library_version=library_version,
1155
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1156
- serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1157
- )
1158
- if isinstance(start_node_result, PackageNodeAsSerializedFlowResultFailure):
1159
- return start_node_result
1160
-
1161
- # Step 6: Create end node commands and data connections
1162
- end_node_result = self._create_end_node_commands(
1163
- request=request,
1164
- package_node=package_node_info.package_node,
1165
- package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1166
- library_version=library_version,
1167
- )
1168
- if isinstance(end_node_result, PackageNodeAsSerializedFlowResultFailure):
1169
- return end_node_result
1170
-
1171
- # Step 7a: Create start node control flow connection
1172
- start_control_connection_result = self._create_start_node_control_connection(
1173
- entry_control_parameter_name=request.entry_control_parameter_name,
1174
- start_node_uuid=start_node_result.start_node_commands.node_uuid,
1175
- package_node_uuid=serialized_package_result.serialized_node_commands.node_uuid,
1176
- package_node=package_node_info.package_node,
1177
- )
1178
- if isinstance(start_control_connection_result, PackageNodeAsSerializedFlowResultFailure):
1179
- return start_control_connection_result
1180
-
1181
- start_control_connections = [start_control_connection_result]
1182
-
1183
- # Use only start control connections for now (end node control connections not implemented yet)
1184
- control_flow_connections = start_control_connections
1185
-
1186
- # Step 8: Assemble the complete serialized flow
1187
- packaged_flow = self._assemble_serialized_flow(
1188
- serialized_package_result=serialized_package_result,
1189
- start_node_result=start_node_result,
1190
- end_node_result=end_node_result,
1191
- control_flow_connections=control_flow_connections,
1192
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1193
- library_version=library_version,
1194
- request=request,
1195
- )
1196
-
1197
- # Step 9: Build WorkflowShape from collected parameter shape data
1198
- workflow_shape = GriptapeNodes.WorkflowManager().build_workflow_shape_from_parameter_info(
1199
- input_node_params=start_node_result.input_shape_data, output_node_params=end_node_result.output_shape_data
1200
- )
1201
- # Return success result
1202
- return PackageNodeAsSerializedFlowResultSuccess(
1203
- 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}".',
1204
- serialized_flow_commands=packaged_flow,
1205
- workflow_shape=workflow_shape,
1206
- )
1207
-
1208
- def _validate_package_node_and_flow( # noqa: PLR0911
1209
- self, request: PackageNodeAsSerializedFlowRequest
1210
- ) -> PackageNodeInfo | PackageNodeAsSerializedFlowResultFailure:
1211
- """Validate and retrieve the package node and its parent flow."""
1212
- node_name = request.node_name
1213
- package_node = None
1214
-
1215
- if node_name is None:
1216
- # First check if we have a current node
1217
- if not GriptapeNodes.ContextManager().has_current_node():
1218
- details = (
1219
- "Attempted to package node from Current Context. Failed because the Current Context was empty."
1220
- )
1221
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1222
-
1223
- # Get the current node from context
1224
- package_node = GriptapeNodes.ContextManager().get_current_node()
1225
- node_name = package_node.name
1226
-
1227
- if package_node is None:
1228
- try:
1229
- package_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1230
- except ValueError as err:
1231
- details = f"Attempted to package node '{node_name}'. Failed because node does not exist. Error: {err}."
1232
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1233
-
1234
- # Get the flow containing this node using the same pattern
1235
- package_flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
1236
- if package_flow_name is None:
1237
- details = f"Attempted to package node '{node_name}'. Failed because node is not assigned to any flow."
1238
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1239
-
1240
- try:
1241
- self.get_flow_by_name(flow_name=package_flow_name)
1242
- except KeyError as err:
1243
- details = f"Attempted to package node '{node_name}' from flow '{package_flow_name}'. Failed because flow does not exist. Error: {err}."
1244
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1245
-
1246
- # Validate entry control parameter if specified
1247
- if request.entry_control_parameter_name is not None:
1248
- entry_param = package_node.get_parameter_by_name(request.entry_control_parameter_name)
1249
- if entry_param is None:
1250
- 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."
1251
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1252
-
1253
- # Verify it's actually a control parameter
1254
- if ParameterTypeBuiltin.CONTROL_TYPE.value not in entry_param.input_types:
1255
- 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."
1256
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1257
-
1258
- return PackageNodeInfo(package_node=package_node, package_flow_name=package_flow_name)
1259
-
1260
- def _validate_and_get_library_info(
1261
- self, request: PackageNodeAsSerializedFlowRequest
1262
- ) -> str | PackageNodeAsSerializedFlowResultFailure:
1263
- """Validate start/end node types exist in library and return library version."""
1264
- # Early validation - ensure both start and end node types exist in the specified library
1265
- try:
1266
- start_end_library = LibraryRegistry.get_library_for_node_type(
1267
- node_type=request.start_node_type, specific_library_name=request.start_end_specific_library_name
1268
- )
1269
- except KeyError as err:
1270
- 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}."
1271
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1272
-
1273
- try:
1274
- LibraryRegistry.get_library_for_node_type(
1275
- node_type=request.end_node_type, specific_library_name=request.start_end_specific_library_name
1276
- )
1277
- except KeyError as err:
1278
- 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}."
1279
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1280
-
1281
- # Get the actual library version
1282
- start_end_library_metadata = start_end_library.get_metadata()
1283
- return start_end_library_metadata.library_version
1284
-
1285
- def _serialize_package_node(
1286
- self,
1287
- node_name: str,
1288
- package_node: BaseNode,
1289
- unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1290
- serialized_parameter_value_tracker: SerializedParameterValueTracker,
1291
- ) -> SerializeNodeToCommandsResultSuccess | PackageNodeAsSerializedFlowResultFailure:
1292
- """Serialize the package node to commands, adding OUTPUT mode to PROPERTY-only parameters."""
1293
- # Use the provided parameter tracking structures
1294
-
1295
- # Create serialization request for the package node
1296
- serialize_node_request = SerializeNodeToCommandsRequest(
1297
- node_name=node_name,
1298
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1299
- serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1300
- )
1301
-
1302
- # Execute the serialization
1303
- serialize_node_result = GriptapeNodes.handle_request(serialize_node_request)
1304
- if not isinstance(serialize_node_result, SerializeNodeToCommandsResultSuccess):
1305
- details = f"Attempted to serialize package node '{node_name}'. Failed because node serialization failed."
1306
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1307
-
1308
- # Add ALTER parameter commands for PROPERTY-only parameters to enable OUTPUT mode
1309
- # We need these to emit their values back so that the orchestrator/caller
1310
- # can reconcile the packaged node's values after it is executed.
1311
- package_alter_parameter_commands = []
1312
- for package_param in package_node.parameters:
1313
- has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
1314
- has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
1315
-
1316
- # If has PROPERTY but not OUTPUT, add ALTER command to enable OUTPUT
1317
- if has_property_mode and not has_output_mode:
1318
- alter_param_request = AlterParameterDetailsRequest(
1319
- parameter_name=package_param.name,
1320
- node_name=package_node.name,
1321
- mode_allowed_output=True,
1322
- )
1323
- package_alter_parameter_commands.append(alter_param_request)
1324
-
1325
- # If we have alter parameter commands, append them to the existing element_modification_commands
1326
- if package_alter_parameter_commands:
1327
- serialize_node_result.serialized_node_commands.element_modification_commands.extend(
1328
- package_alter_parameter_commands
1329
- )
1330
-
1331
- return serialize_node_result
1332
-
1333
- def _create_start_node_commands( # noqa: PLR0913
1334
- self,
1335
- request: PackageNodeAsSerializedFlowRequest,
1336
- incoming_data_connections: list[IncomingConnection],
1337
- package_node: BaseNode,
1338
- package_node_uuid: SerializedNodeCommands.NodeUUID,
1339
- library_version: str,
1340
- unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1341
- serialized_parameter_value_tracker: SerializedParameterValueTracker,
1342
- ) -> PackagingStartNodeResult | PackageNodeAsSerializedFlowResultFailure:
1343
- """Create start node commands and connections for incoming data connections."""
1344
- # Generate UUID and name for start node
1345
- start_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
1346
- start_node_name = f"Start_Package_{package_node.name}"
1347
-
1348
- # Build start node CreateNodeRequest
1349
- start_create_node_command = CreateNodeRequest(
1350
- node_type=request.start_node_type,
1351
- specific_library_name=request.start_end_specific_library_name,
1352
- node_name=start_node_name,
1353
- metadata={},
1354
- initial_setup=True,
1355
- create_error_proxy_on_failure=False,
1356
- )
1357
-
1358
- # Create library details
1359
- start_node_library_details = LibraryNameAndVersion(
1360
- library_name=request.start_end_specific_library_name,
1361
- library_version=library_version,
1362
- )
1363
-
1364
- # Create parameter modification commands and connection mappings for the start node based on incoming DATA connections
1365
- start_node_parameter_commands = []
1366
- start_to_package_connections = []
1367
- start_node_parameter_value_commands = []
1368
- input_shape_data: WorkflowShapeNodes = {}
1369
-
1370
- for incoming_conn in incoming_data_connections:
1371
- # Parameter name: use the package node's parameter name
1372
- param_name = incoming_conn.target_parameter_name
1373
-
1374
- # Get the source node to determine parameter type
1375
- try:
1376
- source_node = GriptapeNodes.NodeManager().get_node_by_name(incoming_conn.source_node_name)
1377
- except ValueError as err:
1378
- 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}."
1379
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1380
-
1381
- # Get the source parameter
1382
- source_param = source_node.get_parameter_by_name(incoming_conn.source_parameter_name)
1383
- if not source_param:
1384
- 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."
1385
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1386
-
1387
- # Extract parameter shape info for workflow shape (inputs from external sources)
1388
- param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
1389
- source_param, include_control_params=True
1390
- )
1391
- if param_shape_info is not None:
1392
- if start_node_name not in input_shape_data:
1393
- input_shape_data[start_node_name] = {}
1394
- input_shape_data[start_node_name][param_name] = param_shape_info
1395
-
1396
- # Extract parameter value from source node to set on start node
1397
- param_value_commands = GriptapeNodes.NodeManager().handle_parameter_value_saving(
1398
- parameter=source_param,
1399
- node=source_node,
1400
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1401
- serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1402
- create_node_request=start_create_node_command,
1403
- )
1404
- if param_value_commands is not None:
1405
- # Modify each command to target the start node parameter instead
1406
- for param_value_command in param_value_commands:
1407
- param_value_command.set_parameter_value_command.node_name = start_node_name
1408
- param_value_command.set_parameter_value_command.parameter_name = param_name
1409
- start_node_parameter_value_commands.append(param_value_command)
1410
-
1411
- # Create parameter command for start node
1412
- add_param_request = AddParameterToNodeRequest(
1413
- node_name=start_node_name,
1414
- parameter_name=param_name,
1415
- # Use the source parameter's output_type as the type for our start node parameter
1416
- # since we want to match what the original connection was providing
1417
- type=source_param.output_type,
1418
- default_value=None,
1419
- tooltip=f"Parameter {param_name} from packaged flow",
1420
- initial_setup=True,
1421
- )
1422
- start_node_parameter_commands.append(add_param_request)
1423
-
1424
- # Create connection from start node to package node
1425
- start_to_package_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1426
- source_node_uuid=start_node_uuid,
1427
- source_parameter_name=param_name,
1428
- target_node_uuid=package_node_uuid,
1429
- target_parameter_name=param_name,
1430
- )
1431
- start_to_package_connections.append(start_to_package_connection)
1432
-
1433
- # Build complete SerializedNodeCommands for start node
1434
- start_node_dependencies = NodeDependencies()
1435
- start_node_dependencies.libraries.add(start_node_library_details)
1436
-
1437
- start_node_commands = SerializedNodeCommands(
1438
- create_node_command=start_create_node_command,
1439
- element_modification_commands=start_node_parameter_commands,
1440
- node_dependencies=start_node_dependencies,
1441
- node_uuid=start_node_uuid,
1442
- )
1443
-
1444
- return PackagingStartNodeResult(
1445
- start_node_commands=start_node_commands,
1446
- start_to_package_connections=start_to_package_connections,
1447
- input_shape_data=input_shape_data,
1448
- start_node_parameter_value_commands=start_node_parameter_value_commands,
1449
- )
1450
-
1451
- def _create_end_node_commands(
1452
- self,
1453
- request: PackageNodeAsSerializedFlowRequest,
1454
- package_node: BaseNode,
1455
- package_node_uuid: SerializedNodeCommands.NodeUUID,
1456
- library_version: str,
1457
- ) -> PackagingEndNodeResult | PackageNodeAsSerializedFlowResultFailure:
1458
- """Create end node commands and connections for ALL package parameters that meet criteria."""
1459
- # Generate UUID and name for end node
1460
- end_node_uuid = SerializedNodeCommands.NodeUUID(str(uuid4()))
1461
- end_node_name = f"End_Package_{package_node.name}"
1462
-
1463
- # Build end node CreateNodeRequest
1464
- end_create_node_command = CreateNodeRequest(
1465
- node_type=request.end_node_type,
1466
- specific_library_name=request.start_end_specific_library_name,
1467
- node_name=end_node_name,
1468
- metadata={},
1469
- initial_setup=True,
1470
- create_error_proxy_on_failure=False,
1471
- )
1472
-
1473
- # Create library details
1474
- end_node_library_details = LibraryNameAndVersion(
1475
- library_name=request.start_end_specific_library_name,
1476
- library_version=library_version,
1477
- )
1478
-
1479
- # Process ALL package node parameters to create end node parameters and connections
1480
- # Note: PROPERTY-only parameters are guaranteed to have OUTPUT mode after serialization
1481
- end_node_parameter_commands = []
1482
- package_to_end_connections = []
1483
- output_shape_data: WorkflowShapeNodes = {}
1484
-
1485
- for package_param in package_node.parameters:
1486
- # Only ignore parameters that have ONLY INPUT mode (no OUTPUT or PROPERTY)
1487
- has_output_mode = ParameterMode.OUTPUT in package_param.allowed_modes
1488
- has_property_mode = ParameterMode.PROPERTY in package_param.allowed_modes
1489
-
1490
- # Skip parameters that only have INPUT mode
1491
- if not has_output_mode and not has_property_mode:
1492
- continue
1493
-
1494
- # Create prefixed parameter name for end node to avoid collisions
1495
- end_param_name = f"{request.output_parameter_prefix}{package_param.name}"
1496
-
1497
- # Extract parameter shape info for workflow shape (outputs to external consumers)
1498
- param_shape_info = GriptapeNodes.WorkflowManager().extract_parameter_shape_info(
1499
- package_param, include_control_params=True
1500
- )
1501
- if param_shape_info is not None:
1502
- if end_node_name not in output_shape_data:
1503
- output_shape_data[end_node_name] = {}
1504
- output_shape_data[end_node_name][end_param_name] = param_shape_info
1505
-
1506
- # Create parameter command for end node
1507
- add_param_request = AddParameterToNodeRequest(
1508
- node_name=end_node_name,
1509
- parameter_name=end_param_name,
1510
- # Use the package node's output_type as the type for our end node parameter
1511
- type=package_param.output_type,
1512
- default_value=None,
1513
- tooltip=f"Output parameter {package_param.name} from packaged node {package_node.name}",
1514
- initial_setup=True,
1515
- )
1516
- end_node_parameter_commands.append(add_param_request)
1517
-
1518
- # Create connection from package node to end node
1519
- package_to_end_connection = SerializedFlowCommands.IndirectConnectionSerialization(
1520
- source_node_uuid=package_node_uuid,
1521
- source_parameter_name=package_param.name,
1522
- target_node_uuid=end_node_uuid,
1523
- target_parameter_name=end_param_name,
1524
- )
1525
- package_to_end_connections.append(package_to_end_connection)
1526
-
1527
- # Build complete SerializedNodeCommands for end node
1528
- end_node_dependencies = NodeDependencies()
1529
- end_node_dependencies.libraries.add(end_node_library_details)
1530
-
1531
- end_node_commands = SerializedNodeCommands(
1532
- create_node_command=end_create_node_command,
1533
- element_modification_commands=end_node_parameter_commands,
1534
- node_dependencies=end_node_dependencies,
1535
- node_uuid=end_node_uuid,
1536
- )
1537
-
1538
- return PackagingEndNodeResult(
1539
- end_node_commands=end_node_commands,
1540
- package_to_end_connections=package_to_end_connections,
1541
- output_shape_data=output_shape_data,
1542
- )
1543
-
1544
- def _assemble_serialized_flow( # noqa: PLR0913
1545
- self,
1546
- serialized_package_result: SerializeNodeToCommandsResultSuccess,
1547
- start_node_result: PackagingStartNodeResult,
1548
- end_node_result: PackagingEndNodeResult,
1549
- control_flow_connections: list[SerializedFlowCommands.IndirectConnectionSerialization],
1550
- unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
1551
- library_version: str,
1552
- request: PackageNodeAsSerializedFlowRequest,
1553
- ) -> SerializedFlowCommands:
1554
- """Assemble the complete SerializedFlowCommands from all components."""
1555
- # Combine all connections: Start->Package + Package->End + Control Flow
1556
- all_connections = (
1557
- start_node_result.start_to_package_connections
1558
- + end_node_result.package_to_end_connections
1559
- + control_flow_connections
1560
- )
1561
-
1562
- # Set up lock commands if needed
1563
- set_lock_commands_per_node = {}
1564
- if serialized_package_result.serialized_node_commands.lock_node_command:
1565
- set_lock_commands_per_node[serialized_package_result.serialized_node_commands.node_uuid] = (
1566
- serialized_package_result.serialized_node_commands.lock_node_command
1567
- )
1568
-
1569
- # Include all three nodes in the flow
1570
- all_serialized_nodes = [
1571
- start_node_result.start_node_commands,
1572
- serialized_package_result.serialized_node_commands,
1573
- end_node_result.end_node_commands,
1574
- ]
1575
-
1576
- # Create a CreateFlowRequest for the packaged flow so that it can
1577
- # run as a standalone workflow.
1578
- package_flow_name = (
1579
- f"Packaged_{serialized_package_result.serialized_node_commands.create_node_command.node_name}"
1580
- )
1581
- packaged_flow_metadata = {} # Keep it simple until we have reason to populate it
1582
-
1583
- create_packaged_flow_request = CreateFlowRequest(
1584
- parent_flow_name=None, # Standalone flow
1585
- flow_name=package_flow_name,
1586
- set_as_new_context=False, # Let deserializer decide
1587
- metadata=packaged_flow_metadata,
1588
- )
1589
-
1590
- # Aggregate dependencies from the packaged nodes
1591
- packaged_dependencies = self._aggregate_flow_dependencies(all_serialized_nodes, [])
1592
-
1593
- # Add the start/end specific library dependency
1594
- start_end_library_dependency = LibraryNameAndVersion(
1595
- library_name=request.start_end_specific_library_name,
1596
- library_version=library_version,
1597
- )
1598
- packaged_dependencies.libraries.add(start_end_library_dependency)
1599
-
1600
- # Aggregate node types used
1601
- packaged_node_types_used = self._aggregate_node_types_used(
1602
- serialized_node_commands=all_serialized_nodes, sub_flows_commands=[]
1603
- )
1604
-
1605
- # Build the complete SerializedFlowCommands
1606
- return SerializedFlowCommands(
1607
- flow_initialization_command=create_packaged_flow_request,
1608
- serialized_node_commands=all_serialized_nodes,
1609
- serialized_connections=all_connections,
1610
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1611
- set_parameter_value_commands={
1612
- serialized_package_result.serialized_node_commands.node_uuid: serialized_package_result.set_parameter_value_commands,
1613
- start_node_result.start_node_commands.node_uuid: start_node_result.start_node_parameter_value_commands,
1614
- },
1615
- set_lock_commands_per_node=set_lock_commands_per_node,
1616
- sub_flows_commands=[],
1617
- node_dependencies=packaged_dependencies,
1618
- node_types_used=packaged_node_types_used,
1619
- )
1620
-
1621
- def on_package_nodes_as_serialized_flow_request( # noqa: C901, PLR0911
1106
+ def on_package_nodes_as_serialized_flow_request( # noqa: C901, PLR0911, PLR0912
1622
1107
  self, request: PackageNodesAsSerializedFlowRequest
1623
1108
  ) -> ResultPayload:
1624
1109
  """Handle request to package multiple nodes as a serialized flow.
@@ -1626,6 +1111,12 @@ class FlowManager:
1626
1111
  Creates a self-contained flow with Start → [Selected Nodes] → End structure,
1627
1112
  where artificial start/end nodes interface with external connections only.
1628
1113
  """
1114
+ # Step 0: Apply defaults for None values
1115
+ if request.start_node_type is None:
1116
+ request.start_node_type = "StartFlow"
1117
+ if request.end_node_type is None:
1118
+ request.end_node_type = "EndFlow"
1119
+
1629
1120
  # Step 1: Reject empty node list
1630
1121
  if not request.node_names:
1631
1122
  return PackageNodesAsSerializedFlowResultFailure(
@@ -1665,6 +1156,7 @@ class FlowManager:
1665
1156
  node_name_to_uuid=node_name_to_uuid,
1666
1157
  set_parameter_value_commands=packaged_nodes_set_parameter_value_commands,
1667
1158
  internal_connections=packaged_nodes_internal_connections,
1159
+ proxy_node=request.proxy_node,
1668
1160
  )
1669
1161
  if isinstance(serialized_package_nodes, PackageNodesAsSerializedFlowResultFailure):
1670
1162
  return serialized_package_nodes
@@ -1678,7 +1170,9 @@ class FlowManager:
1678
1170
  self._inject_output_mode_for_property_parameters(nodes_to_package, serialized_package_nodes)
1679
1171
 
1680
1172
  # Step 7: Analyze external connections (connections from/to nodes outside our selection)
1681
- node_connections_dict = self._analyze_multi_node_external_connections(package_nodes=nodes_to_package)
1173
+ node_connections_dict = self._analyze_multi_node_external_connections(
1174
+ package_nodes=nodes_to_package, proxy_node=request.proxy_node
1175
+ )
1682
1176
  if isinstance(node_connections_dict, PackageNodesAsSerializedFlowResultFailure):
1683
1177
  return node_connections_dict
1684
1178
 
@@ -1789,7 +1283,8 @@ class FlowManager:
1789
1283
  # Early validation - ensure both start and end node types exist in the specified library
1790
1284
  try:
1791
1285
  start_end_library = LibraryRegistry.get_library_for_node_type(
1792
- node_type=request.start_node_type, specific_library_name=request.start_end_specific_library_name
1286
+ node_type=request.start_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
1287
+ specific_library_name=request.start_end_specific_library_name,
1793
1288
  )
1794
1289
  except KeyError as err:
1795
1290
  details = f"Attempted to package nodes 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}."
@@ -1797,7 +1292,8 @@ class FlowManager:
1797
1292
 
1798
1293
  try:
1799
1294
  LibraryRegistry.get_library_for_node_type(
1800
- node_type=request.end_node_type, specific_library_name=request.start_end_specific_library_name
1295
+ node_type=request.end_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
1296
+ specific_library_name=request.start_end_specific_library_name,
1801
1297
  )
1802
1298
  except KeyError as err:
1803
1299
  details = f"Attempted to package nodes 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}."
@@ -1838,7 +1334,7 @@ class FlowManager:
1838
1334
 
1839
1335
  return None
1840
1336
 
1841
- def _serialize_package_nodes_for_local_execution( # noqa: C901, PLR0913
1337
+ def _serialize_package_nodes_for_local_execution( # noqa: PLR0913
1842
1338
  self,
1843
1339
  nodes_to_package: list[BaseNode],
1844
1340
  unique_parameter_uuid_to_values: dict[SerializedNodeCommands.UniqueParameterValueUUID, Any],
@@ -1848,6 +1344,7 @@ class FlowManager:
1848
1344
  SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
1849
1345
  ],
1850
1346
  internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization], # OUTPUT: will be populated
1347
+ proxy_node: NodeGroupProxyNode | None = None, # NodeGroupProxyNode if packaging nodes from a proxy
1851
1348
  ) -> list[SerializedNodeCommands] | PackageNodesAsSerializedFlowResultFailure:
1852
1349
  """Serialize package nodes while temporarily setting execution environment to local to prevent recursive loops.
1853
1350
 
@@ -1862,17 +1359,26 @@ class FlowManager:
1862
1359
  node_name_to_uuid: OUTPUT - Dictionary mapping node names to UUIDs (populated by this method)
1863
1360
  set_parameter_value_commands: OUTPUT - Dict mapping node UUIDs to parameter value commands (populated by this method)
1864
1361
  internal_connections: OUTPUT - List of connections between package nodes (populated by this method)
1362
+ proxy_node: NodeGroupProxyNode if packaging nodes from a proxy, provides access to original connections
1363
+ stored before they were redirected to the proxy
1865
1364
 
1866
1365
  Returns:
1867
1366
  List of SerializedNodeCommands on success, or PackageNodesAsSerializedFlowResultFailure on failure
1868
1367
  """
1869
- # Intercept execution_environment for all nodes before serialization
1368
+ # Intercept execution_environment and job_group for all nodes before serialization
1870
1369
  original_execution_environments = {}
1370
+ original_job_groups = {}
1871
1371
  for node in nodes_to_package:
1872
- original_value = node.get_parameter_value("execution_environment")
1873
- original_execution_environments[node.name] = original_value
1372
+ # Save and override execution_environment to prevent recursive packaging loops
1373
+ original_exec_value = node.get_parameter_value("execution_environment")
1374
+ original_execution_environments[node.name] = original_exec_value
1874
1375
  node.set_parameter_value("execution_environment", LOCAL_EXECUTION)
1875
1376
 
1377
+ # Save and clear job_group to prevent group processing in packaged flow
1378
+ original_job_group_value = node.get_parameter_value("job_group")
1379
+ original_job_groups[node.name] = original_job_group_value
1380
+ node.set_parameter_value("job_group", "")
1381
+
1876
1382
  try:
1877
1383
  # Serialize each node using shared unique_parameter_uuid_to_values dictionary for deduplication
1878
1384
  serialized_node_commands = []
@@ -1908,8 +1414,134 @@ class FlowManager:
1908
1414
 
1909
1415
  # Build internal connections between package nodes
1910
1416
  package_node_names_set = {n.name for n in nodes_to_package}
1417
+
1418
+ # Get connections using appropriate method based on whether we have a proxy node
1419
+ connections_result = self._get_internal_connections_for_package(
1420
+ nodes_to_package=nodes_to_package,
1421
+ package_node_names_set=package_node_names_set,
1422
+ node_name_to_uuid=node_name_to_uuid,
1423
+ proxy_node=proxy_node,
1424
+ )
1425
+
1426
+ if isinstance(connections_result, PackageNodesAsSerializedFlowResultFailure):
1427
+ return connections_result
1428
+
1429
+ internal_connections.extend(connections_result)
1430
+ finally:
1431
+ # Always restore original execution_environment and job_group values, even on failure
1432
+ for node_name, original_value in original_execution_environments.items():
1433
+ restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1434
+ restore_node.set_parameter_value("execution_environment", original_value)
1435
+
1436
+ for node_name, original_job_group in original_job_groups.items():
1437
+ restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1438
+ restore_node.set_parameter_value("job_group", original_job_group)
1439
+
1440
+ return serialized_node_commands
1441
+
1442
+ def _get_internal_connections_for_package( # noqa: C901, PLR0912
1443
+ self,
1444
+ nodes_to_package: list[BaseNode],
1445
+ package_node_names_set: set[str],
1446
+ node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
1447
+ proxy_node: NodeGroupProxyNode | None,
1448
+ ) -> list[SerializedFlowCommands.IndirectConnectionSerialization] | PackageNodesAsSerializedFlowResultFailure:
1449
+ """Get internal connections between package nodes.
1450
+
1451
+ If a proxy_node is provided, uses the stored internal_connections from the NodeGroup
1452
+ which preserve the original connection structure before redirection to the proxy.
1453
+ Otherwise, queries connections from the connection manager.
1454
+
1455
+ Args:
1456
+ nodes_to_package: List of nodes being packaged
1457
+ package_node_names_set: Set of node names in the package for O(1) lookup
1458
+ node_name_to_uuid: Mapping of node names to their UUIDs in the serialization
1459
+ proxy_node: Optional proxy node containing the original connection structure
1460
+
1461
+ Returns:
1462
+ List of serialized connections, or PackageNodesAsSerializedFlowResultFailure on error
1463
+ """
1464
+ internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization] = []
1465
+
1466
+ if proxy_node is not None and hasattr(proxy_node, "node_group_data"):
1467
+ # Use stored connections from NodeGroup which have the original node references
1468
+ node_group = proxy_node.node_group_data
1469
+
1470
+ # 1. Add internal connections (between nodes inside the group)
1471
+ for conn in node_group.internal_connections:
1472
+ source_node_name = conn.source_node.name
1473
+ target_node_name = conn.target_node.name
1474
+
1475
+ # Only include connections where BOTH nodes are in our package
1476
+ if source_node_name in package_node_names_set and target_node_name in package_node_names_set:
1477
+ source_uuid = node_name_to_uuid[source_node_name]
1478
+ target_uuid = node_name_to_uuid[target_node_name]
1479
+ internal_connections.append(
1480
+ SerializedFlowCommands.IndirectConnectionSerialization(
1481
+ source_node_uuid=source_uuid,
1482
+ source_parameter_name=conn.source_parameter.name,
1483
+ target_node_uuid=target_uuid,
1484
+ target_parameter_name=conn.target_parameter.name,
1485
+ )
1486
+ )
1487
+
1488
+ # 2. Add external incoming connections (from outside into the group)
1489
+ # These connections have been redirected to point TO the proxy, but we want the original targets
1490
+ for conn in node_group.external_incoming_connections:
1491
+ conn_id = id(conn)
1492
+ original_target = node_group.original_incoming_targets.get(conn_id)
1493
+
1494
+ if original_target and original_target.name in package_node_names_set:
1495
+ # The source is outside the package, target is inside
1496
+ # We include these because the source will be external (like StartFlow)
1497
+ source_node_name = conn.source_node.name
1498
+ target_node_name = original_target.name
1499
+
1500
+ # Only include if source is NOT in package (external) and target IS in package
1501
+ if source_node_name not in package_node_names_set:
1502
+ source_uuid = node_name_to_uuid.get(source_node_name)
1503
+ target_uuid = node_name_to_uuid[target_node_name]
1504
+
1505
+ # Source might not have a UUID if it's external to the package
1506
+ if source_uuid:
1507
+ internal_connections.append(
1508
+ SerializedFlowCommands.IndirectConnectionSerialization(
1509
+ source_node_uuid=source_uuid,
1510
+ source_parameter_name=conn.source_parameter.name,
1511
+ target_node_uuid=target_uuid,
1512
+ target_parameter_name=conn.target_parameter.name,
1513
+ )
1514
+ )
1515
+
1516
+ # 3. Add external outgoing connections (from the group to outside)
1517
+ # These connections have been redirected to point FROM the proxy, but we want the original sources
1518
+ for conn in node_group.external_outgoing_connections:
1519
+ conn_id = id(conn)
1520
+ original_source = node_group.original_outgoing_sources.get(conn_id)
1521
+
1522
+ if original_source and original_source.name in package_node_names_set:
1523
+ # The source is inside the package, target is outside
1524
+ source_node_name = original_source.name
1525
+ target_node_name = conn.target_node.name
1526
+
1527
+ # Only include if source IS in package and target is NOT in package (external)
1528
+ if target_node_name not in package_node_names_set:
1529
+ source_uuid = node_name_to_uuid[source_node_name]
1530
+ target_uuid = node_name_to_uuid.get(target_node_name)
1531
+
1532
+ # Target might not have a UUID if it's external to the package
1533
+ if target_uuid:
1534
+ internal_connections.append(
1535
+ SerializedFlowCommands.IndirectConnectionSerialization(
1536
+ source_node_uuid=source_uuid,
1537
+ source_parameter_name=conn.source_parameter.name,
1538
+ target_node_uuid=target_uuid,
1539
+ target_parameter_name=conn.target_parameter.name,
1540
+ )
1541
+ )
1542
+ else:
1543
+ # No proxy node - query connections from connection manager
1911
1544
  for node in nodes_to_package:
1912
- # Get connections FROM this node TO other nodes in the package
1913
1545
  list_connections_request = ListConnectionsForNodeRequest(node_name=node.name)
1914
1546
  list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1915
1547
  list_connections_request
@@ -1933,13 +1565,8 @@ class FlowManager:
1933
1565
  target_parameter_name=outgoing_conn.target_parameter_name,
1934
1566
  )
1935
1567
  )
1936
- finally:
1937
- # Always restore original execution_environment values, even on failure
1938
- for node_name, original_value in original_execution_environments.items():
1939
- restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1940
- restore_node.set_parameter_value("execution_environment", original_value)
1941
1568
 
1942
- return serialized_node_commands
1569
+ return internal_connections
1943
1570
 
1944
1571
  def _inject_output_mode_for_property_parameters(
1945
1572
  self, nodes_to_package: list[BaseNode], serialized_package_nodes: list[SerializedNodeCommands]
@@ -1988,7 +1615,7 @@ class FlowManager:
1988
1615
  serialized_node.element_modification_commands.extend(package_alter_parameter_commands)
1989
1616
 
1990
1617
  def _analyze_multi_node_external_connections(
1991
- self, package_nodes: list[BaseNode]
1618
+ self, package_nodes: list[BaseNode], proxy_node: NodeGroupProxyNode | None = None
1992
1619
  ) -> dict[str, ConnectionAnalysis] | PackageNodesAsSerializedFlowResultFailure:
1993
1620
  """Analyze external connections for each package node using filtered single-node analysis.
1994
1621
 
@@ -2004,6 +1631,7 @@ class FlowManager:
2004
1631
 
2005
1632
  Args:
2006
1633
  package_nodes: List of nodes being packaged together
1634
+ proxy_node: Optional proxy node containing the original connection structure
2007
1635
 
2008
1636
  Returns:
2009
1637
  Dictionary mapping node_name -> ConnectionAnalysis, where each ConnectionAnalysis
@@ -2018,32 +1646,126 @@ class FlowManager:
2018
1646
  package_node=package_node,
2019
1647
  node_name=package_node.name,
2020
1648
  package_node_names=package_node_names_set,
1649
+ proxy_node=proxy_node,
2021
1650
  )
2022
- if isinstance(connection_analysis, PackageNodeAsSerializedFlowResultFailure):
1651
+ if isinstance(connection_analysis, PackageNodesAsSerializedFlowResultFailure):
2023
1652
  return PackageNodesAsSerializedFlowResultFailure(result_details=connection_analysis.result_details)
2024
1653
 
2025
1654
  node_connections[package_node.name] = connection_analysis
2026
1655
 
2027
1656
  return node_connections
2028
1657
 
2029
- def _analyze_package_node_connections(
2030
- self, package_node: BaseNode, node_name: str, package_node_names: set[str] | None = None
2031
- ) -> ConnectionAnalysis | PackageNodeAsSerializedFlowResultFailure:
2032
- """Analyze package node connections and separate control from data connections."""
2033
- # Get connection details using the efficient approach
2034
- list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
2035
- list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
2036
- list_connections_request
1658
+ def _get_node_connections_from_proxy(
1659
+ self, node_name: str, proxy_node: NodeGroupProxyNode
1660
+ ) -> tuple[list[IncomingConnection], list[OutgoingConnection]]:
1661
+ """Extract incoming and outgoing connections for a specific node from the proxy node's stored data.
1662
+
1663
+ Returns connections in the same format as ListConnectionsForNodeRequest would return them,
1664
+ using the original node references from before proxy redirection.
1665
+
1666
+ Args:
1667
+ node_name: Name of the node to get connections for
1668
+ proxy_node: The proxy node containing the NodeGroup data with stored connections
1669
+
1670
+ Returns:
1671
+ Tuple of (incoming_connections, outgoing_connections) matching the ListConnectionsForNodeResultSuccess format
1672
+ """
1673
+ node_group = proxy_node.node_group_data
1674
+ incoming_connections: list[IncomingConnection] = []
1675
+ outgoing_connections: list[OutgoingConnection] = []
1676
+
1677
+ # Get incoming connections: check internal_connections and external_incoming_connections
1678
+ # Internal connections where this node is the target
1679
+ incoming_connections.extend(
1680
+ IncomingConnection(
1681
+ source_node_name=conn.source_node.name,
1682
+ source_parameter_name=conn.source_parameter.name,
1683
+ target_parameter_name=conn.target_parameter.name,
1684
+ )
1685
+ for conn in node_group.internal_connections
1686
+ if conn.target_node.name == node_name
1687
+ )
1688
+
1689
+ # External incoming connections where this node is the original target
1690
+ incoming_connections.extend(
1691
+ IncomingConnection(
1692
+ source_node_name=conn.source_node.name,
1693
+ source_parameter_name=conn.source_parameter.name,
1694
+ target_parameter_name=conn.target_parameter.name,
1695
+ )
1696
+ for conn in node_group.external_incoming_connections
1697
+ if (original_target := node_group.original_incoming_targets.get(id(conn)))
1698
+ and original_target.name == node_name
1699
+ )
1700
+
1701
+ # Get outgoing connections: check internal_connections and external_outgoing_connections
1702
+ # Internal connections where this node is the source
1703
+ outgoing_connections.extend(
1704
+ OutgoingConnection(
1705
+ source_parameter_name=conn.source_parameter.name,
1706
+ target_node_name=conn.target_node.name,
1707
+ target_parameter_name=conn.target_parameter.name,
1708
+ )
1709
+ for conn in node_group.internal_connections
1710
+ if conn.source_node.name == node_name
2037
1711
  )
2038
1712
 
2039
- if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
2040
- details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
2041
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
1713
+ # External outgoing connections where this node is the original source
1714
+ outgoing_connections.extend(
1715
+ OutgoingConnection(
1716
+ source_parameter_name=conn.source_parameter.name,
1717
+ target_node_name=conn.target_node.name,
1718
+ target_parameter_name=conn.target_parameter.name,
1719
+ )
1720
+ for conn in node_group.external_outgoing_connections
1721
+ if (original_source := node_group.original_outgoing_sources.get(id(conn)))
1722
+ and original_source.name == node_name
1723
+ )
1724
+
1725
+ return incoming_connections, outgoing_connections
1726
+
1727
+ def _analyze_package_node_connections(
1728
+ self,
1729
+ package_node: BaseNode,
1730
+ node_name: str,
1731
+ package_node_names: set[str] | None = None,
1732
+ proxy_node: NodeGroupProxyNode | None = None,
1733
+ ) -> ConnectionAnalysis | PackageNodesAsSerializedFlowResultFailure:
1734
+ """Analyze package node connections and separate control from data connections.
1735
+
1736
+ If a proxy_node is provided, uses the stored connections from the NodeGroup which preserve
1737
+ the original connection structure before proxy redirection.
1738
+
1739
+ Args:
1740
+ package_node: The node being analyzed
1741
+ node_name: Name of the node
1742
+ package_node_names: Set of node names in the package for filtering internal connections
1743
+ proxy_node: Optional proxy node containing original connection structure
1744
+ """
1745
+ # Get incoming and outgoing connections for this node
1746
+ if proxy_node is not None and hasattr(proxy_node, "node_group_data"):
1747
+ # Use stored connections from proxy which have the original node references
1748
+ incoming_connections, outgoing_connections = self._get_node_connections_from_proxy(
1749
+ node_name=node_name, proxy_node=proxy_node
1750
+ )
1751
+ else:
1752
+ # Get connection details using the standard approach
1753
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
1754
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1755
+ list_connections_request
1756
+ )
1757
+
1758
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1759
+ details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
1760
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1761
+
1762
+ incoming_connections = list_connections_result.incoming_connections
1763
+ outgoing_connections = list_connections_result.outgoing_connections
2042
1764
 
2043
1765
  # Separate control connections from data connections based on package node's parameter types
2044
1766
  incoming_data_connections = []
2045
1767
  incoming_control_connections = []
2046
- for incoming_conn in list_connections_result.incoming_connections:
1768
+ for incoming_conn in incoming_connections:
2047
1769
  # Filter out internal connections if package_node_names is provided
2048
1770
  if package_node_names is not None and incoming_conn.source_node_name in package_node_names:
2049
1771
  continue
@@ -2057,7 +1779,7 @@ class FlowManager:
2057
1779
 
2058
1780
  outgoing_data_connections = []
2059
1781
  outgoing_control_connections = []
2060
- for outgoing_conn in list_connections_result.outgoing_connections:
1782
+ for outgoing_conn in outgoing_connections:
2061
1783
  # Filter out internal connections if package_node_names is provided
2062
1784
  if package_node_names is not None and outgoing_conn.target_node_name in package_node_names:
2063
1785
  continue
@@ -2091,7 +1813,7 @@ class FlowManager:
2091
1813
 
2092
1814
  # Build end node CreateNodeRequest
2093
1815
  end_create_node_command = CreateNodeRequest(
2094
- node_type=request.end_node_type,
1816
+ node_type=request.end_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
2095
1817
  specific_library_name=request.start_end_specific_library_name,
2096
1818
  node_name=end_node_name,
2097
1819
  metadata={},
@@ -2344,7 +2066,7 @@ class FlowManager:
2344
2066
 
2345
2067
  # Build start node CreateNodeRequest
2346
2068
  start_create_node_command = CreateNodeRequest(
2347
- node_type=request.start_node_type,
2069
+ node_type=request.start_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
2348
2070
  specific_library_name=request.start_end_specific_library_name,
2349
2071
  node_name=start_node_name,
2350
2072
  metadata={},
@@ -2555,7 +2277,7 @@ class FlowManager:
2555
2277
  package_node_uuid=entry_node_uuid,
2556
2278
  package_node=entry_node,
2557
2279
  )
2558
- if isinstance(control_connection_result, PackageNodeAsSerializedFlowResultFailure):
2280
+ if isinstance(control_connection_result, PackageNodesAsSerializedFlowResultFailure):
2559
2281
  return PackageNodesAsSerializedFlowResultFailure(
2560
2282
  result_details=control_connection_result.result_details
2561
2283
  )
@@ -2570,7 +2292,7 @@ class FlowManager:
2570
2292
  start_node_uuid: SerializedNodeCommands.NodeUUID,
2571
2293
  package_node_uuid: SerializedNodeCommands.NodeUUID,
2572
2294
  package_node: BaseNode,
2573
- ) -> SerializedFlowCommands.IndirectConnectionSerialization | PackageNodeAsSerializedFlowResultFailure:
2295
+ ) -> SerializedFlowCommands.IndirectConnectionSerialization | PackageNodesAsSerializedFlowResultFailure:
2574
2296
  """Create control flow connection from start node to package node.
2575
2297
 
2576
2298
  Connects the start node's first control output to the specified or first available package node control input.
@@ -2594,7 +2316,7 @@ class FlowManager:
2594
2316
 
2595
2317
  if package_control_input_name is None:
2596
2318
  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."
2597
- return PackageNodeAsSerializedFlowResultFailure(result_details=details)
2319
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
2598
2320
 
2599
2321
  # StartNode always has a control output parameter with name "exec_out"
2600
2322
  source_control_parameter_name = "exec_out"
@@ -2740,7 +2462,7 @@ class FlowManager:
2740
2462
  details = f"Could not get flow state. Error: {err}"
2741
2463
  return GetFlowStateResultFailure(result_details=details)
2742
2464
  try:
2743
- control_nodes, resolving_nodes = self.flow_state(flow)
2465
+ control_nodes, resolving_nodes, involved_nodes = self.flow_state(flow)
2744
2466
  except Exception as e:
2745
2467
  details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
2746
2468
  logger.exception(details)
@@ -2748,7 +2470,8 @@ class FlowManager:
2748
2470
  details = f"Successfully got flow state for flow with name {flow_name}."
2749
2471
  return GetFlowStateResultSuccess(
2750
2472
  control_nodes=control_nodes,
2751
- resolving_node=resolving_nodes,
2473
+ resolving_nodes=resolving_nodes,
2474
+ involved_nodes=involved_nodes,
2752
2475
  result_details=details,
2753
2476
  )
2754
2477
 
@@ -3318,9 +3041,11 @@ class FlowManager:
3318
3041
  )
3319
3042
  # Set off the request here.
3320
3043
  try:
3321
- await self._global_control_flow_machine.start_flow(start_node, debug_mode)
3044
+ await self._global_control_flow_machine.start_flow(start_node, debug_mode=debug_mode)
3322
3045
  except Exception:
3323
3046
  if self.check_for_existing_running_flow():
3047
+ # Cleanup proxy nodes before canceling flow
3048
+ self._global_control_flow_machine.cleanup_proxy_nodes()
3324
3049
  await self.cancel_flow_run()
3325
3050
  raise
3326
3051
  GriptapeNodes.EventManager().put_event(
@@ -3349,6 +3074,10 @@ class FlowManager:
3349
3074
  if self._global_control_flow_machine is not None:
3350
3075
  await self._global_control_flow_machine.cancel_flow()
3351
3076
 
3077
+ # Cleanup proxy nodes and restore connections
3078
+ if self._global_control_flow_machine is not None:
3079
+ self._global_control_flow_machine.cleanup_proxy_nodes()
3080
+
3352
3081
  # Reset control flow machine
3353
3082
  if self._global_control_flow_machine is not None:
3354
3083
  self._global_control_flow_machine.reset_machine(cancel=True)
@@ -3366,8 +3095,12 @@ class FlowManager:
3366
3095
  def reset_global_execution_state(self) -> None:
3367
3096
  """Reset all global execution state - useful when clearing all workflows."""
3368
3097
  self._global_flow_queue.queue.clear()
3098
+
3099
+ # Cleanup proxy nodes and restore connections before resetting machine
3369
3100
  if self._global_control_flow_machine is not None:
3101
+ self._global_control_flow_machine.cleanup_proxy_nodes()
3370
3102
  self._global_control_flow_machine.reset_machine()
3103
+
3371
3104
  # Reset control flow machine
3372
3105
  self._global_single_node_resolution = False
3373
3106
 
@@ -3423,7 +3156,7 @@ class FlowManager:
3423
3156
  # Get or create machine
3424
3157
  if self._global_control_flow_machine is None:
3425
3158
  self._global_control_flow_machine = ControlFlowMachine(flow.name)
3426
- await self._global_control_flow_machine.start_flow(start_node, debug_mode)
3159
+ await self._global_control_flow_machine.start_flow(start_node, debug_mode=debug_mode)
3427
3160
 
3428
3161
  async def _handle_post_execution_queue_processing(self, *, debug_mode: bool) -> None:
3429
3162
  """Handle execution queue processing after execution completes."""
@@ -3433,7 +3166,7 @@ class FlowManager:
3433
3166
  self._global_flow_queue.task_done()
3434
3167
  machine = self._global_control_flow_machine
3435
3168
  if machine is not None:
3436
- await machine.start_flow(start_node, debug_mode)
3169
+ await machine.start_flow(start_node, debug_mode=debug_mode)
3437
3170
 
3438
3171
  async def resolve_singular_node(self, flow: ControlFlow, node: BaseNode, *, debug_mode: bool = False) -> None:
3439
3172
  # We are now going to have different behavior depending on how the node is behaving.
@@ -3471,10 +3204,14 @@ class FlowManager:
3471
3204
  )
3472
3205
  )
3473
3206
  try:
3474
- await resolution_machine.resolve_node(node)
3207
+ await self._global_control_flow_machine.start_flow(
3208
+ start_node=node, end_node=node, debug_mode=debug_mode
3209
+ )
3475
3210
  except Exception as e:
3476
3211
  logger.exception("Exception during single node resolution")
3477
3212
  if self.check_for_existing_running_flow():
3213
+ if self._global_control_flow_machine is not None:
3214
+ self._global_control_flow_machine.cleanup_proxy_nodes()
3478
3215
  await self.cancel_flow_run()
3479
3216
  raise RuntimeError(e) from e
3480
3217
  if resolution_machine.is_complete():
@@ -3540,29 +3277,35 @@ class FlowManager:
3540
3277
  # Clear entry control parameter for new execution
3541
3278
  node.set_entry_control_parameter(None)
3542
3279
 
3543
- def flow_state(self, flow: ControlFlow) -> tuple[list[str] | None, list[str] | None]: # noqa: ARG002
3280
+ def flow_state(self, flow: ControlFlow) -> tuple[list[str], list[str], list[str]]:
3544
3281
  if not self.check_for_existing_running_flow():
3545
- return None, None
3282
+ return [], [], []
3546
3283
  if self._global_control_flow_machine is None:
3547
- return None, None
3284
+ return [], [], []
3548
3285
  control_flow_context = self._global_control_flow_machine.context
3549
3286
  current_control_nodes = (
3550
3287
  [control_flow_node.name for control_flow_node in control_flow_context.current_nodes]
3551
3288
  if control_flow_context.current_nodes is not None
3552
- else None
3289
+ else []
3553
3290
  )
3291
+ if self._global_single_node_resolution and isinstance(
3292
+ control_flow_context.resolution_machine, ParallelResolutionMachine
3293
+ ):
3294
+ involved_nodes = list(self._global_dag_builder.node_to_reference.keys())
3295
+ else:
3296
+ involved_nodes = list(flow.nodes.keys())
3554
3297
  # focus_stack is no longer available in the new architecture
3555
3298
  if isinstance(control_flow_context.resolution_machine, ParallelResolutionMachine):
3556
3299
  current_resolving_nodes = [
3557
3300
  node.node_reference.name
3558
3301
  for node in control_flow_context.resolution_machine.context.task_to_node.values()
3559
3302
  ]
3560
- return current_control_nodes, current_resolving_nodes
3303
+ return current_control_nodes, current_resolving_nodes, involved_nodes
3561
3304
  if isinstance(control_flow_context.resolution_machine, SequentialResolutionMachine):
3562
3305
  focus_stack_for_node = control_flow_context.resolution_machine.context.focus_stack
3563
3306
  current_resolving_node = focus_stack_for_node[-1].node.name if len(focus_stack_for_node) else None
3564
- return current_control_nodes, [current_resolving_node] if current_resolving_node else None
3565
- return current_control_nodes, None
3307
+ return current_control_nodes, [current_resolving_node] if current_resolving_node else [], involved_nodes
3308
+ return current_control_nodes, [], involved_nodes
3566
3309
 
3567
3310
  def get_start_node_from_node(self, flow: ControlFlow, node: BaseNode) -> BaseNode | None:
3568
3311
  # backwards chain in control outputs.