griptape-nodes 0.51.2__py3-none-any.whl → 0.52.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 (46) hide show
  1. griptape_nodes/__init__.py +5 -4
  2. griptape_nodes/app/api.py +27 -24
  3. griptape_nodes/app/app.py +243 -221
  4. griptape_nodes/app/watch.py +17 -2
  5. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
  6. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
  7. griptape_nodes/exe_types/core_types.py +16 -4
  8. griptape_nodes/exe_types/node_types.py +74 -16
  9. griptape_nodes/machines/control_flow.py +21 -26
  10. griptape_nodes/machines/fsm.py +16 -16
  11. griptape_nodes/machines/node_resolution.py +30 -119
  12. griptape_nodes/mcp_server/server.py +14 -10
  13. griptape_nodes/mcp_server/ws_request_manager.py +2 -2
  14. griptape_nodes/node_library/workflow_registry.py +5 -0
  15. griptape_nodes/retained_mode/events/base_events.py +12 -7
  16. griptape_nodes/retained_mode/events/execution_events.py +0 -6
  17. griptape_nodes/retained_mode/events/node_events.py +38 -0
  18. griptape_nodes/retained_mode/events/parameter_events.py +11 -0
  19. griptape_nodes/retained_mode/events/variable_events.py +361 -0
  20. griptape_nodes/retained_mode/events/workflow_events.py +35 -0
  21. griptape_nodes/retained_mode/griptape_nodes.py +61 -26
  22. griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
  23. griptape_nodes/retained_mode/managers/event_manager.py +215 -74
  24. griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
  25. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
  26. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
  27. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
  28. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
  30. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +20 -19
  33. griptape_nodes/retained_mode/managers/node_manager.py +83 -8
  34. griptape_nodes/retained_mode/managers/object_manager.py +4 -0
  35. griptape_nodes/retained_mode/managers/settings.py +1 -0
  36. griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
  37. griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
  39. griptape_nodes/retained_mode/variable_types.py +18 -0
  40. griptape_nodes/utils/__init__.py +4 -0
  41. griptape_nodes/utils/async_utils.py +89 -0
  42. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/METADATA +2 -3
  43. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/RECORD +45 -42
  44. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/WHEEL +1 -1
  45. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
  46. {griptape_nodes-0.51.2.dist-info → griptape_nodes-0.52.0.dist-info}/entry_points.txt +0 -0
@@ -1,21 +1,18 @@
1
1
  import logging
2
- from queue import Queue
3
2
  from typing import Any
4
3
 
5
- from griptape.events import BaseEvent, EventBus, EventListener
6
-
7
4
  from griptape_nodes.bootstrap.workflow_executors.workflow_executor import WorkflowExecutor
8
5
  from griptape_nodes.drivers.storage import StorageBackend
9
6
  from griptape_nodes.exe_types.node_types import EndNode, StartNode
10
7
  from griptape_nodes.retained_mode.events.base_events import (
11
- AppEvent,
12
8
  EventRequest,
13
9
  ExecutionGriptapeNodeEvent,
14
- GriptapeNodeEvent,
15
- ProgressEvent,
16
10
  )
17
- from griptape_nodes.retained_mode.events.execution_events import SingleExecutionStepRequest, StartFlowRequest
11
+ from griptape_nodes.retained_mode.events.execution_events import StartFlowRequest
18
12
  from griptape_nodes.retained_mode.events.parameter_events import SetParameterValueRequest
13
+ from griptape_nodes.retained_mode.events.workflow_events import (
14
+ RunWorkflowFromScratchRequest,
15
+ )
19
16
  from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
20
17
 
21
18
  logger = logging.getLogger(__name__)
@@ -26,10 +23,6 @@ class LocalExecutorError(Exception):
26
23
 
27
24
 
28
25
  class LocalWorkflowExecutor(WorkflowExecutor):
29
- def __init__(self) -> None:
30
- self.queue = Queue()
31
- self.output: dict | None = None
32
-
33
26
  def _load_flow_for_workflow(self) -> str:
34
27
  try:
35
28
  context_manager = GriptapeNodes.ContextManager()
@@ -53,69 +46,10 @@ class LocalWorkflowExecutor(WorkflowExecutor):
53
46
  logger.exception(msg)
54
47
  raise LocalExecutorError(msg) from e
55
48
 
56
- def _handle_event(self, event: BaseEvent) -> None:
57
- try:
58
- match event:
59
- case GriptapeNodeEvent():
60
- self.__handle_node_event(event)
61
- case ExecutionGriptapeNodeEvent():
62
- self.__handle_execution_node_event(event)
63
- case ProgressEvent():
64
- self.__handle_progress_event(event)
65
- case AppEvent():
66
- self.__handle_app_event(event)
67
- case _:
68
- msg = f"Unknown event type: {type(event)}"
69
- logger.info(msg)
70
- self.queue.put(event)
71
- except Exception as e:
72
- logger.info(e)
73
-
74
- def __handle_node_event(self, event: GriptapeNodeEvent) -> None:
75
- result_event = event.wrapped_event
76
- event_json = result_event.json()
77
- event_log = f"GriptapeNodeEvent: {event_json}"
78
- logger.info(event_log)
79
-
80
- def __handle_execution_node_event(self, event: ExecutionGriptapeNodeEvent) -> None:
81
- result_event = event.wrapped_event
82
- if type(result_event.payload).__name__ == "NodeStartProcessEvent":
83
- event_log = f"NodeStartProcessEvent: {result_event.payload}"
84
- logger.info(event_log)
85
-
86
- elif type(result_event.payload).__name__ == "ResumeNodeProcessingEvent":
87
- event_log = f"ResumeNodeProcessingEvent: {result_event.payload}"
88
- logger.info(event_log)
89
-
90
- # Here we need to handle the resume event since this is the callback mechanism
91
- # for the flow to be resumed for any Node that yields a generator in its process method.
92
- node_name = result_event.payload.node_name
93
- flow_name = GriptapeNodes.NodeManager().get_node_parent_flow_by_name(node_name)
94
- event_request = EventRequest(request=SingleExecutionStepRequest(flow_name=flow_name))
95
- self.queue.put(event_request)
96
-
97
- elif type(result_event.payload).__name__ == "NodeFinishProcessEvent":
98
- event_log = f"NodeFinishProcessEvent: {result_event.payload}"
99
- logger.info(event_log)
100
-
101
- else:
102
- event_log = f"ExecutionGriptapeNodeEvent: {result_event.payload}"
103
- logger.info(event_log)
104
-
105
- self.queue.put(event)
106
-
107
- def __handle_progress_event(self, gt_event: ProgressEvent) -> None:
108
- event_log = f"ProgressEvent: {gt_event}"
109
- logger.info(event_log)
110
-
111
- def __handle_app_event(self, event: AppEvent) -> None:
112
- event_log = f"AppEvent: {event.payload}"
113
- logger.info(event_log)
114
-
115
49
  def _submit_output(self, output: dict) -> None:
116
50
  self.output = output
117
51
 
118
- def _set_input_for_flow(self, flow_name: str, flow_input: dict[str, dict]) -> None:
52
+ async def _set_input_for_flow(self, flow_name: str, flow_input: dict[str, dict]) -> None:
119
53
  control_flow = GriptapeNodes.FlowManager().get_flow_by_name(flow_name)
120
54
  nodes = control_flow.nodes
121
55
  for node_name, node in nodes.items():
@@ -128,7 +62,7 @@ class LocalWorkflowExecutor(WorkflowExecutor):
128
62
  value=parameter_value,
129
63
  node_name=node_name,
130
64
  )
131
- set_parameter_value_result = GriptapeNodes.handle_request(set_parameter_value_request)
65
+ set_parameter_value_result = await GriptapeNodes.ahandle_request(set_parameter_value_request)
132
66
 
133
67
  if set_parameter_value_result.failed():
134
68
  msg = f"Failed to set parameter {parameter_name} for node {node_name}."
@@ -144,12 +78,54 @@ class LocalWorkflowExecutor(WorkflowExecutor):
144
78
 
145
79
  return output
146
80
 
147
- def run(
81
+ async def _load_workflow_from_path(self, workflow_path: str) -> None:
82
+ """Load a workflow from a file path."""
83
+
84
+ def _raise_load_error(msg: str) -> None:
85
+ raise LocalExecutorError(msg)
86
+
87
+ try:
88
+ # Use the RunWorkflowFromScratchRequest to load the workflow
89
+ request = RunWorkflowFromScratchRequest(file_path=workflow_path)
90
+ result = await GriptapeNodes.ahandle_request(request)
91
+
92
+ logger.info("Successfully loaded workflow from %s", workflow_path)
93
+ except Exception as e:
94
+ msg = f"Error loading workflow from path {workflow_path}: {e}"
95
+ logger.exception(msg)
96
+ raise LocalExecutorError(msg) from e
97
+
98
+ if result.failed():
99
+ msg = f"Failed to load workflow from path {workflow_path}"
100
+ _raise_load_error(msg)
101
+
102
+ async def _handle_event_request(self, event: EventRequest) -> None:
103
+ """Handle EventRequest objects by processing them through GriptapeNodes."""
104
+ await GriptapeNodes.ahandle_request(event.request)
105
+
106
+ async def _handle_execution_event(
107
+ self, event: ExecutionGriptapeNodeEvent, flow_name: str
108
+ ) -> tuple[bool, Exception | None]:
109
+ """Handle ExecutionGriptapeNodeEvent and return (is_finished, error)."""
110
+ result_event = event.wrapped_event
111
+
112
+ if type(result_event.payload).__name__ == "ControlFlowResolvedEvent":
113
+ self._submit_output(self._get_output_for_flow(flow_name=flow_name))
114
+ logger.info("Workflow finished!")
115
+ return True, None
116
+ if type(result_event.payload).__name__ == "ControlFlowCancelledEvent":
117
+ msg = "Control flow cancelled"
118
+ logger.error(msg)
119
+ return True, LocalExecutorError(msg)
120
+
121
+ return False, None
122
+
123
+ async def arun(
148
124
  self,
149
125
  workflow_name: str,
150
126
  flow_input: Any,
151
127
  storage_backend: StorageBackend = StorageBackend.LOCAL,
152
- **kwargs: Any, # noqa: ARG002
128
+ **kwargs: Any,
153
129
  ) -> None:
154
130
  """Executes a local workflow.
155
131
 
@@ -165,23 +141,24 @@ class LocalWorkflowExecutor(WorkflowExecutor):
165
141
  None
166
142
  """
167
143
  logger.info("Executing workflow: %s", workflow_name)
168
-
169
- EventBus.add_event_listener(
170
- event_listener=EventListener(
171
- on_event=self._handle_event,
172
- )
173
- )
144
+ GriptapeNodes.EventManager().initialize_queue()
174
145
 
175
146
  # Set the storage backend
176
147
  self._set_storage_backend(storage_backend=storage_backend)
148
+
149
+ # Load workflow from file if workflow_path is provided
150
+ workflow_path = kwargs.get("workflow_path")
151
+ if workflow_path:
152
+ await self._load_workflow_from_path(workflow_path)
153
+
177
154
  # Load the flow
178
155
  flow_name = self._load_flow_for_workflow()
179
156
  # Now let's set the input to the flow
180
- self._set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
157
+ await self._set_input_for_flow(flow_name=flow_name, flow_input=flow_input)
181
158
 
182
159
  # Now send the run command to actually execute it
183
160
  start_flow_request = StartFlowRequest(flow_name=flow_name)
184
- start_flow_result = GriptapeNodes.handle_request(start_flow_request)
161
+ start_flow_result = await GriptapeNodes.ahandle_request(start_flow_request)
185
162
 
186
163
  if start_flow_result.failed():
187
164
  msg = f"Failed to start flow {flow_name}"
@@ -192,32 +169,18 @@ class LocalWorkflowExecutor(WorkflowExecutor):
192
169
  # Wait for the control flow to finish
193
170
  is_flow_finished = False
194
171
  error: Exception | None = None
172
+
173
+ event_queue = GriptapeNodes.EventManager().event_queue
195
174
  while not is_flow_finished:
196
175
  try:
197
- event = self.queue.get(block=True)
176
+ event = await event_queue.get()
198
177
 
199
178
  if isinstance(event, EventRequest):
200
- # Handle EventRequest objects by processing them through GriptapeNodes
201
- request_payload = event.request
202
- GriptapeNodes.handle_request(
203
- request_payload, response_topic=event.response_topic, request_id=event.request_id
204
- )
179
+ await self._handle_event_request(event)
205
180
  elif isinstance(event, ExecutionGriptapeNodeEvent):
206
- result_event = event.wrapped_event
207
-
208
- if type(result_event.payload).__name__ == "ControlFlowResolvedEvent":
209
- self._submit_output(self._get_output_for_flow(flow_name=flow_name))
210
- is_flow_finished = True
211
- logger.info("Workflow finished!")
212
- elif type(result_event.payload).__name__ == "ControlFlowCancelledEvent":
213
- msg = "Control flow cancelled"
214
- is_flow_finished = True
215
- logger.error(msg)
216
- error = LocalExecutorError(msg)
217
- else:
218
- logger.info("Unknown event type encountered: %s", type(event))
219
-
220
- self.queue.task_done()
181
+ is_flow_finished, error = await self._handle_execution_event(event, flow_name)
182
+
183
+ event_queue.task_done()
221
184
 
222
185
  except Exception as e:
223
186
  msg = f"Error handling queue event: {e}"
@@ -1,5 +1,6 @@
1
+ import asyncio
1
2
  import logging
2
- from abc import ABC, abstractmethod
3
+ from abc import abstractmethod
3
4
  from typing import Any
4
5
 
5
6
  from griptape_nodes.drivers.storage import StorageBackend
@@ -7,8 +8,10 @@ from griptape_nodes.drivers.storage import StorageBackend
7
8
  logger = logging.getLogger(__name__)
8
9
 
9
10
 
10
- class WorkflowExecutor(ABC):
11
- @abstractmethod
11
+ class WorkflowExecutor:
12
+ def __init__(self) -> None:
13
+ self.output: dict | None = None
14
+
12
15
  def run(
13
16
  self,
14
17
  workflow_name: str,
@@ -16,4 +19,13 @@ class WorkflowExecutor(ABC):
16
19
  storage_backend: StorageBackend = StorageBackend.LOCAL,
17
20
  **kwargs: Any,
18
21
  ) -> None:
19
- pass
22
+ return asyncio.run(self.arun(workflow_name, flow_input, storage_backend, **kwargs))
23
+
24
+ @abstractmethod
25
+ async def arun(
26
+ self,
27
+ workflow_name: str,
28
+ flow_input: Any,
29
+ storage_backend: StorageBackend = StorageBackend.LOCAL,
30
+ **kwargs: Any,
31
+ ) -> None: ...
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
12
12
  from types import TracebackType
13
13
 
14
14
  from griptape_nodes.exe_types.node_types import BaseNode
15
+
15
16
  T = TypeVar("T", bound="Parameter")
16
17
  N = TypeVar("N", bound="BaseNodeElement")
17
18
 
@@ -253,11 +254,12 @@ class BaseNodeElement:
253
254
 
254
255
  def _emit_alter_element_event_if_possible(self) -> None:
255
256
  """Emit an AlterElementEvent if we have node context and the necessary dependencies."""
257
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
258
+
256
259
  if self._node_context is None:
257
260
  return
258
261
 
259
262
  # Import here to avoid circular dependencies
260
- from griptape.events import EventBus
261
263
 
262
264
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
263
265
  from griptape_nodes.retained_mode.events.parameter_events import AlterElementEvent
@@ -281,7 +283,8 @@ class BaseNodeElement:
281
283
  event = ExecutionGriptapeNodeEvent(
282
284
  wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
283
285
  )
284
- EventBus.publish_event(event)
286
+
287
+ GriptapeNodes.EventManager().put_event(event)
285
288
  self._changes.clear()
286
289
 
287
290
  def to_dict(self) -> dict[str, Any]:
@@ -712,7 +715,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
712
715
 
713
716
  # "settable" here means whether it can be assigned to during regular business operation.
714
717
  # During save/load, this value IS still serialized to save its proper state.
715
- settable: bool = True
718
+ _settable: bool = True
716
719
 
717
720
  # "serializable" controls whether parameter values should be serialized during save/load operations.
718
721
  # Set to False for parameters containing non-serializable types (ImageDrivers, PromptDrivers, file handles, etc.)
@@ -768,7 +771,7 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
768
771
  self.tooltip_as_input = tooltip_as_input
769
772
  self.tooltip_as_property = tooltip_as_property
770
773
  self.tooltip_as_output = tooltip_as_output
771
- self.settable = settable
774
+ self._settable = settable
772
775
  self.serializable = serializable
773
776
  self.user_defined = user_defined
774
777
  if allowed_modes is None:
@@ -909,6 +912,15 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
909
912
  self._changes["mode_allowed_output"] = ParameterMode.OUTPUT in value
910
913
  self._changes["mode_allowed_property"] = ParameterMode.PROPERTY in value
911
914
 
915
+ @property
916
+ def settable(self) -> bool:
917
+ return self._settable
918
+
919
+ @settable.setter
920
+ @BaseNodeElement.emits_update_on_write
921
+ def settable(self, value: bool) -> None:
922
+ self._settable = value
923
+
912
924
  @property
913
925
  def ui_options(self) -> dict:
914
926
  ui_options = {}
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  from abc import ABC, abstractmethod
5
6
  from collections.abc import Callable, Generator, Iterable
7
+ from concurrent.futures import ThreadPoolExecutor
6
8
  from enum import StrEnum, auto
7
9
  from typing import Any, NamedTuple, TypeVar
8
10
 
9
- from griptape.events import BaseEvent, EventBus
10
-
11
11
  from griptape_nodes.exe_types.core_types import (
12
12
  BaseNodeElement,
13
13
  ControlParameterInput,
@@ -120,11 +120,14 @@ class BaseNode(ABC):
120
120
  # 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!
121
121
  # https://github.com/griptape-ai/griptape-nodes/issues/994
122
122
  def make_node_unresolved(self, current_states_to_trigger_change_event: set[NodeResolutionState] | None) -> None:
123
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
124
+
123
125
  # See if the current state is in the set of states to trigger a change event.
124
126
  if current_states_to_trigger_change_event is not None and self.state in current_states_to_trigger_change_event:
125
127
  # Trigger the change event.
126
128
  # Send an event to the GUI so it knows this node has changed resolution state.
127
- EventBus.publish_event(
129
+
130
+ GriptapeNodes.EventManager().put_event(
128
131
  ExecutionGriptapeNodeEvent(
129
132
  wrapped_event=ExecutionEvent(payload=NodeUnresolvedEvent(node_name=self.name))
130
133
  )
@@ -284,10 +287,6 @@ class BaseNode(ABC):
284
287
  # Waiting for https://github.com/griptape-ai/griptape-nodes/issues/1309
285
288
  return
286
289
 
287
- def on_griptape_event(self, event: BaseEvent) -> None: # noqa: ARG002
288
- """Callback for when a Griptape Event comes destined for this Node."""
289
- return
290
-
291
290
  def on_node_message_received(
292
291
  self,
293
292
  optional_element_name: str | None, # noqa: ARG002
@@ -435,6 +434,9 @@ class BaseNode(ABC):
435
434
  if traits:
436
435
  trait = traits[0] # Take the first Options trait
437
436
  trait.choices = choices
437
+ # Update the manually set UI options to include the new simple_dropdown
438
+ if hasattr(parameter, "_ui_options") and parameter._ui_options:
439
+ parameter._ui_options["simple_dropdown"] = choices
438
440
 
439
441
  if default in choices:
440
442
  parameter.default_value = default
@@ -443,9 +445,6 @@ class BaseNode(ABC):
443
445
  msg = f"Default model '{default}' is not in the provided choices."
444
446
  raise ValueError(msg)
445
447
 
446
- # Update the manually set UI options to include the new simple_dropdown
447
- if hasattr(parameter, "_ui_options") and parameter._ui_options:
448
- parameter._ui_options["simple_dropdown"] = choices
449
448
  else:
450
449
  msg = f"No Options trait found for parameter '{param}'."
451
450
  raise ValueError(msg)
@@ -565,7 +564,13 @@ class BaseNode(ABC):
565
564
  return None
566
565
 
567
566
  def set_parameter_value(
568
- self, param_name: str, value: Any, *, initial_setup: bool = False, emit_change: bool = True
567
+ self,
568
+ param_name: str,
569
+ value: Any,
570
+ *,
571
+ initial_setup: bool = False,
572
+ emit_change: bool = True,
573
+ skip_before_value_set: bool = False,
569
574
  ) -> None:
570
575
  """Attempt to set a Parameter's value.
571
576
 
@@ -586,6 +591,7 @@ class BaseNode(ABC):
586
591
  value: the value intended to be set
587
592
  emit_change: whether to emit a parameter lifecycle event, defaults to True
588
593
  initial_setup: Whether this value is being set as the initial setup on the node, defaults to False. When True, the value is not given to any before/after hooks.
594
+ skip_before_value_set: Whether to skip the before_value_set hook, defaults to False. Used when before_value_set has already been called earlier in the flow.
589
595
 
590
596
  Returns:
591
597
  A set of parameter names within this node that were modified as a result
@@ -614,7 +620,10 @@ class BaseNode(ABC):
614
620
  # Allow custom node logic to prepare and possibly mutate the value before it is actually set.
615
621
  # Record any parameters modified for cascading.
616
622
  if not initial_setup:
617
- final_value = self.before_value_set(parameter=parameter, value=candidate_value)
623
+ if not skip_before_value_set:
624
+ final_value = self.before_value_set(parameter=parameter, value=candidate_value)
625
+ else:
626
+ final_value = candidate_value
618
627
  # ACTUALLY SET THE NEW VALUE
619
628
  self.parameter_values[param_name] = final_value
620
629
 
@@ -710,6 +719,41 @@ class BaseNode(ABC):
710
719
  def process[T](self) -> AsyncResult | None:
711
720
  pass
712
721
 
722
+ async def aprocess(self) -> None:
723
+ """Async version of process().
724
+
725
+ Default implementation wraps the existing process() method to maintain backwards compatibility.
726
+ Subclasses can override this method to provide direct async implementation.
727
+ """
728
+ result = self.process()
729
+
730
+ if result is None:
731
+ # Simple synchronous node - nothing to do
732
+ return
733
+
734
+ if isinstance(result, Generator):
735
+ # Handle generator pattern asynchronously using the same logic as before
736
+ loop = asyncio.get_running_loop()
737
+
738
+ try:
739
+ # Start the generator
740
+ func = next(result)
741
+
742
+ while True:
743
+ # Run callable in thread pool (preserving existing behavior)
744
+ with ThreadPoolExecutor() as executor:
745
+ future_result = await loop.run_in_executor(executor, func)
746
+
747
+ # Send result back and get next callable
748
+ func = result.send(future_result)
749
+
750
+ except StopIteration:
751
+ # Generator is done
752
+ return
753
+ else:
754
+ # Some other return type - log warning but continue
755
+ logger.warning("Node %s process() returned unexpected type: %s", self.name, type(result))
756
+
713
757
  # if not implemented, it will return no issues.
714
758
  def validate_before_workflow_run(self) -> list[Exception] | None:
715
759
  """Runs before the entire workflow is run."""
@@ -768,6 +812,8 @@ class BaseNode(ABC):
768
812
  self.current_spotlight_parameter = None
769
813
 
770
814
  def append_value_to_parameter(self, parameter_name: str, value: Any) -> None:
815
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
816
+
771
817
  # Add the value to the node
772
818
  if parameter_name in self.parameter_output_values:
773
819
  try:
@@ -781,9 +827,14 @@ class BaseNode(ABC):
781
827
  else:
782
828
  self.parameter_output_values[parameter_name] = value
783
829
  # Publish the event up!
784
- EventBus.publish_event(ProgressEvent(value=value, node_name=self.name, parameter_name=parameter_name))
830
+
831
+ GriptapeNodes.EventManager().put_event(
832
+ ProgressEvent(value=value, node_name=self.name, parameter_name=parameter_name)
833
+ )
785
834
 
786
835
  def publish_update_to_parameter(self, parameter_name: str, value: Any) -> None:
836
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
837
+
787
838
  parameter = self.get_parameter_by_name(parameter_name)
788
839
  if parameter:
789
840
  data_type = parameter.type
@@ -794,7 +845,10 @@ class BaseNode(ABC):
794
845
  data_type=data_type,
795
846
  value=TypeValidator.safe_serialize(value),
796
847
  )
797
- EventBus.publish_event(ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=payload)))
848
+
849
+ GriptapeNodes.EventManager().put_event(
850
+ ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=payload))
851
+ )
798
852
  else:
799
853
  msg = f"Parameter '{parameter_name} doesn't exist on {self.name}'"
800
854
  raise RuntimeError(msg)
@@ -907,6 +961,7 @@ class BaseNode(ABC):
907
961
  """Emit an AlterElementEvent for parameter add/remove operations."""
908
962
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
909
963
  from griptape_nodes.retained_mode.events.parameter_events import AlterElementEvent
964
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
910
965
 
911
966
  # Create event data using the parameter's to_event method
912
967
  if remove:
@@ -920,7 +975,8 @@ class BaseNode(ABC):
920
975
  event = ExecutionGriptapeNodeEvent(
921
976
  wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
922
977
  )
923
- EventBus.publish_event(event)
978
+
979
+ GriptapeNodes.EventManager().put_event(event)
924
980
 
925
981
  def _get_element_name(self, element: str | int, element_names: list[str]) -> str:
926
982
  """Convert an element identifier (name or index) to its name.
@@ -1061,6 +1117,7 @@ class TrackedParameterOutputValues(dict[str, Any]):
1061
1117
  if parameter is not None:
1062
1118
  from griptape_nodes.retained_mode.events.base_events import ExecutionEvent, ExecutionGriptapeNodeEvent
1063
1119
  from griptape_nodes.retained_mode.events.parameter_events import AlterElementEvent
1120
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
1064
1121
 
1065
1122
  # Create event data using the parameter's to_event method
1066
1123
  event_data = parameter.to_event(self._node)
@@ -1073,7 +1130,8 @@ class TrackedParameterOutputValues(dict[str, Any]):
1073
1130
  event = ExecutionGriptapeNodeEvent(
1074
1131
  wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
1075
1132
  )
1076
- EventBus.publish_event(event)
1133
+
1134
+ GriptapeNodes.EventManager().put_event(event)
1077
1135
 
1078
1136
 
1079
1137
  class ControlNode(BaseNode):