griptape-nodes 0.63.10__py3-none-any.whl → 0.64.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. griptape_nodes/common/node_executor.py +95 -171
  2. griptape_nodes/exe_types/connections.py +51 -2
  3. griptape_nodes/exe_types/flow.py +3 -3
  4. griptape_nodes/exe_types/node_types.py +330 -202
  5. griptape_nodes/exe_types/param_components/artifact_url/__init__.py +1 -0
  6. griptape_nodes/exe_types/param_components/artifact_url/public_artifact_url_parameter.py +155 -0
  7. griptape_nodes/exe_types/param_components/progress_bar_component.py +1 -1
  8. griptape_nodes/exe_types/param_types/parameter_string.py +27 -0
  9. griptape_nodes/machines/control_flow.py +64 -203
  10. griptape_nodes/machines/dag_builder.py +85 -238
  11. griptape_nodes/machines/parallel_resolution.py +9 -236
  12. griptape_nodes/machines/sequential_resolution.py +133 -11
  13. griptape_nodes/retained_mode/events/agent_events.py +2 -0
  14. griptape_nodes/retained_mode/events/flow_events.py +5 -6
  15. griptape_nodes/retained_mode/events/node_events.py +151 -1
  16. griptape_nodes/retained_mode/events/workflow_events.py +10 -0
  17. griptape_nodes/retained_mode/managers/agent_manager.py +33 -1
  18. griptape_nodes/retained_mode/managers/flow_manager.py +213 -290
  19. griptape_nodes/retained_mode/managers/library_manager.py +24 -7
  20. griptape_nodes/retained_mode/managers/node_manager.py +391 -77
  21. griptape_nodes/retained_mode/managers/workflow_manager.py +45 -10
  22. griptape_nodes/servers/mcp.py +32 -0
  23. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.0.dist-info}/METADATA +3 -1
  24. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.0.dist-info}/RECORD +26 -24
  25. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.0.dist-info}/WHEEL +0 -0
  26. {griptape_nodes-0.63.10.dist-info → griptape_nodes-0.64.0.dist-info}/entry_points.txt +0 -0
@@ -17,11 +17,10 @@ from griptape_nodes.exe_types.core_types import (
17
17
  )
18
18
  from griptape_nodes.exe_types.flow import ControlFlow
19
19
  from griptape_nodes.exe_types.node_types import (
20
- LOCAL_EXECUTION,
21
20
  BaseNode,
22
21
  ErrorProxyNode,
23
22
  NodeDependencies,
24
- NodeGroupProxyNode,
23
+ NodeGroupNode,
25
24
  NodeResolutionState,
26
25
  StartLoopNode,
27
26
  StartNode,
@@ -121,6 +120,7 @@ from griptape_nodes.retained_mode.events.flow_events import (
121
120
  SetFlowMetadataResultSuccess,
122
121
  )
123
122
  from griptape_nodes.retained_mode.events.node_events import (
123
+ CreateNodeGroupRequest,
124
124
  CreateNodeRequest,
125
125
  DeleteNodeRequest,
126
126
  DeleteNodeResultFailure,
@@ -876,12 +876,13 @@ class FlowManager:
876
876
  details = f"Deleted the previous connection from '{old_source_node_name}.{old_source_param_name}' to '{old_target_node_name}.{old_target_param_name}' to make room for the new connection."
877
877
  try:
878
878
  # Actually create the Connection.
879
- self._connections.add_connection(
879
+ conn = self._connections.add_connection(
880
880
  source_node=source_node,
881
881
  source_parameter=source_param,
882
882
  target_node=target_node,
883
883
  target_parameter=target_param,
884
884
  )
885
+ conn_id = id(conn)
885
886
  except ValueError as e:
886
887
  details = f'Connection failed: "{e}"'
887
888
 
@@ -916,6 +917,33 @@ class FlowManager:
916
917
  target_parameter=target_param,
917
918
  )
918
919
 
920
+ # Check if either node is in a NodeGroup and track connections
921
+
922
+ source_parent = source_node.parent_group
923
+ target_parent = target_node.parent_group
924
+
925
+ # If source is in a group, this is an outgoing external connection
926
+ if source_parent is not None and isinstance(source_parent, NodeGroupNode) and target_parent != source_parent:
927
+ source_parent.track_external_connection(
928
+ conn=conn,
929
+ conn_id=conn_id,
930
+ is_incoming=False,
931
+ grouped_node=source_node,
932
+ )
933
+
934
+ # If target is in a group, this is an incoming external connection
935
+ if target_parent is not None and isinstance(target_parent, NodeGroupNode) and source_parent != target_parent:
936
+ target_parent.track_external_connection(
937
+ conn=conn,
938
+ conn_id=conn_id,
939
+ is_incoming=True,
940
+ grouped_node=target_node,
941
+ )
942
+
943
+ # If both in same group, track as internal connection
944
+ if source_parent is not None and source_parent == target_parent and isinstance(source_parent, NodeGroupNode):
945
+ source_parent.track_internal_connection(conn)
946
+
919
947
  details = f'Connected "{source_node_name}.{request.source_parameter_name}" to "{target_node_name}.{request.target_parameter_name}"'
920
948
 
921
949
  # Now update the parameter values if it exists.
@@ -1054,6 +1082,66 @@ class FlowManager:
1054
1082
 
1055
1083
  return DeleteConnectionResultFailure(result_details=details)
1056
1084
 
1085
+ # Check if either node is in a NodeGroup and untrack connections BEFORE removing connection
1086
+
1087
+ source_parent = source_node.parent_group
1088
+ target_parent = target_node.parent_group
1089
+
1090
+ # Find the connection before it's deleted
1091
+ conn = None
1092
+ conn_id = None
1093
+ if (
1094
+ source_node.name in self._connections.outgoing_index
1095
+ and source_param.name in self._connections.outgoing_index[source_node.name]
1096
+ ):
1097
+ connection_ids = self._connections.outgoing_index[source_node.name][source_param.name]
1098
+ for candidate_id in connection_ids:
1099
+ candidate = self._connections.connections[candidate_id]
1100
+ if (
1101
+ candidate.target_node.name == target_node.name
1102
+ and candidate.target_parameter.name == target_param.name
1103
+ ):
1104
+ conn = candidate
1105
+ conn_id = candidate_id
1106
+ break
1107
+
1108
+ # If source is in a group, untrack outgoing external connection
1109
+ if (
1110
+ conn
1111
+ and conn_id
1112
+ and source_parent is not None
1113
+ and isinstance(source_parent, NodeGroupNode)
1114
+ and target_parent != source_parent
1115
+ ):
1116
+ source_parent.untrack_external_connection(
1117
+ conn=conn,
1118
+ conn_id=conn_id,
1119
+ is_incoming=False,
1120
+ )
1121
+
1122
+ # If target is in a group, untrack incoming external connection
1123
+ if (
1124
+ conn
1125
+ and conn_id
1126
+ and target_parent is not None
1127
+ and isinstance(target_parent, NodeGroupNode)
1128
+ and source_parent != target_parent
1129
+ ):
1130
+ target_parent.untrack_external_connection(
1131
+ conn=conn,
1132
+ conn_id=conn_id,
1133
+ is_incoming=True,
1134
+ )
1135
+
1136
+ # If both in same group, untrack internal connection
1137
+ if (
1138
+ conn
1139
+ and source_parent is not None
1140
+ and source_parent == target_parent
1141
+ and isinstance(source_parent, NodeGroupNode)
1142
+ ):
1143
+ source_parent.untrack_internal_connection(conn)
1144
+
1057
1145
  # Remove the connection.
1058
1146
  if not self._connections.remove_connection(
1059
1147
  source_node=source_node.name,
@@ -1065,6 +1153,18 @@ class FlowManager:
1065
1153
 
1066
1154
  return DeleteConnectionResultFailure(result_details=details)
1067
1155
 
1156
+ # Let the source make any internal handling decisions before the Connection is REMOVED.
1157
+ source_node.before_outgoing_connection_removed(
1158
+ source_parameter=source_param, target_node=target_node, target_parameter=target_param
1159
+ )
1160
+
1161
+ # And target.
1162
+ target_node.before_incoming_connection_removed(
1163
+ source_node=source_node,
1164
+ source_parameter=source_param,
1165
+ target_parameter=target_param,
1166
+ )
1167
+
1068
1168
  # After the connection has been removed, if it doesn't have PROPERTY as a type, wipe the set parameter value and unresolve future nodes
1069
1169
  if ParameterMode.PROPERTY not in target_param.allowed_modes:
1070
1170
  try:
@@ -1157,7 +1257,6 @@ class FlowManager:
1157
1257
  node_name_to_uuid=node_name_to_uuid,
1158
1258
  set_parameter_value_commands=packaged_nodes_set_parameter_value_commands,
1159
1259
  internal_connections=packaged_nodes_internal_connections,
1160
- proxy_node=request.proxy_node,
1161
1260
  )
1162
1261
  if isinstance(serialized_package_nodes, PackageNodesAsSerializedFlowResultFailure):
1163
1262
  return serialized_package_nodes
@@ -1171,9 +1270,7 @@ class FlowManager:
1171
1270
  self._inject_output_mode_for_property_parameters(nodes_to_package, serialized_package_nodes)
1172
1271
 
1173
1272
  # Step 7: Analyze external connections (connections from/to nodes outside our selection)
1174
- node_connections_dict = self._analyze_multi_node_external_connections(
1175
- package_nodes=nodes_to_package, proxy_node=request.proxy_node
1176
- )
1273
+ node_connections_dict = self._analyze_multi_node_external_connections(package_nodes=nodes_to_package)
1177
1274
  if isinstance(node_connections_dict, PackageNodesAsSerializedFlowResultFailure):
1178
1275
  return node_connections_dict
1179
1276
 
@@ -1285,19 +1382,19 @@ class FlowManager:
1285
1382
  try:
1286
1383
  start_end_library = LibraryRegistry.get_library_for_node_type(
1287
1384
  node_type=request.start_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
1288
- specific_library_name=request.start_end_specific_library_name,
1385
+ specific_library_name=request.start_node_library_name,
1289
1386
  )
1290
1387
  except KeyError as err:
1291
- 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}."
1388
+ details = f"Attempted to package nodes with start node type '{request.start_node_type}' from library '{request.start_node_library_name}'. Failed because start node type was not found in library. Error: {err}."
1292
1389
  return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1293
1390
 
1294
1391
  try:
1295
1392
  LibraryRegistry.get_library_for_node_type(
1296
1393
  node_type=request.end_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
1297
- specific_library_name=request.start_end_specific_library_name,
1394
+ specific_library_name=request.end_node_library_name,
1298
1395
  )
1299
1396
  except KeyError as err:
1300
- 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}."
1397
+ details = f"Attempted to package nodes with end node type '{request.end_node_type}' from library '{request.end_node_library_name}'. Failed because end node type was not found in library. Error: {err}."
1301
1398
  return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1302
1399
 
1303
1400
  # Get the actual library version
@@ -1345,14 +1442,9 @@ class FlowManager:
1345
1442
  SerializedNodeCommands.NodeUUID, list[SerializedNodeCommands.IndirectSetParameterValueCommand]
1346
1443
  ],
1347
1444
  internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization], # OUTPUT: will be populated
1348
- proxy_node: NodeGroupProxyNode | None = None, # NodeGroupProxyNode if packaging nodes from a proxy
1349
1445
  ) -> list[SerializedNodeCommands] | PackageNodesAsSerializedFlowResultFailure:
1350
1446
  """Serialize package nodes while temporarily setting execution environment to local to prevent recursive loops.
1351
1447
 
1352
- This method temporarily overrides each node's execution_environment parameter to LOCAL_EXECUTION
1353
- during serialization, then restores the original values. This prevents recursive packaging loops
1354
- that could occur if nodes were configured for remote execution environments.
1355
-
1356
1448
  Args:
1357
1449
  nodes_to_package: List of nodes to serialize
1358
1450
  unique_parameter_uuid_to_values: Shared dictionary for deduplicating parameter values across all nodes
@@ -1360,213 +1452,112 @@ class FlowManager:
1360
1452
  node_name_to_uuid: OUTPUT - Dictionary mapping node names to UUIDs (populated by this method)
1361
1453
  set_parameter_value_commands: OUTPUT - Dict mapping node UUIDs to parameter value commands (populated by this method)
1362
1454
  internal_connections: OUTPUT - List of connections between package nodes (populated by this method)
1363
- proxy_node: NodeGroupProxyNode if packaging nodes from a proxy, provides access to original connections
1364
- stored before they were redirected to the proxy
1365
1455
 
1366
1456
  Returns:
1367
1457
  List of SerializedNodeCommands on success, or PackageNodesAsSerializedFlowResultFailure on failure
1368
1458
  """
1369
- # Intercept execution_environment and job_group for all nodes before serialization
1370
- original_execution_environments = {}
1371
- original_job_groups = {}
1372
- for node in nodes_to_package:
1373
- # Save and override execution_environment to prevent recursive packaging loops
1374
- original_exec_value = node.get_parameter_value("execution_environment")
1375
- original_execution_environments[node.name] = original_exec_value
1376
- node.set_parameter_value("execution_environment", LOCAL_EXECUTION)
1377
-
1378
- # Save and clear job_group to prevent group processing in packaged flow
1379
- original_job_group_value = node.get_parameter_value("job_group")
1380
- original_job_groups[node.name] = original_job_group_value
1381
- node.set_parameter_value("job_group", "")
1459
+ # Serialize each node using shared unique_parameter_uuid_to_values dictionary for deduplication
1460
+ serialized_node_commands = []
1461
+ serialized_node_group_commands = [] # NodeGroupNodes must be added LAST
1382
1462
 
1383
- try:
1384
- # Serialize each node using shared unique_parameter_uuid_to_values dictionary for deduplication
1385
- serialized_node_commands = []
1463
+ for node in nodes_to_package:
1464
+ # Serialize this node using shared dictionaries for value deduplication
1465
+ serialize_request = SerializeNodeToCommandsRequest(
1466
+ node_name=node.name,
1467
+ unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1468
+ serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1469
+ )
1470
+ serialize_result = GriptapeNodes.NodeManager().on_serialize_node_to_commands(serialize_request)
1386
1471
 
1387
- for node in nodes_to_package:
1388
- # Serialize this node using shared dictionaries for value deduplication
1389
- serialize_request = SerializeNodeToCommandsRequest(
1390
- node_name=node.name,
1391
- unique_parameter_uuid_to_values=unique_parameter_uuid_to_values,
1392
- serialized_parameter_value_tracker=serialized_parameter_value_tracker,
1472
+ if not isinstance(serialize_result, SerializeNodeToCommandsResultSuccess):
1473
+ return PackageNodesAsSerializedFlowResultFailure(
1474
+ result_details=f"Attempted to package nodes as serialized flow. Failed to serialize node '{node.name}': {serialize_result.result_details}"
1393
1475
  )
1394
- serialize_result = GriptapeNodes.NodeManager().on_serialize_node_to_commands(serialize_request)
1395
1476
 
1396
- if not isinstance(serialize_result, SerializeNodeToCommandsResultSuccess):
1397
- return PackageNodesAsSerializedFlowResultFailure(
1398
- result_details=f"Attempted to package nodes as serialized flow. Failed to serialize node '{node.name}': {serialize_result.result_details}"
1399
- )
1477
+ # Populate the shared node_name_to_uuid mapping
1478
+ create_cmd = serialize_result.serialized_node_commands.create_node_command
1479
+ # Get the node name from the CreateNodeGroupRequest command if necessary.
1480
+ node_name = (
1481
+ create_cmd.node_group_name if isinstance(create_cmd, CreateNodeGroupRequest) else create_cmd.node_name
1482
+ )
1483
+ if node_name is not None:
1484
+ node_name_to_uuid[node_name] = serialize_result.serialized_node_commands.node_uuid
1400
1485
 
1401
- # Collect serialized node
1486
+ # NodeGroupNodes must be serialized LAST because CreateNodeGroupRequest references child node names
1487
+ # If we deserialize a NodeGroup before its children, the child nodes won't exist yet
1488
+ if isinstance(node, NodeGroupNode):
1489
+ serialized_node_group_commands.append(serialize_result.serialized_node_commands)
1490
+ else:
1402
1491
  serialized_node_commands.append(serialize_result.serialized_node_commands)
1403
1492
 
1404
- # Populate the shared node_name_to_uuid mapping
1405
- if serialize_result.serialized_node_commands.create_node_command.node_name is not None:
1406
- node_name_to_uuid[serialize_result.serialized_node_commands.create_node_command.node_name] = (
1407
- serialize_result.serialized_node_commands.node_uuid
1408
- )
1409
-
1410
- # Collect set parameter value commands (references to unique_parameter_uuid_to_values)
1411
- if serialize_result.set_parameter_value_commands:
1412
- set_parameter_value_commands[serialize_result.serialized_node_commands.node_uuid] = (
1413
- serialize_result.set_parameter_value_commands
1414
- )
1415
-
1416
- # Build internal connections between package nodes
1417
- package_node_names_set = {n.name for n in nodes_to_package}
1418
-
1419
- # Get connections using appropriate method based on whether we have a proxy node
1420
- connections_result = self._get_internal_connections_for_package(
1421
- nodes_to_package=nodes_to_package,
1422
- package_node_names_set=package_node_names_set,
1423
- node_name_to_uuid=node_name_to_uuid,
1424
- proxy_node=proxy_node,
1425
- )
1493
+ # Collect set parameter value commands (references to unique_parameter_uuid_to_values)
1494
+ if serialize_result.set_parameter_value_commands:
1495
+ set_parameter_value_commands[serialize_result.serialized_node_commands.node_uuid] = (
1496
+ serialize_result.set_parameter_value_commands
1497
+ )
1426
1498
 
1427
- if isinstance(connections_result, PackageNodesAsSerializedFlowResultFailure):
1428
- return connections_result
1499
+ # Build internal connections between package nodes
1500
+ package_node_names_set = {n.name for n in nodes_to_package}
1429
1501
 
1430
- internal_connections.extend(connections_result)
1431
- finally:
1432
- # Always restore original execution_environment and job_group values, even on failure
1433
- for node_name, original_value in original_execution_environments.items():
1434
- restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1435
- restore_node.set_parameter_value("execution_environment", original_value)
1502
+ # Get connections from the connection manager
1503
+ connections_result = self._get_internal_connections_for_package(
1504
+ nodes_to_package=nodes_to_package,
1505
+ package_node_names_set=package_node_names_set,
1506
+ node_name_to_uuid=node_name_to_uuid,
1507
+ )
1436
1508
 
1437
- for node_name, original_job_group in original_job_groups.items():
1438
- restore_node = GriptapeNodes.NodeManager().get_node_by_name(node_name)
1439
- restore_node.set_parameter_value("job_group", original_job_group)
1509
+ if isinstance(connections_result, PackageNodesAsSerializedFlowResultFailure):
1510
+ return connections_result
1440
1511
 
1512
+ internal_connections.extend(connections_result)
1513
+ serialized_node_commands.extend(serialized_node_group_commands)
1441
1514
  return serialized_node_commands
1442
1515
 
1443
- def _get_internal_connections_for_package( # noqa: C901, PLR0912
1516
+ def _get_internal_connections_for_package(
1444
1517
  self,
1445
1518
  nodes_to_package: list[BaseNode],
1446
1519
  package_node_names_set: set[str],
1447
1520
  node_name_to_uuid: dict[str, SerializedNodeCommands.NodeUUID],
1448
- proxy_node: NodeGroupProxyNode | None,
1449
1521
  ) -> list[SerializedFlowCommands.IndirectConnectionSerialization] | PackageNodesAsSerializedFlowResultFailure:
1450
1522
  """Get internal connections between package nodes.
1451
1523
 
1452
- If a proxy_node is provided, uses the stored internal_connections from the NodeGroup
1453
- which preserve the original connection structure before redirection to the proxy.
1454
- Otherwise, queries connections from the connection manager.
1524
+ Queries connections from the connection manager for all nodes being packaged.
1455
1525
 
1456
1526
  Args:
1457
1527
  nodes_to_package: List of nodes being packaged
1458
1528
  package_node_names_set: Set of node names in the package for O(1) lookup
1459
1529
  node_name_to_uuid: Mapping of node names to their UUIDs in the serialization
1460
- proxy_node: Optional proxy node containing the original connection structure
1461
1530
 
1462
1531
  Returns:
1463
1532
  List of serialized connections, or PackageNodesAsSerializedFlowResultFailure on error
1464
1533
  """
1465
1534
  internal_connections: list[SerializedFlowCommands.IndirectConnectionSerialization] = []
1535
+ # Query connections from connection manager
1536
+ for node in nodes_to_package:
1537
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node.name)
1538
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1539
+ list_connections_request
1540
+ )
1466
1541
 
1467
- if proxy_node is not None and hasattr(proxy_node, "node_group_data"):
1468
- # Use stored connections from NodeGroup which have the original node references
1469
- node_group = proxy_node.node_group_data
1470
-
1471
- # 1. Add internal connections (between nodes inside the group)
1472
- for conn in node_group.internal_connections:
1473
- source_node_name = conn.source_node.name
1474
- target_node_name = conn.target_node.name
1542
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1543
+ return PackageNodesAsSerializedFlowResultFailure(
1544
+ result_details=f"Attempted to package nodes as serialized flow. Failed to list connections for node '{node.name}': {list_connections_result.result_details}"
1545
+ )
1475
1546
 
1476
- # Only include connections where BOTH nodes are in our package
1477
- if source_node_name in package_node_names_set and target_node_name in package_node_names_set:
1478
- source_uuid = node_name_to_uuid[source_node_name]
1479
- target_uuid = node_name_to_uuid[target_node_name]
1547
+ # Only include connections where BOTH source and target are in the package
1548
+ for outgoing_conn in list_connections_result.outgoing_connections:
1549
+ if outgoing_conn.target_node_name in package_node_names_set:
1550
+ source_uuid = node_name_to_uuid[node.name]
1551
+ target_uuid = node_name_to_uuid[outgoing_conn.target_node_name]
1480
1552
  internal_connections.append(
1481
1553
  SerializedFlowCommands.IndirectConnectionSerialization(
1482
1554
  source_node_uuid=source_uuid,
1483
- source_parameter_name=conn.source_parameter.name,
1555
+ source_parameter_name=outgoing_conn.source_parameter_name,
1484
1556
  target_node_uuid=target_uuid,
1485
- target_parameter_name=conn.target_parameter.name,
1557
+ target_parameter_name=outgoing_conn.target_parameter_name,
1486
1558
  )
1487
1559
  )
1488
1560
 
1489
- # 2. Add external incoming connections (from outside into the group)
1490
- # These connections have been redirected to point TO the proxy, but we want the original targets
1491
- for conn in node_group.external_incoming_connections:
1492
- conn_id = id(conn)
1493
- original_target = node_group.original_incoming_targets.get(conn_id)
1494
-
1495
- if original_target and original_target.name in package_node_names_set:
1496
- # The source is outside the package, target is inside
1497
- # We include these because the source will be external (like StartFlow)
1498
- source_node_name = conn.source_node.name
1499
- target_node_name = original_target.name
1500
-
1501
- # Only include if source is NOT in package (external) and target IS in package
1502
- if source_node_name not in package_node_names_set:
1503
- source_uuid = node_name_to_uuid.get(source_node_name)
1504
- target_uuid = node_name_to_uuid[target_node_name]
1505
-
1506
- # Source might not have a UUID if it's external to the package
1507
- if source_uuid:
1508
- internal_connections.append(
1509
- SerializedFlowCommands.IndirectConnectionSerialization(
1510
- source_node_uuid=source_uuid,
1511
- source_parameter_name=conn.source_parameter.name,
1512
- target_node_uuid=target_uuid,
1513
- target_parameter_name=conn.target_parameter.name,
1514
- )
1515
- )
1516
-
1517
- # 3. Add external outgoing connections (from the group to outside)
1518
- # These connections have been redirected to point FROM the proxy, but we want the original sources
1519
- for conn in node_group.external_outgoing_connections:
1520
- conn_id = id(conn)
1521
- original_source = node_group.original_outgoing_sources.get(conn_id)
1522
-
1523
- if original_source and original_source.name in package_node_names_set:
1524
- # The source is inside the package, target is outside
1525
- source_node_name = original_source.name
1526
- target_node_name = conn.target_node.name
1527
-
1528
- # Only include if source IS in package and target is NOT in package (external)
1529
- if target_node_name not in package_node_names_set:
1530
- source_uuid = node_name_to_uuid[source_node_name]
1531
- target_uuid = node_name_to_uuid.get(target_node_name)
1532
-
1533
- # Target might not have a UUID if it's external to the package
1534
- if target_uuid:
1535
- internal_connections.append(
1536
- SerializedFlowCommands.IndirectConnectionSerialization(
1537
- source_node_uuid=source_uuid,
1538
- source_parameter_name=conn.source_parameter.name,
1539
- target_node_uuid=target_uuid,
1540
- target_parameter_name=conn.target_parameter.name,
1541
- )
1542
- )
1543
- else:
1544
- # No proxy node - query connections from connection manager
1545
- for node in nodes_to_package:
1546
- list_connections_request = ListConnectionsForNodeRequest(node_name=node.name)
1547
- list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1548
- list_connections_request
1549
- )
1550
-
1551
- if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1552
- return PackageNodesAsSerializedFlowResultFailure(
1553
- result_details=f"Attempted to package nodes as serialized flow. Failed to list connections for node '{node.name}': {list_connections_result.result_details}"
1554
- )
1555
-
1556
- # Only include connections where BOTH source and target are in the package
1557
- for outgoing_conn in list_connections_result.outgoing_connections:
1558
- if outgoing_conn.target_node_name in package_node_names_set:
1559
- source_uuid = node_name_to_uuid[node.name]
1560
- target_uuid = node_name_to_uuid[outgoing_conn.target_node_name]
1561
- internal_connections.append(
1562
- SerializedFlowCommands.IndirectConnectionSerialization(
1563
- source_node_uuid=source_uuid,
1564
- source_parameter_name=outgoing_conn.source_parameter_name,
1565
- target_node_uuid=target_uuid,
1566
- target_parameter_name=outgoing_conn.target_parameter_name,
1567
- )
1568
- )
1569
-
1570
1561
  return internal_connections
1571
1562
 
1572
1563
  def _inject_output_mode_for_property_parameters(
@@ -1589,7 +1580,15 @@ class FlowManager:
1589
1580
  # Find the corresponding serialized node
1590
1581
  serialized_node = None
1591
1582
  for serialized_node_command in serialized_package_nodes:
1592
- if serialized_node_command.create_node_command.node_name == package_node.name:
1583
+ # We need to get the create commoand.
1584
+ create_cmd = serialized_node_command.create_node_command
1585
+ # In a CreateNodeGroupRequest, the node_name is the node_group_name, so we need to add both.
1586
+ cmd_node_name = (
1587
+ create_cmd.node_group_name
1588
+ if isinstance(create_cmd, CreateNodeGroupRequest)
1589
+ else create_cmd.node_name
1590
+ )
1591
+ if cmd_node_name == package_node.name:
1593
1592
  serialized_node = serialized_node_command
1594
1593
  break
1595
1594
 
@@ -1616,7 +1615,7 @@ class FlowManager:
1616
1615
  serialized_node.element_modification_commands.extend(package_alter_parameter_commands)
1617
1616
 
1618
1617
  def _analyze_multi_node_external_connections(
1619
- self, package_nodes: list[BaseNode], proxy_node: NodeGroupProxyNode | None = None
1618
+ self, package_nodes: list[BaseNode]
1620
1619
  ) -> dict[str, ConnectionAnalysis] | PackageNodesAsSerializedFlowResultFailure:
1621
1620
  """Analyze external connections for each package node using filtered single-node analysis.
1622
1621
 
@@ -1632,7 +1631,6 @@ class FlowManager:
1632
1631
 
1633
1632
  Args:
1634
1633
  package_nodes: List of nodes being packaged together
1635
- proxy_node: Optional proxy node containing the original connection structure
1636
1634
 
1637
1635
  Returns:
1638
1636
  Dictionary mapping node_name -> ConnectionAnalysis, where each ConnectionAnalysis
@@ -1647,7 +1645,6 @@ class FlowManager:
1647
1645
  package_node=package_node,
1648
1646
  node_name=package_node.name,
1649
1647
  package_node_names=package_node_names_set,
1650
- proxy_node=proxy_node,
1651
1648
  )
1652
1649
  if isinstance(connection_analysis, PackageNodesAsSerializedFlowResultFailure):
1653
1650
  return PackageNodesAsSerializedFlowResultFailure(result_details=connection_analysis.result_details)
@@ -1656,112 +1653,32 @@ class FlowManager:
1656
1653
 
1657
1654
  return node_connections
1658
1655
 
1659
- def _get_node_connections_from_proxy(
1660
- self, node_name: str, proxy_node: NodeGroupProxyNode
1661
- ) -> tuple[list[IncomingConnection], list[OutgoingConnection]]:
1662
- """Extract incoming and outgoing connections for a specific node from the proxy node's stored data.
1663
-
1664
- Returns connections in the same format as ListConnectionsForNodeRequest would return them,
1665
- using the original node references from before proxy redirection.
1666
-
1667
- Args:
1668
- node_name: Name of the node to get connections for
1669
- proxy_node: The proxy node containing the NodeGroup data with stored connections
1670
-
1671
- Returns:
1672
- Tuple of (incoming_connections, outgoing_connections) matching the ListConnectionsForNodeResultSuccess format
1673
- """
1674
- node_group = proxy_node.node_group_data
1675
- incoming_connections: list[IncomingConnection] = []
1676
- outgoing_connections: list[OutgoingConnection] = []
1677
-
1678
- # Get incoming connections: check internal_connections and external_incoming_connections
1679
- # Internal connections where this node is the target
1680
- incoming_connections.extend(
1681
- IncomingConnection(
1682
- source_node_name=conn.source_node.name,
1683
- source_parameter_name=conn.source_parameter.name,
1684
- target_parameter_name=conn.target_parameter.name,
1685
- )
1686
- for conn in node_group.internal_connections
1687
- if conn.target_node.name == node_name
1688
- )
1689
-
1690
- # External incoming connections where this node is the original target
1691
- incoming_connections.extend(
1692
- IncomingConnection(
1693
- source_node_name=conn.source_node.name,
1694
- source_parameter_name=conn.source_parameter.name,
1695
- target_parameter_name=conn.target_parameter.name,
1696
- )
1697
- for conn in node_group.external_incoming_connections
1698
- if (original_target := node_group.original_incoming_targets.get(id(conn)))
1699
- and original_target.name == node_name
1700
- )
1701
-
1702
- # Get outgoing connections: check internal_connections and external_outgoing_connections
1703
- # Internal connections where this node is the source
1704
- outgoing_connections.extend(
1705
- OutgoingConnection(
1706
- source_parameter_name=conn.source_parameter.name,
1707
- target_node_name=conn.target_node.name,
1708
- target_parameter_name=conn.target_parameter.name,
1709
- )
1710
- for conn in node_group.internal_connections
1711
- if conn.source_node.name == node_name
1712
- )
1713
-
1714
- # External outgoing connections where this node is the original source
1715
- outgoing_connections.extend(
1716
- OutgoingConnection(
1717
- source_parameter_name=conn.source_parameter.name,
1718
- target_node_name=conn.target_node.name,
1719
- target_parameter_name=conn.target_parameter.name,
1720
- )
1721
- for conn in node_group.external_outgoing_connections
1722
- if (original_source := node_group.original_outgoing_sources.get(id(conn)))
1723
- and original_source.name == node_name
1724
- )
1725
-
1726
- return incoming_connections, outgoing_connections
1727
-
1728
1656
  def _analyze_package_node_connections(
1729
1657
  self,
1730
1658
  package_node: BaseNode,
1731
1659
  node_name: str,
1732
1660
  package_node_names: set[str] | None = None,
1733
- proxy_node: NodeGroupProxyNode | None = None,
1734
1661
  ) -> ConnectionAnalysis | PackageNodesAsSerializedFlowResultFailure:
1735
1662
  """Analyze package node connections and separate control from data connections.
1736
1663
 
1737
- If a proxy_node is provided, uses the stored connections from the NodeGroup which preserve
1738
- the original connection structure before proxy redirection.
1739
-
1740
1664
  Args:
1741
1665
  package_node: The node being analyzed
1742
1666
  node_name: Name of the node
1743
1667
  package_node_names: Set of node names in the package for filtering internal connections
1744
- proxy_node: Optional proxy node containing original connection structure
1745
1668
  """
1746
1669
  # Get incoming and outgoing connections for this node
1747
- if proxy_node is not None and hasattr(proxy_node, "node_group_data"):
1748
- # Use stored connections from proxy which have the original node references
1749
- incoming_connections, outgoing_connections = self._get_node_connections_from_proxy(
1750
- node_name=node_name, proxy_node=proxy_node
1751
- )
1752
- else:
1753
- # Get connection details using the standard approach
1754
- list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
1755
- list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1756
- list_connections_request
1757
- )
1670
+ # Get connection details using the standard approach
1671
+ list_connections_request = ListConnectionsForNodeRequest(node_name=node_name)
1672
+ list_connections_result = GriptapeNodes.NodeManager().on_list_connections_for_node_request(
1673
+ list_connections_request
1674
+ )
1758
1675
 
1759
- if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1760
- details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
1761
- return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1676
+ if not isinstance(list_connections_result, ListConnectionsForNodeResultSuccess):
1677
+ details = f"Attempted to analyze connections for package node '{node_name}'. Failed because connection listing failed."
1678
+ return PackageNodesAsSerializedFlowResultFailure(result_details=details)
1762
1679
 
1763
- incoming_connections = list_connections_result.incoming_connections
1764
- outgoing_connections = list_connections_result.outgoing_connections
1680
+ incoming_connections = list_connections_result.incoming_connections
1681
+ outgoing_connections = list_connections_result.outgoing_connections
1765
1682
 
1766
1683
  # Separate control connections from data connections based on package node's parameter types
1767
1684
  incoming_data_connections = []
@@ -1815,7 +1732,7 @@ class FlowManager:
1815
1732
  # Build end node CreateNodeRequest
1816
1733
  end_create_node_command = CreateNodeRequest(
1817
1734
  node_type=request.end_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
1818
- specific_library_name=request.start_end_specific_library_name,
1735
+ specific_library_name=request.end_node_library_name,
1819
1736
  node_name=end_node_name,
1820
1737
  metadata={},
1821
1738
  initial_setup=True,
@@ -1824,7 +1741,7 @@ class FlowManager:
1824
1741
 
1825
1742
  # Create library details
1826
1743
  end_node_library_details = LibraryNameAndVersion(
1827
- library_name=request.start_end_specific_library_name,
1744
+ library_name=request.end_node_library_name,
1828
1745
  library_version=library_version,
1829
1746
  )
1830
1747
 
@@ -2068,7 +1985,7 @@ class FlowManager:
2068
1985
  # Build start node CreateNodeRequest
2069
1986
  start_create_node_command = CreateNodeRequest(
2070
1987
  node_type=request.start_node_type, # type: ignore[arg-type] # Guaranteed non-None by handler
2071
- specific_library_name=request.start_end_specific_library_name,
1988
+ specific_library_name=request.start_node_library_name,
2072
1989
  node_name=start_node_name,
2073
1990
  metadata={},
2074
1991
  initial_setup=True,
@@ -2077,7 +1994,7 @@ class FlowManager:
2077
1994
 
2078
1995
  # Create library details
2079
1996
  start_node_library_details = LibraryNameAndVersion(
2080
- library_name=request.start_end_specific_library_name,
1997
+ library_name=request.start_node_library_name,
2081
1998
  library_version=library_version,
2082
1999
  )
2083
2000
 
@@ -2740,8 +2657,12 @@ class FlowManager:
2740
2657
 
2741
2658
  # Collect node types from all nodes in this flow
2742
2659
  for node_cmd in serialized_node_commands:
2743
- node_type = node_cmd.create_node_command.node_type
2744
- library_name = node_cmd.create_node_command.specific_library_name
2660
+ create_cmd = node_cmd.create_node_command
2661
+ # Skip NodeGroupNode as it doesn't have node_type/specific_library_name
2662
+ if isinstance(create_cmd, CreateNodeGroupRequest):
2663
+ continue
2664
+ node_type = create_cmd.node_type
2665
+ library_name = create_cmd.specific_library_name
2745
2666
  if library_name is None:
2746
2667
  msg = f"Node type '{node_type}' has no library name specified during serialization"
2747
2668
  raise ValueError(msg)
@@ -2799,6 +2720,7 @@ class FlowManager:
2799
2720
  create_flow_request = None
2800
2721
 
2801
2722
  serialized_node_commands = []
2723
+ serialized_node_group_commands = [] # NodeGroupNodes must be added LAST
2802
2724
  set_parameter_value_commands_per_node = {} # Maps a node UUID to a list of set parameter value commands
2803
2725
  set_lock_commands_per_node = {} # Maps a node UUID to a set Lock command, if it exists.
2804
2726
 
@@ -2835,7 +2757,13 @@ class FlowManager:
2835
2757
  # Store the serialized node's UUID for correlation to connections and setting parameter values later.
2836
2758
  node_name_to_uuid[node_name] = serialized_node.node_uuid
2837
2759
 
2838
- serialized_node_commands.append(serialized_node)
2760
+ # NodeGroupNodes must be serialized LAST because CreateNodeGroupRequest references child node names
2761
+ # If we deserialize a NodeGroup before its children, the child nodes won't exist yet
2762
+ if isinstance(node, NodeGroupNode):
2763
+ serialized_node_group_commands.append(serialized_node)
2764
+ else:
2765
+ serialized_node_commands.append(serialized_node)
2766
+
2839
2767
  # Get the list of set value commands for THIS node.
2840
2768
  set_value_commands_list = serialize_node_result.set_parameter_value_commands
2841
2769
  if serialize_node_result.serialized_node_commands.lock_node_command is not None:
@@ -2912,6 +2840,10 @@ class FlowManager:
2912
2840
  serialized_flow = child_flow_result.serialized_flow_commands
2913
2841
  sub_flow_commands.append(serialized_flow)
2914
2842
 
2843
+ # Append NodeGroup commands AFTER regular node commands
2844
+ # This ensures child nodes exist before their parent NodeGroups are created during deserialization
2845
+ serialized_node_commands.extend(serialized_node_group_commands)
2846
+
2915
2847
  # Aggregate all dependencies from nodes and sub-flows
2916
2848
  aggregated_dependencies = self._aggregate_flow_dependencies(serialized_node_commands, sub_flow_commands)
2917
2849
 
@@ -3093,8 +3025,6 @@ class FlowManager:
3093
3025
  await self._global_control_flow_machine.start_flow(start_node, debug_mode=debug_mode)
3094
3026
  except Exception:
3095
3027
  if self.check_for_existing_running_flow():
3096
- # Cleanup proxy nodes before canceling flow
3097
- self._global_control_flow_machine.cleanup_proxy_nodes()
3098
3028
  await self.cancel_flow_run()
3099
3029
  raise
3100
3030
  GriptapeNodes.EventManager().put_event(
@@ -3123,10 +3053,6 @@ class FlowManager:
3123
3053
  if self._global_control_flow_machine is not None:
3124
3054
  await self._global_control_flow_machine.cancel_flow()
3125
3055
 
3126
- # Cleanup proxy nodes and restore connections
3127
- if self._global_control_flow_machine is not None:
3128
- self._global_control_flow_machine.cleanup_proxy_nodes()
3129
-
3130
3056
  # Reset control flow machine
3131
3057
  if self._global_control_flow_machine is not None:
3132
3058
  self._global_control_flow_machine.reset_machine(cancel=True)
@@ -3147,7 +3073,6 @@ class FlowManager:
3147
3073
 
3148
3074
  # Cleanup proxy nodes and restore connections before resetting machine
3149
3075
  if self._global_control_flow_machine is not None:
3150
- self._global_control_flow_machine.cleanup_proxy_nodes()
3151
3076
  self._global_control_flow_machine.reset_machine()
3152
3077
 
3153
3078
  # Reset control flow machine
@@ -3259,8 +3184,6 @@ class FlowManager:
3259
3184
  except Exception as e:
3260
3185
  logger.exception("Exception during single node resolution")
3261
3186
  if self.check_for_existing_running_flow():
3262
- if self._global_control_flow_machine is not None:
3263
- self._global_control_flow_machine.cleanup_proxy_nodes()
3264
3187
  await self.cancel_flow_run()
3265
3188
  raise RuntimeError(e) from e
3266
3189
  if resolution_machine.is_complete():