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
@@ -11,7 +11,6 @@ from griptape_nodes.exe_types.node_types import (
11
11
  CONTROL_INPUT_PARAMETER,
12
12
  LOCAL_EXECUTION,
13
13
  BaseNode,
14
- NodeGroupProxyNode,
15
14
  NodeResolutionState,
16
15
  )
17
16
  from griptape_nodes.exe_types.type_validator import TypeValidator
@@ -129,12 +128,6 @@ class ExecuteDagState(State):
129
128
  network_name,
130
129
  )
131
130
  return
132
-
133
- # Check if this is a NodeGroupProxyNode - if so, handle grouped nodes
134
-
135
- if isinstance(current_node, NodeGroupProxyNode):
136
- await ExecuteDagState._handle_group_proxy_completion(context, current_node, network_name)
137
- return
138
131
  # Publish all parameter updates.
139
132
  current_node.state = NodeResolutionState.RESOLVED
140
133
  # Track this as the last resolved node
@@ -192,171 +185,6 @@ class ExecuteDagState(State):
192
185
  # Now the final thing to do, is to take their directed graph and update it.
193
186
  ExecuteDagState.get_next_control_graph(context, current_node, network_name)
194
187
 
195
- # Method is mirrored in Control_flow.py. If you update one, update the other.
196
- @staticmethod
197
- async def _handle_group_proxy_completion(
198
- context: ParallelResolutionContext, proxy_node: BaseNode, network_name: str
199
- ) -> None:
200
- """Handle completion of a NodeGroupProxyNode by marking all grouped nodes as resolved.
201
-
202
- When a NodeGroupProxyNode completes, all nodes in the group have been executed
203
- in parallel by the NodeExecutor. This method marks each grouped node as RESOLVED
204
- and emits NodeResolvedEvent for each one.
205
-
206
- Args:
207
- proxy_node: The NodeGroupProxyNode that completed execution
208
- context: The ParallelResolutionContext
209
- network_name: The name of the network
210
- """
211
- from griptape_nodes.exe_types.node_types import NodeGroupProxyNode
212
-
213
- if not isinstance(proxy_node, NodeGroupProxyNode):
214
- return
215
-
216
- node_group = proxy_node.node_group_data
217
-
218
- # Mark all grouped nodes as resolved and emit events
219
- proxy_node.state = NodeResolutionState.RESOLVED
220
- for grouped_node in node_group.nodes.values():
221
- # Mark node as resolved
222
- grouped_node.state = NodeResolutionState.RESOLVED
223
-
224
- # Emit parameter update events for each output parameter
225
- for parameter_name, value in grouped_node.parameter_output_values.items():
226
- parameter = grouped_node.get_parameter_by_name(parameter_name)
227
- if parameter is None:
228
- logger.warning(
229
- "Node '%s' in group '%s' has output parameter '%s' but parameter not found",
230
- grouped_node.name,
231
- node_group.group_id,
232
- parameter_name,
233
- )
234
- continue
235
-
236
- data_type = parameter.type
237
- if data_type is None:
238
- data_type = ParameterTypeBuiltin.NONE.value
239
-
240
- await GriptapeNodes.EventManager().aput_event(
241
- ExecutionGriptapeNodeEvent(
242
- wrapped_event=ExecutionEvent(
243
- payload=ParameterValueUpdateEvent(
244
- node_name=grouped_node.name,
245
- parameter_name=parameter_name,
246
- data_type=data_type,
247
- value=TypeValidator.safe_serialize(value),
248
- )
249
- ),
250
- )
251
- )
252
-
253
- # Emit NodeResolvedEvent for the grouped node
254
- library = LibraryRegistry.get_libraries_with_node_type(grouped_node.__class__.__name__)
255
- library_name = library[0] if len(library) == 1 else None
256
-
257
- await GriptapeNodes.EventManager().aput_event(
258
- ExecutionGriptapeNodeEvent(
259
- wrapped_event=ExecutionEvent(
260
- payload=NodeResolvedEvent(
261
- node_name=grouped_node.name,
262
- parameter_output_values=TypeValidator.safe_serialize(grouped_node.parameter_output_values),
263
- node_type=grouped_node.__class__.__name__,
264
- specific_library_name=library_name,
265
- )
266
- )
267
- )
268
- )
269
-
270
- # Cleanup: restore connections and deregister proxy
271
- ExecuteDagState.get_next_control_graph(context, proxy_node, network_name)
272
- ExecuteDagState._cleanup_proxy_node(proxy_node)
273
-
274
- @staticmethod
275
- def _cleanup_proxy_node(proxy_node: BaseNode) -> None:
276
- """Clean up a NodeGroupProxyNode after execution completes.
277
-
278
- Restores original connections from proxy back to grouped nodes and
279
- deregisters the proxy node from ObjectManager.
280
-
281
- Args:
282
- proxy_node: The NodeGroupProxyNode to clean up
283
- """
284
- from griptape_nodes.exe_types.node_types import NodeGroupProxyNode
285
-
286
- if not isinstance(proxy_node, NodeGroupProxyNode):
287
- return
288
-
289
- node_group = proxy_node.node_group_data
290
- connections = GriptapeNodes.FlowManager().get_connections()
291
-
292
- # Restore external incoming connections (proxy -> original target node)
293
- for conn in node_group.external_incoming_connections:
294
- conn_id = id(conn)
295
-
296
- # Get original target node
297
- original_target = node_group.original_incoming_targets.get(conn_id)
298
- if original_target is None:
299
- continue
300
-
301
- # Create proxy parameter name to find it in the index
302
- sanitized_node_name = original_target.name.replace(" ", "_")
303
- proxy_param_name = f"{sanitized_node_name}__{conn.target_parameter.name}"
304
-
305
- # Remove proxy from incoming index (using proxy parameter name)
306
- if (
307
- proxy_node.name in connections.incoming_index
308
- and proxy_param_name in connections.incoming_index[proxy_node.name]
309
- ):
310
- connections.incoming_index[proxy_node.name][proxy_param_name].remove(conn_id)
311
-
312
- # Restore connection to original target node
313
- conn.target_node = original_target
314
-
315
- # Add back to original target node's incoming index (using original parameter name)
316
- connections.incoming_index.setdefault(conn.target_node.name, {}).setdefault(
317
- conn.target_parameter.name, []
318
- ).append(conn_id)
319
-
320
- # Restore external outgoing connections (original source node -> proxy)
321
- for conn in node_group.external_outgoing_connections:
322
- conn_id = id(conn)
323
-
324
- # Get original source node
325
- original_source = node_group.original_outgoing_sources.get(conn_id)
326
- if original_source is None:
327
- continue
328
-
329
- # Create proxy parameter name to find it in the index
330
- sanitized_node_name = original_source.name.replace(" ", "_")
331
- proxy_param_name = f"{sanitized_node_name}__{conn.source_parameter.name}"
332
-
333
- # Remove proxy from outgoing index (using proxy parameter name)
334
- if (
335
- proxy_node.name in connections.outgoing_index
336
- and proxy_param_name in connections.outgoing_index[proxy_node.name]
337
- ):
338
- connections.outgoing_index[proxy_node.name][proxy_param_name].remove(conn_id)
339
-
340
- # Restore connection to original source node
341
- conn.source_node = original_source
342
-
343
- # Add back to original source node's outgoing index (using original parameter name)
344
- connections.outgoing_index.setdefault(conn.source_node.name, {}).setdefault(
345
- conn.source_parameter.name, []
346
- ).append(conn_id)
347
-
348
- # Deregister proxy node from ObjectManager
349
- obj_manager = GriptapeNodes.ObjectManager()
350
- if obj_manager.has_object_with_name(proxy_node.name):
351
- del obj_manager._name_to_objects[proxy_node.name]
352
-
353
- logger.debug(
354
- "Cleaned up proxy node '%s' for group '%s' - restored %d connections",
355
- proxy_node.name,
356
- node_group.group_id,
357
- len(node_group.external_incoming_connections) + len(node_group.external_outgoing_connections),
358
- )
359
-
360
188
  @staticmethod
361
189
  def get_next_control_output_for_non_local_execution(node: BaseNode) -> Parameter | None:
362
190
  for param_name, value in node.parameter_output_values.items():
@@ -381,11 +209,12 @@ class ExecuteDagState(State):
381
209
  # Early returns for various conditions
382
210
  if ExecuteDagState._should_skip_control_flow(context, node, network_name, flow_manager):
383
211
  return
384
- if node.get_parameter_value(node.execution_environment.name) != LOCAL_EXECUTION:
385
- next_output = ExecuteDagState.get_next_control_output_for_non_local_execution(node)
386
- else:
387
- next_output = node.get_next_control_output()
388
- if node.get_parameter_value(node.execution_environment.name) != LOCAL_EXECUTION:
212
+ from griptape_nodes.exe_types.node_types import NodeGroupNode
213
+
214
+ if (
215
+ isinstance(node, NodeGroupNode)
216
+ and node.get_parameter_value(node.execution_environment.name) != LOCAL_EXECUTION
217
+ ):
389
218
  next_output = ExecuteDagState.get_next_control_output_for_non_local_execution(node)
390
219
  else:
391
220
  next_output = node.get_next_control_output()
@@ -425,14 +254,6 @@ class ExecuteDagState(State):
425
254
  """Process the next control node in the flow."""
426
255
  node_connection = flow_manager.get_connections().get_connected_node(node, next_output)
427
256
  if node_connection is not None:
428
- next_node, next_parameter = node_connection
429
- # Set entry control parameter
430
- logger.debug(
431
- "Parallel Resolution: Setting entry control parameter for node '%s' to '%s'",
432
- next_node.name,
433
- next_parameter.name if next_parameter else None,
434
- )
435
- next_node.set_entry_control_parameter(next_parameter)
436
257
  next_node, next_parameter = node_connection
437
258
  # Set entry control parameter
438
259
  logger.debug(
@@ -519,7 +340,6 @@ class ExecuteDagState(State):
519
340
  for parameter in current_node.parameters:
520
341
  # Get the connected upstream node for this parameter
521
342
  upstream_connection = connections.get_connected_node(current_node, parameter, direction=Direction.UPSTREAM)
522
- upstream_connection = connections.get_connected_node(current_node, parameter, direction=Direction.UPSTREAM)
523
343
  if upstream_connection:
524
344
  upstream_node, upstream_parameter = upstream_connection
525
345
 
@@ -544,20 +364,6 @@ class ExecuteDagState(State):
544
364
  msg = f"Failed to set value for parameter '{parameter.name}' on node '{current_node.name}': {result.result_details}"
545
365
  logger.error(msg)
546
366
  raise RuntimeError(msg)
547
- result = await GriptapeNodes.get_instance().ahandle_request(
548
- SetParameterValueRequest(
549
- parameter_name=parameter.name,
550
- node_name=current_node.name,
551
- value=output_value,
552
- data_type=upstream_parameter.output_type,
553
- incoming_connection_source_node_name=upstream_node.name,
554
- incoming_connection_source_parameter_name=upstream_parameter.name,
555
- )
556
- )
557
- if isinstance(result, SetParameterValueResultFailure):
558
- msg = f"Failed to set value for parameter '{parameter.name}' on node '{current_node.name}': {result.result_details}"
559
- logger.error(msg)
560
- raise RuntimeError(msg)
561
367
 
562
368
  @staticmethod
563
369
  def build_node_states(context: ParallelResolutionContext) -> tuple[set[str], set[str], set[str]]:
@@ -582,6 +388,7 @@ class ExecuteDagState(State):
582
388
  canceled_nodes.add(node)
583
389
  elif node_state == NodeState.QUEUED:
584
390
  queued_nodes.add(node)
391
+
585
392
  return canceled_nodes, queued_nodes, leaf_nodes
586
393
 
587
394
  @staticmethod
@@ -636,7 +443,7 @@ class ExecuteDagState(State):
636
443
  return None
637
444
 
638
445
  @staticmethod
639
- async def on_update(context: ParallelResolutionContext) -> type[State] | None: # noqa: C901, PLR0911, PLR0915
446
+ async def on_update(context: ParallelResolutionContext) -> type[State] | None: # noqa: C901, PLR0911
640
447
  # Check if execution is paused
641
448
  if context.paused:
642
449
  return None
@@ -728,26 +535,12 @@ class ExecuteDagState(State):
728
535
  exc,
729
536
  )
730
537
 
731
- dag_node = context.task_to_node.get(task)
732
- node_name = dag_node.node_reference.name if dag_node else "Unknown"
733
- node_type = dag_node.node_reference.__class__.__name__ if dag_node else "Unknown"
734
-
735
- logger.exception(
736
- "Task execution failed for node '%s' (type: %s) in flow '%s'. Exception: %s",
737
- node_name,
738
- node_type,
739
- context.flow_name,
740
- exc,
741
- )
742
-
743
538
  context.task_to_node.pop(task)
744
539
  context.error_message = f"Task execution failed for node '{node_name}': {exc}"
745
540
  context.workflow_state = WorkflowState.ERRORED
746
541
  return ErrorState
747
- context.error_message = f"Task execution failed for node '{node_name}': {exc}"
748
- context.workflow_state = WorkflowState.ERRORED
749
- return ErrorState
750
542
  context.task_to_node.pop(task)
543
+
751
544
  # Once a task has finished, loop back to the top.
752
545
  await ExecuteDagState.pop_done_states(context)
753
546
  # Remove all nodes that are done
@@ -762,18 +555,7 @@ class ErrorState(State):
762
555
  if context.error_message:
763
556
  logger.error("DAG execution error: %s", context.error_message)
764
557
 
765
- # Clean up any proxy nodes that failed before completion
766
- from griptape_nodes.exe_types.node_types import NodeGroupProxyNode
767
-
768
558
  for node in context.node_to_reference.values():
769
- # Clean up proxy nodes that were processing or queued
770
- if isinstance(node.node_reference, NodeGroupProxyNode) and node.node_state in (
771
- NodeState.PROCESSING,
772
- NodeState.QUEUED,
773
- ):
774
- logger.info("Cleaning up proxy node '%s' that failed during execution", node.node_reference.name)
775
- ExecuteDagState._cleanup_proxy_node(node.node_reference)
776
-
777
559
  # Cancel all nodes that haven't yet begun processing.
778
560
  if node.node_state == NodeState.QUEUED:
779
561
  node.node_state = NodeState.CANCELED
@@ -796,15 +578,6 @@ class ErrorState(State):
796
578
  node.node_state = NodeState.CANCELED
797
579
  task_to_node.pop(task)
798
580
 
799
- # Handle async tasks
800
- task_to_node = context.task_to_node
801
- for task, node in task_to_node.copy().items():
802
- if task.done():
803
- node.node_state = NodeState.DONE
804
- elif task.cancelled():
805
- node.node_state = NodeState.CANCELED
806
- task_to_node.pop(task)
807
-
808
581
  if len(task_to_node) == 0:
809
582
  # Finish up. We failed.
810
583
  context.workflow_state = WorkflowState.ERRORED
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  from dataclasses import dataclass
6
+ from typing import Any
6
7
 
7
8
  from griptape_nodes.exe_types.connections import Direction
8
9
  from griptape_nodes.exe_types.core_types import ParameterTypeBuiltin
9
- from griptape_nodes.exe_types.node_types import BaseNode, NodeResolutionState
10
+ from griptape_nodes.exe_types.node_types import LOCAL_EXECUTION, BaseNode, NodeGroupNode, NodeResolutionState
10
11
  from griptape_nodes.exe_types.type_validator import TypeValidator
11
12
  from griptape_nodes.machines.fsm import FSM, State
12
13
  from griptape_nodes.node_library.library_registry import LibraryRegistry
@@ -77,6 +78,43 @@ class InitializeSpotlightState(State):
77
78
  if not len(context.focus_stack):
78
79
  return CompleteState
79
80
  current_node = context.current_node
81
+
82
+ # If this node has a non-LOCAL parent group, redirect to parent instead
83
+ # Handle nested groups recursively - keep redirecting until we find the top-level parent
84
+ from griptape_nodes.exe_types.node_types import LOCAL_EXECUTION, NodeGroupNode
85
+
86
+ while current_node.parent_group is not None and isinstance(current_node.parent_group, NodeGroupNode):
87
+ execution_env = current_node.parent_group.get_parameter_value(
88
+ current_node.parent_group.execution_environment.name
89
+ )
90
+ if execution_env != LOCAL_EXECUTION:
91
+ # Replace current node with parent group
92
+ parent_group = current_node.parent_group
93
+ logger.info(
94
+ "Sequential Resolution: Redirecting from child node '%s' to parent node group '%s' at InitializeSpotlight",
95
+ current_node.name,
96
+ parent_group.name,
97
+ )
98
+ # Update the focus stack to use parent instead
99
+ context.focus_stack[-1] = Focus(node=parent_group)
100
+ current_node = parent_group
101
+ # Continue loop to check if this parent also has a parent
102
+ else:
103
+ # Parent is LOCAL_EXECUTION, stop redirecting
104
+ break
105
+
106
+ # For NodeGroups, check external connections for unresolved dependencies
107
+ if isinstance(current_node, NodeGroupNode):
108
+ unresolved_dependency = EvaluateParameterState._check_node_group_external_dependencies(current_node)
109
+ if unresolved_dependency:
110
+ logger.info(
111
+ "Sequential Resolution: NodeGroup '%s' has unresolved external dependency on '%s', queuing dependency first",
112
+ current_node.name,
113
+ unresolved_dependency.name,
114
+ )
115
+ context.focus_stack.append(Focus(node=unresolved_dependency))
116
+ return InitializeSpotlightState
117
+
80
118
  if current_node.state == NodeResolutionState.UNRESOLVED:
81
119
  # Mark all future nodes unresolved.
82
120
  # TODO: https://github.com/griptape-ai/griptape-nodes/issues/862
@@ -120,27 +158,111 @@ class EvaluateParameterState(State):
120
158
  return EvaluateParameterState
121
159
  return None
122
160
 
161
+ @staticmethod
162
+ def _get_next_node(current_node: BaseNode, current_parameter: Any, connections: Any) -> BaseNode | None:
163
+ """Get the next node connected to the current parameter."""
164
+ next_node = connections.get_connected_node(current_node, current_parameter)
165
+ if next_node:
166
+ next_node, _ = next_node
167
+ return next_node
168
+
169
+ @staticmethod
170
+ def _check_for_cycle(next_node: BaseNode, current_node: BaseNode, focus_stack_names: set[str]) -> None:
171
+ """Check if queuing next_node would create a cycle."""
172
+ if next_node.name in focus_stack_names:
173
+ msg = f"Cycle detected between node '{current_node.name}' and '{next_node.name}'."
174
+ raise RuntimeError(msg)
175
+
176
+ @staticmethod
177
+ def _handle_parent_already_resolved(current_node: BaseNode) -> type[State]:
178
+ """Handle case where parent node group is already resolved."""
179
+ if current_node.advance_parameter():
180
+ return InitializeSpotlightState
181
+ return ExecuteNodeState
182
+
183
+ @staticmethod
184
+ def _check_node_group_external_dependencies(node_group: BaseNode) -> BaseNode | None:
185
+ """Check if NodeGroup has unresolved external incoming connections.
186
+
187
+ Returns the first unresolved source node (or its parent if applicable) if found, None otherwise.
188
+ """
189
+ if not isinstance(node_group, NodeGroupNode):
190
+ return None
191
+
192
+ for conn in node_group.stored_connections.external_connections.incoming_connections:
193
+ source_node = conn.source_node
194
+ if source_node.state == NodeResolutionState.UNRESOLVED:
195
+ # Check if source has a parent group to use instead
196
+ if source_node.parent_group is not None and isinstance(source_node.parent_group, NodeGroupNode):
197
+ execution_env = source_node.parent_group.get_parameter_value(
198
+ source_node.parent_group.execution_environment.name
199
+ )
200
+ if execution_env != LOCAL_EXECUTION:
201
+ return source_node.parent_group
202
+ return source_node
203
+ return None
204
+
205
+ @staticmethod
206
+ def _determine_node_to_queue(
207
+ next_node: BaseNode, current_node: BaseNode, focus_stack_names: set[str]
208
+ ) -> BaseNode | None:
209
+ """Determine which node to queue - the next node or its parent group.
210
+
211
+ Returns None if the parent node group is already resolved.
212
+ """
213
+ if next_node.parent_group is None or not isinstance(next_node.parent_group, NodeGroupNode):
214
+ return next_node
215
+
216
+ parent_group = next_node.parent_group
217
+ execution_env = parent_group.get_parameter_value(parent_group.execution_environment.name)
218
+ if execution_env == LOCAL_EXECUTION:
219
+ return next_node
220
+
221
+ if parent_group.state == NodeResolutionState.RESOLVED:
222
+ logger.info(
223
+ "Sequential Resolution: Parent node group '%s' is already resolved, skipping child node '%s' (execution environment: %s)",
224
+ parent_group.name,
225
+ next_node.name,
226
+ execution_env,
227
+ )
228
+ return None
229
+
230
+ if parent_group.name in focus_stack_names:
231
+ msg = f"Cycle detected: parent node group '{parent_group.name}' is already in focus stack while processing dependency for '{current_node.name}'."
232
+ raise RuntimeError(msg)
233
+
234
+ logger.info(
235
+ "Sequential Resolution: Queuing parent node group '%s' instead of child node '%s' (execution environment: %s) - child is a dependency of '%s'",
236
+ parent_group.name,
237
+ next_node.name,
238
+ execution_env,
239
+ current_node.name,
240
+ )
241
+ return parent_group
242
+
123
243
  @staticmethod
124
244
  async def on_update(context: ResolutionContext) -> type[State] | None:
245
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
246
+
125
247
  current_node = context.current_node
126
248
  current_parameter = current_node.get_current_parameter()
127
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
128
249
 
129
- connections = GriptapeNodes.FlowManager().get_connections()
130
250
  if current_parameter is None:
131
251
  msg = "No current parameter set."
132
252
  raise ValueError(msg)
133
- # Get the next node
134
- next_node = connections.get_connected_node(current_node, current_parameter)
135
- if next_node:
136
- next_node, _ = next_node
253
+
254
+ connections = GriptapeNodes.FlowManager().get_connections()
255
+ next_node = EvaluateParameterState._get_next_node(current_node, current_parameter, connections)
256
+
137
257
  if next_node and next_node.state == NodeResolutionState.UNRESOLVED:
138
258
  focus_stack_names = {focus.node.name for focus in context.focus_stack}
139
- if next_node.name in focus_stack_names:
140
- msg = f"Cycle detected between node '{current_node.name}' and '{next_node.name}'."
141
- raise RuntimeError(msg)
259
+ EvaluateParameterState._check_for_cycle(next_node, current_node, focus_stack_names)
260
+
261
+ node_to_queue = EvaluateParameterState._determine_node_to_queue(next_node, current_node, focus_stack_names)
262
+ if node_to_queue is None:
263
+ return EvaluateParameterState._handle_parent_already_resolved(current_node)
142
264
 
143
- context.focus_stack.append(Focus(node=next_node))
265
+ context.focus_stack.append(Focus(node=node_to_queue))
144
266
  return InitializeSpotlightState
145
267
 
146
268
  if current_node.advance_parameter():
@@ -120,11 +120,13 @@ class ConfigureAgentRequest(RequestPayload):
120
120
 
121
121
  Args:
122
122
  prompt_driver: Dictionary of prompt driver configuration options
123
+ image_generation_driver: Dictionary of image generation driver configuration options
123
124
 
124
125
  Results: ConfigureAgentResultSuccess | ConfigureAgentResultFailure (configuration error)
125
126
  """
126
127
 
127
128
  prompt_driver: dict = field(default_factory=dict)
129
+ image_generation_driver: dict = field(default_factory=dict)
128
130
 
129
131
 
130
132
  @dataclass
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING, Any, NamedTuple
5
5
 
6
6
  if TYPE_CHECKING:
7
- from griptape_nodes.exe_types.node_types import NodeDependencies, NodeGroupProxyNode
7
+ from griptape_nodes.exe_types.node_types import NodeDependencies
8
8
  from griptape_nodes.node_library.workflow_registry import LibraryNameAndNodeType, WorkflowShape
9
9
  from griptape_nodes.retained_mode.events.node_events import SerializedNodeCommands, SetLockNodeStateRequest
10
10
  from griptape_nodes.retained_mode.events.workflow_events import ImportWorkflowAsReferencedSubFlowRequest
@@ -428,7 +428,8 @@ class PackageNodesAsSerializedFlowRequest(RequestPayload):
428
428
  node_names: List of node names to package as a flow (empty list will create StartFlow→EndFlow only with warning)
429
429
  start_node_type: Node type name for the artificial start node (None or omitted defaults to "StartFlow")
430
430
  end_node_type: Node type name for the artificial end node (None or omitted defaults to "EndFlow")
431
- start_end_specific_library_name: Library name containing the start/end nodes (defaults to "Griptape Nodes Library")
431
+ start_node_library_name: Library name containing the start node (defaults to "Griptape Nodes Library")
432
+ end_node_library_name: Library name containing the end node (defaults to "Griptape Nodes Library")
432
433
  entry_control_node_name: Name of the node that should receive the control flow entry (required if entry_control_parameter_name specified)
433
434
  entry_control_parameter_name: Name of the control parameter on the entry node (None for auto-detection of first available control parameter)
434
435
  output_parameter_prefix: Prefix for parameter names on the generated end node to avoid collisions (defaults to "packaged_node_")
@@ -440,13 +441,11 @@ class PackageNodesAsSerializedFlowRequest(RequestPayload):
440
441
  node_names: list[str] = field(default_factory=list)
441
442
  start_node_type: str | None = None
442
443
  end_node_type: str | None = None
443
- start_end_specific_library_name: str = "Griptape Nodes Library"
444
+ start_node_library_name: str = "Griptape Nodes Library"
445
+ end_node_library_name: str = "Griptape Nodes Library"
444
446
  entry_control_node_name: str | None = None
445
447
  entry_control_parameter_name: str | None = None
446
448
  output_parameter_prefix: str = "packaged_node_"
447
- proxy_node: NodeGroupProxyNode | None = (
448
- None # NodeGroupProxyNode if packaging nodes from a proxy, used to access original connections
449
- )
450
449
 
451
450
 
452
451
  @dataclass