griptape-nodes 0.37.0__py3-none-any.whl → 0.38.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 (38) hide show
  1. griptape_nodes/__init__.py +292 -132
  2. griptape_nodes/app/__init__.py +1 -6
  3. griptape_nodes/app/app.py +108 -76
  4. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +80 -5
  5. griptape_nodes/drivers/storage/local_storage_driver.py +5 -1
  6. griptape_nodes/exe_types/core_types.py +84 -3
  7. griptape_nodes/exe_types/node_types.py +260 -50
  8. griptape_nodes/machines/node_resolution.py +2 -14
  9. griptape_nodes/retained_mode/events/agent_events.py +7 -0
  10. griptape_nodes/retained_mode/events/base_events.py +16 -0
  11. griptape_nodes/retained_mode/events/library_events.py +26 -0
  12. griptape_nodes/retained_mode/events/parameter_events.py +31 -0
  13. griptape_nodes/retained_mode/griptape_nodes.py +32 -0
  14. griptape_nodes/retained_mode/managers/agent_manager.py +25 -12
  15. griptape_nodes/retained_mode/managers/config_manager.py +37 -4
  16. griptape_nodes/retained_mode/managers/event_manager.py +15 -0
  17. griptape_nodes/retained_mode/managers/flow_manager.py +64 -61
  18. griptape_nodes/retained_mode/managers/library_manager.py +215 -45
  19. griptape_nodes/retained_mode/managers/node_manager.py +344 -147
  20. griptape_nodes/retained_mode/managers/operation_manager.py +6 -0
  21. griptape_nodes/retained_mode/managers/os_manager.py +6 -1
  22. griptape_nodes/retained_mode/managers/secrets_manager.py +7 -2
  23. griptape_nodes/retained_mode/managers/settings.py +2 -11
  24. griptape_nodes/retained_mode/managers/static_files_manager.py +12 -3
  25. griptape_nodes/retained_mode/managers/version_compatibility_manager.py +105 -0
  26. griptape_nodes/retained_mode/managers/workflow_manager.py +4 -4
  27. griptape_nodes/updater/__init__.py +14 -8
  28. griptape_nodes/version_compatibility/__init__.py +1 -0
  29. griptape_nodes/version_compatibility/versions/__init__.py +1 -0
  30. griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +1 -0
  31. griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +77 -0
  32. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/METADATA +4 -1
  33. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/RECORD +36 -33
  34. griptape_nodes/app/app_websocket.py +0 -481
  35. griptape_nodes/app/nodes_api_socket_manager.py +0 -117
  36. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/WHEEL +0 -0
  37. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/entry_points.txt +0 -0
  38. {griptape_nodes-0.37.0.dist-info → griptape_nodes-0.38.0.dist-info}/licenses/LICENSE +0 -0
@@ -32,8 +32,10 @@ from griptape_nodes.retained_mode.events.execution_events import (
32
32
  ParameterValueUpdateEvent,
33
33
  )
34
34
  from griptape_nodes.retained_mode.events.parameter_events import (
35
+ RemoveElementEvent,
35
36
  RemoveParameterFromNodeRequest,
36
37
  )
38
+ from griptape_nodes.traits.options import Options
37
39
 
38
40
  logger = logging.getLogger("griptape_nodes")
39
41
 
@@ -62,6 +64,7 @@ class BaseNode(ABC):
62
64
  parameter_output_values: dict[str, Any]
63
65
  stop_flow: bool = False
64
66
  root_ui_element: BaseNodeElement
67
+ _tracked_parameters: list[BaseNodeElement]
65
68
 
66
69
  @property
67
70
  def parameters(self) -> list[Parameter]:
@@ -83,9 +86,12 @@ class BaseNode(ABC):
83
86
  else:
84
87
  self.metadata = metadata
85
88
  self.parameter_values = {}
86
- self.parameter_output_values = {}
89
+ self.parameter_output_values = TrackedParameterOutputValues(self)
87
90
  self.root_ui_element = BaseNodeElement()
91
+ # Set the node context for the root element
92
+ self.root_ui_element._node_context = self
88
93
  self.process_generator = None
94
+ self._tracked_parameters = []
89
95
 
90
96
  # This is gross and we need to have a universal pass on resolution state changes and emission of events. That's what this ticket does!
91
97
  # https://github.com/griptape-ai/griptape-nodes/issues/994
@@ -101,6 +107,12 @@ class BaseNode(ABC):
101
107
  )
102
108
  self.state = NodeResolutionState.UNRESOLVED
103
109
 
110
+ def emit_parameter_changes(self) -> None:
111
+ if self._tracked_parameters:
112
+ for parameter in self._tracked_parameters:
113
+ parameter._emit_alter_element_event_if_possible()
114
+ self._tracked_parameters.clear()
115
+
104
116
  def allow_incoming_connection(
105
117
  self,
106
118
  source_node: BaseNode, # noqa: ARG002
@@ -123,8 +135,8 @@ class BaseNode(ABC):
123
135
  self,
124
136
  source_node: BaseNode, # noqa: ARG002
125
137
  source_parameter: Parameter, # noqa: ARG002
126
- target_parameter: Parameter, # noqa: ARG002,
127
- modified_parameters_set: set[str], # noqa: ARG002
138
+ target_parameter: Parameter, # noqa: ARG002
139
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
128
140
  ) -> None:
129
141
  """Callback after a Connection has been established TO this Node."""
130
142
  return
@@ -134,7 +146,7 @@ class BaseNode(ABC):
134
146
  source_parameter: Parameter, # noqa: ARG002
135
147
  target_node: BaseNode, # noqa: ARG002
136
148
  target_parameter: Parameter, # noqa: ARG002
137
- modified_parameters_set: set[str], # noqa: ARG002
149
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
138
150
  ) -> None:
139
151
  """Callback after a Connection has been established OUT of this Node."""
140
152
  return
@@ -144,7 +156,7 @@ class BaseNode(ABC):
144
156
  source_node: BaseNode, # noqa: ARG002
145
157
  source_parameter: Parameter, # noqa: ARG002
146
158
  target_parameter: Parameter, # noqa: ARG002
147
- modified_parameters_set: set[str], # noqa: ARG002
159
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
148
160
  ) -> None:
149
161
  """Callback after a Connection TO this Node was REMOVED."""
150
162
  return
@@ -154,12 +166,17 @@ class BaseNode(ABC):
154
166
  source_parameter: Parameter, # noqa: ARG002
155
167
  target_node: BaseNode, # noqa: ARG002
156
168
  target_parameter: Parameter, # noqa: ARG002
157
- modified_parameters_set: set[str], # noqa: ARG002
169
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
158
170
  ) -> None:
159
171
  """Callback after a Connection OUT of this Node was REMOVED."""
160
172
  return
161
173
 
162
- def before_value_set(self, parameter: Parameter, value: Any, modified_parameters_set: set[str]) -> Any: # noqa: ARG002
174
+ def before_value_set(
175
+ self,
176
+ parameter: Parameter, # noqa: ARG002
177
+ value: Any,
178
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
179
+ ) -> Any:
163
180
  """Callback when a Parameter's value is ABOUT to be set.
164
181
 
165
182
  Custom nodes may elect to override the default behavior by implementing this function in their node code.
@@ -174,8 +191,7 @@ class BaseNode(ABC):
174
191
  Args:
175
192
  parameter: the Parameter on this node that is about to be changed
176
193
  value: the value intended to be set (this has already gone through any converters and validators on the Parameter)
177
- modified_parameters_set: A set of parameter names within this node that were modified as a result
178
- of this call. The Parameter this was called on does NOT need to be part of the return.
194
+ modified_parameters_set: A set of parameter names within this node that were modified as a result of this call.
179
195
 
180
196
  Returns:
181
197
  The final value to set for the Parameter. This gives the Node logic one last opportunity to mutate the value
@@ -184,7 +200,12 @@ class BaseNode(ABC):
184
200
  # Default behavior is to do nothing to the supplied value, and indicate no other modified Parameters.
185
201
  return value
186
202
 
187
- def after_value_set(self, parameter: Parameter, value: Any, modified_parameters_set: set[str]) -> None: # noqa: ARG002
203
+ def after_value_set(
204
+ self,
205
+ parameter: Parameter, # noqa: ARG002
206
+ value: Any, # noqa: ARG002
207
+ modified_parameters_set: set[str] | None = None, # noqa: ARG002
208
+ ) -> None:
188
209
  """Callback AFTER a Parameter's value was set.
189
210
 
190
211
  Custom nodes may elect to override the default behavior by implementing this function in their node code.
@@ -193,11 +214,19 @@ class BaseNode(ABC):
193
214
  changing other Parameters on the node. If other Parameters are changed, the engine needs a list of which
194
215
  ones have changed to cascade unresolved state.
195
216
 
217
+ NOTE: Subclasses can override this method with either signature:
218
+ - def after_value_set(self, parameter, value) -> None: (most common)
219
+ - def after_value_set(self, parameter, value, **kwargs) -> None: (advanced)
220
+ The base implementation uses **kwargs for compatibility with both patterns.
221
+ The engine will try calling with 2 arguments first, then fall back to 3 if needed.
222
+ Pyright may show false positive "incompatible override" warnings for the 2-argument
223
+ version - this is expected and the code will work correctly at runtime.
224
+
196
225
  Args:
197
226
  parameter: the Parameter on this node that was just changed
198
227
  value: the value that was set (already converted, validated, and possibly mutated by the node code)
199
- modified_parameters_set: A set of parameter names within this node that were modified as a result
200
- of this call. The Parameter this was called on does NOT need to be part of the return.
228
+ modified_parameters_set: Optional set of parameter names within this node
229
+ that were modified as a result of this call. The Parameter this was called on does NOT need to be part of the return.
201
230
 
202
231
  Returns:
203
232
  Nothing
@@ -205,7 +234,7 @@ class BaseNode(ABC):
205
234
  # Default behavior is to do nothing, and indicate no other modified Parameters.
206
235
  return None # noqa: RET501
207
236
 
208
- def after_settings_changed(self, modified_parameters_set: set[str]) -> None: # noqa: ARG002
237
+ def after_settings_changed(self, **kwargs: Any) -> None: # noqa: ARG002
209
238
  """Callback for when the settings of this Node are changed."""
210
239
  # Waiting for https://github.com/griptape-ai/griptape-nodes/issues/1309
211
240
  return
@@ -229,6 +258,7 @@ class BaseNode(ABC):
229
258
  msg = "Cannot have duplicate names on parameters."
230
259
  raise ValueError(msg)
231
260
  self.add_node_element(param)
261
+ self._emit_parameter_lifecycle_event(param)
232
262
 
233
263
  def remove_parameter_element_by_name(self, element_name: str) -> None:
234
264
  element = self.root_ui_element.find_element_by_name(element_name)
@@ -236,6 +266,9 @@ class BaseNode(ABC):
236
266
  self.remove_parameter_element(element)
237
267
 
238
268
  def remove_parameter_element(self, param: BaseNodeElement) -> None:
269
+ # Emit event before removal if it's a Parameter
270
+ if isinstance(param, Parameter):
271
+ self._emit_parameter_lifecycle_event(param)
239
272
  for child in param.find_elements_by_type(BaseNodeElement):
240
273
  self.remove_node_element(child)
241
274
  self.remove_node_element(param)
@@ -248,6 +281,8 @@ class BaseNode(ABC):
248
281
  return None
249
282
 
250
283
  def add_node_element(self, ui_element: BaseNodeElement) -> None:
284
+ # Set the node context before adding to ensure proper propagation
285
+ ui_element._node_context = self
251
286
  self.root_ui_element.add_child(ui_element)
252
287
 
253
288
  def remove_node_element(self, ui_element: BaseNodeElement) -> None:
@@ -269,7 +304,9 @@ class BaseNode(ABC):
269
304
  for name in names:
270
305
  parameter = self.get_parameter_by_name(name)
271
306
  if parameter is not None:
272
- parameter._ui_options["hide"] = not visible
307
+ ui_options = parameter.ui_options
308
+ ui_options["hide"] = not visible
309
+ parameter.ui_options = ui_options
273
310
 
274
311
  def get_message_by_name_or_element_id(self, element: str) -> ParameterMessage | None:
275
312
  element_items = self.root_ui_element.find_elements_by_type(ParameterMessage)
@@ -291,7 +328,9 @@ class BaseNode(ABC):
291
328
  for name in names:
292
329
  message = self.get_message_by_name_or_element_id(name)
293
330
  if message is not None:
294
- message.ui_options["hide"] = not visible
331
+ ui_options = message.ui_options
332
+ ui_options["hide"] = not visible
333
+ message.ui_options = ui_options
295
334
 
296
335
  def hide_message_by_name(self, names: str | list[str]) -> None:
297
336
  self._set_message_visibility(names, visible=False)
@@ -307,8 +346,91 @@ class BaseNode(ABC):
307
346
  """Shows one or more parameters by name."""
308
347
  self._set_parameter_visibility(names, visible=True)
309
348
 
349
+ def _update_option_choices(self, param: str, choices: list[str], default: str) -> None:
350
+ """Updates the model selection parameter with a new set of choices.
351
+
352
+ This method is intended to be called by subclasses to set the available
353
+ models for the driver. It modifies the 'model' parameter's `Options` trait
354
+ to reflect the provided choices.
355
+
356
+ Args:
357
+ param: The name of the parameter representing the model selection or the Parameter object itself.
358
+ choices: A list of model names to be set as choices.
359
+ default: The default model name to be set. It must be one of the provided choices.
360
+ """
361
+ parameter = self.get_parameter_by_name(param)
362
+ if parameter is not None:
363
+ trait = parameter.find_element_by_id("Options")
364
+ if trait and isinstance(trait, Options):
365
+ trait.choices = choices
366
+
367
+ if default in choices:
368
+ parameter.default_value = default
369
+ self.set_parameter_value(param, default)
370
+ else:
371
+ msg = f"Default model '{default}' is not in the provided choices."
372
+ raise ValueError(msg)
373
+ else:
374
+ msg = f"Parameter '{param}' not found for updating model choices."
375
+ raise ValueError(msg)
376
+
377
+ def _remove_options_trait(self, param: str) -> None:
378
+ """Removes the options trait from the specified parameter.
379
+
380
+ This method is intended to be called by subclasses to remove the
381
+ `Options` trait from a parameter, if it exists.
382
+
383
+ Args:
384
+ param: The name of the parameter from which to remove the `Options` trait.
385
+ """
386
+ parameter = self.get_parameter_by_name(param)
387
+ if parameter is not None:
388
+ trait = parameter.find_element_by_id("Options")
389
+ if trait and isinstance(trait, Options):
390
+ parameter.remove_trait(trait)
391
+ else:
392
+ msg = f"Parameter '{param}' not found for removing options trait."
393
+ raise ValueError(msg)
394
+
395
+ def _replace_param_by_name( # noqa: PLR0913
396
+ self,
397
+ param_name: str,
398
+ new_param_name: str,
399
+ new_output_type: str | None = None,
400
+ tooltip: str | list[dict] | None = None,
401
+ default_value: Any = None,
402
+ ui_options: dict | None = None,
403
+ ) -> None:
404
+ """Replaces a parameter in the node configuration.
405
+
406
+ This method is used to replace a parameter with a new name and
407
+ optionally update its tooltip and default value.
408
+
409
+ Args:
410
+ param_name (str): The name of the parameter to replace.
411
+ new_param_name (str): The new name for the parameter.
412
+ new_output_type (str, optional): The new output type for the parameter.
413
+ tooltip (str, list[dict], optional): The new tooltip for the parameter.
414
+ default_value (Any, optional): The new default value for the parameter.
415
+ ui_options (dict, optional): UI options for the parameter.
416
+ """
417
+ param = self.get_parameter_by_name(param_name)
418
+ if param is not None:
419
+ param.name = new_param_name
420
+ if tooltip is not None:
421
+ param.tooltip = tooltip
422
+ if default_value is not None:
423
+ param.default_value = default_value
424
+ if new_output_type is not None:
425
+ param.output_type = new_output_type
426
+ if ui_options is not None:
427
+ param.ui_options = ui_options
428
+ else:
429
+ msg = f"Parameter '{param_name}' not found in node configuration."
430
+ raise ValueError(msg)
431
+
310
432
  def initialize_spotlight(self) -> None:
311
- # Make a deep copy of all of the parameters and create the linked list.
433
+ # Create a linked list of parameters for spotlight navigation.
312
434
  curr_param = None
313
435
  prev_param = None
314
436
  for parameter in self.parameters:
@@ -317,14 +439,13 @@ class BaseNode(ABC):
317
439
  and ParameterTypeBuiltin.CONTROL_TYPE.value not in parameter.input_types
318
440
  ):
319
441
  if not self.current_spotlight_parameter or prev_param is None:
320
- # make a copy of the parameter and assign it to current spotlight
321
- param_copy = parameter.copy()
322
- self.current_spotlight_parameter = param_copy
323
- prev_param = param_copy
442
+ # Use the original parameter and assign it to current spotlight
443
+ self.current_spotlight_parameter = parameter
444
+ prev_param = parameter
324
445
  # go on to the next one because prev and next don't need to be set yet.
325
446
  continue
326
447
  # prev_param will have been initialized at this point
327
- curr_param = parameter.copy()
448
+ curr_param = parameter
328
449
  prev_param.next = curr_param
329
450
  curr_param.prev = prev_param
330
451
  prev_param = curr_param
@@ -349,7 +470,17 @@ class BaseNode(ABC):
349
470
  return parameter
350
471
  return None
351
472
 
352
- def set_parameter_value(self, param_name: str, value: Any) -> set[str] | None:
473
+ def get_element_by_name_and_type(
474
+ self, elem_name: str, element_type: type[BaseNodeElement] | None = None
475
+ ) -> BaseNodeElement | None:
476
+ find_type = element_type if element_type is not None else BaseNodeElement
477
+ element_items = self.root_ui_element.find_elements_by_type(find_type)
478
+ for element_item in element_items:
479
+ if elem_name == element_item.name:
480
+ return element_item
481
+ return None
482
+
483
+ def set_parameter_value(self, param_name: str, value: Any) -> None:
353
484
  """Attempt to set a Parameter's value.
354
485
 
355
486
  The Node may choose to store a different value (or type) than what was passed in.
@@ -392,25 +523,24 @@ class BaseNode(ABC):
392
523
  for validator in parameter.validators:
393
524
  validator(parameter, candidate_value)
394
525
 
395
- # Keep track of which other parameters got modified as a result of any node-specific logic.
396
- modified_parameters: set[str] = set()
397
-
398
526
  # Allow custom node logic to prepare and possibly mutate the value before it is actually set.
399
527
  # Record any parameters modified for cascading.
400
- final_value = self.before_value_set(
401
- parameter=parameter,
402
- value=candidate_value,
403
- modified_parameters_set=modified_parameters,
404
- )
528
+ try:
529
+ final_value = self.before_value_set(parameter=parameter, value=candidate_value)
530
+ except TypeError:
531
+ final_value = self.before_value_set(
532
+ parameter=parameter, value=candidate_value, modified_parameters_set=set()
533
+ )
405
534
  # ACTUALLY SET THE NEW VALUE
406
535
  self.parameter_values[param_name] = final_value
536
+
407
537
  # If a parameter value has been set at the top level of a container, wipe all children.
408
538
  # Allow custom node logic to respond after it's been set. Record any modified parameters for cascading.
409
- self.after_value_set(
410
- parameter=parameter,
411
- value=final_value,
412
- modified_parameters_set=modified_parameters,
413
- )
539
+ try:
540
+ self.after_value_set(parameter=parameter, value=final_value)
541
+ except TypeError:
542
+ self.after_value_set(parameter=parameter, value=final_value, modified_parameters_set=set())
543
+ self._emit_parameter_lifecycle_event(parameter)
414
544
  # handle with container parameters
415
545
  if parameter.parent_container_name is not None:
416
546
  # Does it have a parent container
@@ -421,13 +551,7 @@ class BaseNode(ABC):
421
551
  new_parent_value = handle_container_parameter(self, parent_parameter)
422
552
  if new_parent_value is not None:
423
553
  # set that new value if it exists.
424
- modified_parameters_from_container = self.set_parameter_value(
425
- parameter.parent_container_name, new_parent_value
426
- )
427
- # Return the complete set of modified parameters.
428
- if modified_parameters_from_container:
429
- modified_parameters = modified_parameters | modified_parameters_from_container
430
- return modified_parameters
554
+ self.set_parameter_value(parameter.parent_container_name, new_parent_value)
431
555
 
432
556
  def kill_parameter_children(self, parameter: Parameter) -> None:
433
557
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
@@ -436,13 +560,13 @@ class BaseNode(ABC):
436
560
  GriptapeNodes.handle_request(RemoveParameterFromNodeRequest(parameter_name=child.name, node_name=self.name))
437
561
 
438
562
  def get_parameter_value(self, param_name: str) -> Any:
439
- if param_name in self.parameter_values:
440
- return self.parameter_values[param_name]
441
563
  param = self.get_parameter_by_name(param_name)
442
- if param:
564
+ if param and isinstance(param, ParameterContainer):
443
565
  value = handle_container_parameter(self, param)
444
566
  if value:
445
567
  return value
568
+ if param_name in self.parameter_values:
569
+ return self.parameter_values[param_name]
446
570
  return param.default_value if param else None
447
571
 
448
572
  def get_parameter_list_value(self, param: str) -> list:
@@ -543,11 +667,16 @@ class BaseNode(ABC):
543
667
  self.state = NodeResolutionState.UNRESOLVED
544
668
  # delete all output values potentially generated
545
669
  self.parameter_output_values.clear()
546
- # Remove the current spotlight
547
- while self.current_spotlight_parameter is not None:
548
- temp = self.current_spotlight_parameter.next
549
- del self.current_spotlight_parameter
550
- self.current_spotlight_parameter = temp
670
+ # Clear the spotlight linked list
671
+ # First, clear all next/prev pointers to break the linked list
672
+ current = self.current_spotlight_parameter
673
+ while current is not None:
674
+ next_param = current.next
675
+ current.next = None
676
+ current.prev = None
677
+ current = next_param
678
+ # Then clear the reference to the first spotlight parameter
679
+ self.current_spotlight_parameter = None
551
680
 
552
681
  def append_value_to_parameter(self, parameter_name: str, value: Any) -> None:
553
682
  # Add the value to the node
@@ -685,6 +814,25 @@ class BaseNode(ABC):
685
814
  # Use reorder_elements to apply the move
686
815
  self.reorder_elements(list(new_order))
687
816
 
817
+ def _emit_parameter_lifecycle_event(self, parameter: BaseNodeElement, *, remove: bool = False) -> None:
818
+ """Emit an AlterElementEvent for parameter add/remove operations."""
819
+ from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
820
+ from griptape_nodes.retained_mode.events.parameter_events import AlterElementEvent
821
+
822
+ # Create event data using the parameter's to_event method
823
+ if remove:
824
+ event = ExecutionGriptapeNodeEvent(
825
+ wrapped_event=ExecutionEvent(payload=RemoveElementEvent(element_id=parameter.element_id))
826
+ )
827
+ else:
828
+ event_data = parameter.to_event(self)
829
+
830
+ # Publish the event
831
+ event = ExecutionGriptapeNodeEvent(
832
+ wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
833
+ )
834
+ EventBus.publish_event(event)
835
+
688
836
  def _get_element_name(self, element: str | int, element_names: list[str]) -> str:
689
837
  """Convert an element identifier (name or index) to its name.
690
838
 
@@ -777,6 +925,68 @@ class BaseNode(ABC):
777
925
  self.reorder_elements(list(new_order))
778
926
 
779
927
 
928
+ class TrackedParameterOutputValues(dict[str, Any]):
929
+ """A dictionary that tracks modifications and emits AlterElementEvent when parameter output values change."""
930
+
931
+ def __init__(self, node: BaseNode) -> None:
932
+ super().__init__()
933
+ self._node = node
934
+
935
+ def __setitem__(self, key: str, value: Any) -> None:
936
+ old_value = self.get(key)
937
+ super().__setitem__(key, value)
938
+
939
+ # Only emit event if value actually changed
940
+ if old_value != value:
941
+ self._emit_parameter_change_event(key, value)
942
+
943
+ def __delitem__(self, key: str) -> None:
944
+ if key in self:
945
+ super().__delitem__(key)
946
+ self._emit_parameter_change_event(key, None, deleted=True)
947
+
948
+ def clear(self) -> None:
949
+ if self: # Only emit events if there were values to clear
950
+ keys_to_clear = list(self.keys())
951
+ super().clear()
952
+ for key in keys_to_clear:
953
+ self._emit_parameter_change_event(key, None, deleted=True)
954
+
955
+ def update(self, *args, **kwargs) -> None:
956
+ # Handle both dict.update(other) and dict.update(**kwargs) patterns
957
+ if args:
958
+ other = args[0]
959
+ if hasattr(other, "items"):
960
+ for key, value in other.items():
961
+ self[key] = value # Use __setitem__ to trigger events
962
+ else:
963
+ for key, value in other:
964
+ self[key] = value
965
+
966
+ for key, value in kwargs.items():
967
+ self[key] = value
968
+
969
+ def _emit_parameter_change_event(self, parameter_name: str, value: Any, *, deleted: bool = False) -> None:
970
+ """Emit an AlterElementEvent for parameter output value changes."""
971
+ parameter = self._node.get_parameter_by_name(parameter_name)
972
+ if parameter is not None:
973
+ from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
974
+ from griptape_nodes.retained_mode.events.parameter_events import AlterElementEvent
975
+
976
+ # Create event data using the parameter's to_event method
977
+ event_data = parameter.to_event(self._node)
978
+ event_data["value"] = value
979
+
980
+ # Add modification metadata
981
+ event_data["modification_type"] = "deleted" if deleted else "set"
982
+
983
+ # Publish the event
984
+ event = ExecutionGriptapeNodeEvent(
985
+ wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
986
+ )
987
+ EventBus.publish_event(event)
988
+
989
+
780
990
  class ControlNode(BaseNode):
781
991
  # Control Nodes may have one Control Input Port and at least one Control Output Port
782
992
  def __init__(self, name: str, metadata: dict[Any, Any] | None = None) -> None:
@@ -28,7 +28,6 @@ from griptape_nodes.retained_mode.events.execution_events import (
28
28
  ResumeNodeProcessingEvent,
29
29
  )
30
30
  from griptape_nodes.retained_mode.events.parameter_events import (
31
- AlterElementEvent,
32
31
  SetParameterValueRequest,
33
32
  )
34
33
 
@@ -199,7 +198,7 @@ class ExecuteNodeState(State):
199
198
  current_node.parameter_output_values.clear()
200
199
 
201
200
  @staticmethod
202
- def on_enter(context: ResolutionContext) -> type[State] | None: # noqa: C901
201
+ def on_enter(context: ResolutionContext) -> type[State] | None:
203
202
  current_node = context.focus_stack[-1].node
204
203
  # Clear all of the current output values
205
204
  ExecuteNodeState.clear_parameter_output_values(context)
@@ -210,18 +209,7 @@ class ExecuteNodeState(State):
210
209
  # If a parameter value is not already set
211
210
  value = current_node.get_parameter_value(parameter.name)
212
211
  if value is not None:
213
- modified_parameters = current_node.set_parameter_value(parameter.name, value)
214
- if modified_parameters:
215
- for modified_parameter_name in modified_parameters:
216
- # TODO: https://github.com/griptape-ai/griptape-nodes/issues/865
217
- modified_parameter = current_node.get_parameter_by_name(modified_parameter_name)
218
- if modified_parameter is not None:
219
- modified_request = AlterElementEvent(
220
- element_details=modified_parameter.to_event(current_node)
221
- )
222
- EventBus.publish_event(
223
- ExecutionGriptapeNodeEvent(ExecutionEvent(payload=modified_request))
224
- )
212
+ current_node.set_parameter_value(parameter.name, value)
225
213
 
226
214
  if parameter.name in current_node.parameter_values:
227
215
  parameter_value = current_node.get_parameter_value(parameter.name)
@@ -3,6 +3,7 @@ from dataclasses import dataclass, field
3
3
  from griptape.memory.structure import Run
4
4
 
5
5
  from griptape_nodes.retained_mode.events.base_events import (
6
+ ExecutionPayload,
6
7
  RequestPayload,
7
8
  ResultPayloadFailure,
8
9
  ResultPayloadSuccess,
@@ -81,3 +82,9 @@ class ResetAgentConversationMemoryResultSuccess(WorkflowNotAlteredMixin, ResultP
81
82
  @PayloadRegistry.register
82
83
  class ResetAgentConversationMemoryResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
83
84
  pass
85
+
86
+
87
+ @dataclass
88
+ @PayloadRegistry.register
89
+ class AgentStreamEvent(ExecutionPayload):
90
+ token: str
@@ -557,3 +557,19 @@ class ProgressEvent(GtBaseEvent):
557
557
  value: Any = field()
558
558
  node_name: str = field()
559
559
  parameter_name: str = field()
560
+
561
+
562
+ # Special internal request for flushing parameter changes
563
+ @dataclass(kw_only=True)
564
+ class FlushParameterChangesRequest(RequestPayload, WorkflowNotAlteredMixin):
565
+ pass
566
+
567
+
568
+ @dataclass
569
+ class FlushParameterChangesResultSuccess(ResultPayloadSuccess):
570
+ pass
571
+
572
+
573
+ @dataclass
574
+ class FlushParameterChangesResultFailure(ResultPayloadFailure):
575
+ pass
@@ -196,3 +196,29 @@ class UnloadLibraryFromRegistryResultSuccess(WorkflowAlteredMixin, ResultPayload
196
196
  @PayloadRegistry.register
197
197
  class UnloadLibraryFromRegistryResultFailure(ResultPayloadFailure):
198
198
  pass
199
+
200
+
201
+ @dataclass
202
+ @PayloadRegistry.register
203
+ class ReloadAllLibrariesRequest(RequestPayload):
204
+ """WARNING: This request will CLEAR ALL CURRENT WORKFLOW STATE!
205
+
206
+ Reloading all libraries requires clearing all existing workflows, nodes, and execution state
207
+ because there is no way to comprehensively erase references to old Python modules.
208
+ All current work will be lost and must be recreated after the reload operation completes.
209
+
210
+ Use this operation only when you need to pick up changes to library code during development
211
+ or when library corruption requires a complete reset.
212
+ """
213
+
214
+
215
+ @dataclass
216
+ @PayloadRegistry.register
217
+ class ReloadAllLibrariesResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
218
+ pass
219
+
220
+
221
+ @dataclass
222
+ @PayloadRegistry.register
223
+ class ReloadAllLibrariesResultFailure(ResultPayloadFailure):
224
+ pass
@@ -298,3 +298,34 @@ class GetNodeElementDetailsResultFailure(WorkflowNotAlteredMixin, ResultPayloadF
298
298
  @PayloadRegistry.register
299
299
  class AlterElementEvent(ExecutionPayload):
300
300
  element_details: dict[str, Any]
301
+
302
+
303
+ @dataclass
304
+ @PayloadRegistry.register
305
+ class RenameParameterRequest(RequestPayload):
306
+ parameter_name: str
307
+ new_parameter_name: str
308
+ # If node name is None, use the Current Context
309
+ node_name: str | None = None
310
+ # initial_setup prevents unnecessary work when we are loading a workflow from a file.
311
+ initial_setup: bool = False
312
+
313
+
314
+ @dataclass
315
+ @PayloadRegistry.register
316
+ class RenameParameterResultSuccess(WorkflowAlteredMixin, ResultPayloadSuccess):
317
+ old_parameter_name: str
318
+ new_parameter_name: str
319
+ node_name: str
320
+
321
+
322
+ @dataclass
323
+ @PayloadRegistry.register
324
+ class RenameParameterResultFailure(ResultPayloadFailure):
325
+ pass
326
+
327
+
328
+ @dataclass
329
+ @PayloadRegistry.register
330
+ class RemoveElementEvent(ExecutionPayload):
331
+ element_id: str