griptape-nodes 0.54.1__tar.gz → 0.55.1__tar.gz

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 (152) hide show
  1. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/PKG-INFO +1 -1
  2. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/pyproject.toml +1 -1
  3. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/core_types.py +104 -18
  4. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/node_types.py +5 -2
  5. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/parallel_resolution.py +20 -16
  6. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/node_library/library_registry.py +1 -4
  7. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/execution_events.py +2 -0
  8. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/flow_manager.py +31 -28
  9. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_manager.py +51 -22
  10. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/node_manager.py +24 -4
  11. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/workflow_manager.py +36 -9
  12. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/README.md +0 -0
  13. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/__init__.py +0 -0
  14. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/__main__.py +0 -0
  15. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/app/.python-version +0 -0
  16. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/app/__init__.py +0 -0
  17. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/app/app.py +0 -0
  18. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/app/watch.py +0 -0
  19. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/bootstrap/__init__.py +0 -0
  20. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/bootstrap/workflow_executors/__init__.py +0 -0
  21. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +0 -0
  22. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +0 -0
  23. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/__init__.py +0 -0
  24. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/__init__.py +0 -0
  25. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/config.py +0 -0
  26. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/engine.py +0 -0
  27. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/init.py +0 -0
  28. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/libraries.py +0 -0
  29. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/models.py +0 -0
  30. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/commands/self.py +0 -0
  31. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/main.py +0 -0
  32. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/cli/shared.py +0 -0
  33. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/common/__init__.py +0 -0
  34. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/common/directed_graph.py +0 -0
  35. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/__init__.py +0 -0
  36. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/storage/__init__.py +0 -0
  37. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/storage/base_storage_driver.py +0 -0
  38. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +0 -0
  39. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/storage/local_storage_driver.py +0 -0
  40. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/drivers/storage/storage_backend.py +0 -0
  41. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/__init__.py +0 -0
  42. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/connections.py +0 -0
  43. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/flow.py +0 -0
  44. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/param_components/__init__.py +0 -0
  45. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/param_components/execution_status_component.py +0 -0
  46. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/exe_types/type_validator.py +0 -0
  47. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/__init__.py +0 -0
  48. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/control_flow.py +0 -0
  49. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/dag_builder.py +0 -0
  50. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/fsm.py +0 -0
  51. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/machines/sequential_resolution.py +0 -0
  52. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/mcp_server/__init__.py +0 -0
  53. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/node_library/__init__.py +0 -0
  54. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/node_library/advanced_node_library.py +0 -0
  55. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/node_library/workflow_registry.py +0 -0
  56. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/py.typed +0 -0
  57. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/__init__.py +0 -0
  58. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/__init__.py +0 -0
  59. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/agent_events.py +0 -0
  60. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/app_events.py +0 -0
  61. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/arbitrary_python_events.py +0 -0
  62. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/base_events.py +0 -0
  63. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/config_events.py +0 -0
  64. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/connection_events.py +0 -0
  65. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/context_events.py +0 -0
  66. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/flow_events.py +0 -0
  67. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/generate_request_payload_schemas.py +0 -0
  68. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/library_events.py +0 -0
  69. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/logger_events.py +0 -0
  70. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/model_events.py +0 -0
  71. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/node_events.py +0 -0
  72. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/object_events.py +0 -0
  73. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/os_events.py +0 -0
  74. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/parameter_events.py +0 -0
  75. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/payload_registry.py +0 -0
  76. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/secrets_events.py +0 -0
  77. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/static_file_events.py +0 -0
  78. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/sync_events.py +0 -0
  79. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/validation_events.py +0 -0
  80. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/variable_events.py +0 -0
  81. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/events/workflow_events.py +0 -0
  82. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/griptape_nodes.py +0 -0
  83. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/__init__.py +0 -0
  84. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/agent_manager.py +0 -0
  85. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +0 -0
  86. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/config_manager.py +0 -0
  87. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/context_manager.py +0 -0
  88. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/engine_identity_manager.py +0 -0
  89. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/event_manager.py +0 -0
  90. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/__init__.py +0 -0
  91. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/data_models.py +0 -0
  92. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +0 -0
  93. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +0 -0
  94. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/__init__.py +0 -0
  95. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +0 -0
  96. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +0 -0
  97. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +0 -0
  98. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +0 -0
  99. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +0 -0
  100. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance.py +0 -0
  101. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/library_lifecycle/library_status.py +0 -0
  102. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/model_manager.py +0 -0
  103. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/object_manager.py +0 -0
  104. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/operation_manager.py +0 -0
  105. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/os_manager.py +0 -0
  106. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/secrets_manager.py +0 -0
  107. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/session_manager.py +0 -0
  108. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/settings.py +0 -0
  109. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/static_files_manager.py +0 -0
  110. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/sync_manager.py +0 -0
  111. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/variable_manager.py +0 -0
  112. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/managers/version_compatibility_manager.py +0 -0
  113. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/retained_mode.py +0 -0
  114. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/utils/__init__.py +0 -0
  115. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/utils/engine_identity.py +0 -0
  116. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/utils/name_generator.py +0 -0
  117. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/retained_mode/variable_types.py +0 -0
  118. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/servers/__init__.py +0 -0
  119. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/servers/mcp.py +0 -0
  120. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/servers/static.py +0 -0
  121. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/servers/ws_request_manager.py +0 -0
  122. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/__init__.py +0 -0
  123. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/add_param_button.py +0 -0
  124. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/button.py +0 -0
  125. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/clamp.py +0 -0
  126. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/color_picker.py +0 -0
  127. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/compare.py +0 -0
  128. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/compare_images.py +0 -0
  129. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/file_system_picker.py +0 -0
  130. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/minmax.py +0 -0
  131. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/multi_options.py +0 -0
  132. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/numbers_selector.py +0 -0
  133. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/options.py +0 -0
  134. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/slider.py +0 -0
  135. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/trait_registry.py +0 -0
  136. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/traits/traits.json +0 -0
  137. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/updater/__init__.py +0 -0
  138. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/updater/__main__.py +0 -0
  139. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/__init__.py +0 -0
  140. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/async_utils.py +0 -0
  141. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/dict_utils.py +0 -0
  142. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/image_preview.py +0 -0
  143. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/metaclasses.py +0 -0
  144. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/uv_utils.py +0 -0
  145. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/utils/version_utils.py +0 -0
  146. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/__init__.py +0 -0
  147. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/versions/__init__.py +0 -0
  148. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/versions/v0_39_0/__init__.py +0 -0
  149. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/versions/v0_39_0/modified_parameters_set_removal.py +0 -0
  150. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/workflow_versions/__init__.py +0 -0
  151. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/workflow_versions/v0_7_0/__init__.py +0 -0
  152. {griptape_nodes-0.54.1 → griptape_nodes-0.55.1}/src/griptape_nodes/version_compatibility/workflow_versions/v0_7_0/local_executor_argument_addition.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: griptape-nodes
3
- Version: 0.54.1
3
+ Version: 0.55.1
4
4
  Summary: Add your description here
5
5
  Requires-Dist: griptape>=1.8.2
6
6
  Requires-Dist: pydantic>=2.10.6
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "griptape-nodes"
3
- version = "0.54.1"
3
+ version = "0.55.1"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12.0, <3.13"
@@ -501,18 +501,42 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
501
501
  "error": "Error",
502
502
  "success": "Success",
503
503
  "tip": "Tip",
504
+ "link": "Link",
505
+ "docs": "Documentation",
506
+ "help": "Help",
507
+ "note": "Note",
508
+ "none": "",
509
+ }
510
+
511
+ # Define default icons as a class-level constant (based on Lucide icons)
512
+ DEFAULT_ICONS: ClassVar[dict[str, str]] = {
513
+ "info": "info",
514
+ "warning": "alert-triangle",
515
+ "error": "x-circle",
516
+ "success": "check-circle",
517
+ "tip": "lightbulb",
518
+ "link": "external-link",
519
+ "docs": "book-open",
520
+ "help": "help-circle",
521
+ "note": "sticky-note",
504
522
  "none": "",
505
523
  }
506
524
 
507
525
  # Create a type alias using the keys from DEFAULT_TITLES
508
- type VariantType = Literal["info", "warning", "error", "success", "tip", "none"]
526
+ type VariantType = Literal["info", "warning", "error", "success", "tip", "link", "docs", "help", "note", "none"]
527
+ type ButtonAlignType = Literal["full-width", "left", "center", "right"]
528
+ type ButtonVariantType = Literal["default", "destructive", "outline", "secondary", "ghost", "link"]
509
529
 
510
530
  element_type: str = field(default_factory=lambda: ParameterMessage.__name__)
511
531
  _variant: VariantType = field(init=False)
512
532
  _title: str | None = field(default=None, init=False)
513
533
  _value: str = field(init=False)
534
+ _message_icon: str | None = field(default="__DEFAULT__", init=False)
514
535
  _button_link: str | None = field(default=None, init=False)
515
536
  _button_text: str | None = field(default=None, init=False)
537
+ _button_icon: str | None = field(default=None, init=False)
538
+ _button_variant: ButtonVariantType = field(default="outline", init=False)
539
+ _button_align: ButtonAlignType = field(default="full-width", init=False)
516
540
  _full_width: bool = field(default=False, init=False)
517
541
  _ui_options: dict = field(default_factory=dict, init=False)
518
542
 
@@ -522,8 +546,12 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
522
546
  value: str,
523
547
  *,
524
548
  title: str | None = None,
549
+ message_icon: str | None = "__DEFAULT__",
525
550
  button_link: str | None = None,
526
551
  button_text: str | None = None,
552
+ button_icon: str | None = None,
553
+ button_variant: ButtonVariantType = "outline",
554
+ button_align: ButtonAlignType = "full-width",
527
555
  full_width: bool = False,
528
556
  ui_options: dict | None = None,
529
557
  **kwargs,
@@ -532,8 +560,12 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
532
560
  self._variant = variant
533
561
  self._title = title
534
562
  self._value = value
563
+ self._message_icon = message_icon
535
564
  self._button_link = button_link
536
565
  self._button_text = button_text
566
+ self._button_icon = button_icon
567
+ self._button_variant = button_variant
568
+ self._button_align = button_align
537
569
  self._full_width = full_width
538
570
  self._ui_options = ui_options or {}
539
571
 
@@ -591,6 +623,42 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
591
623
  def full_width(self, value: bool) -> None:
592
624
  self._full_width = value
593
625
 
626
+ @property
627
+ def message_icon(self) -> str | None:
628
+ return self._message_icon
629
+
630
+ @message_icon.setter
631
+ @BaseNodeElement.emits_update_on_write
632
+ def message_icon(self, value: str | None) -> None:
633
+ self._message_icon = value
634
+
635
+ @property
636
+ def button_icon(self) -> str | None:
637
+ return self._button_icon
638
+
639
+ @button_icon.setter
640
+ @BaseNodeElement.emits_update_on_write
641
+ def button_icon(self, value: str | None) -> None:
642
+ self._button_icon = value
643
+
644
+ @property
645
+ def button_variant(self) -> ButtonVariantType:
646
+ return self._button_variant
647
+
648
+ @button_variant.setter
649
+ @BaseNodeElement.emits_update_on_write
650
+ def button_variant(self, value: ButtonVariantType) -> None:
651
+ self._button_variant = value
652
+
653
+ @property
654
+ def button_align(self) -> ButtonAlignType:
655
+ return self._button_align
656
+
657
+ @button_align.setter
658
+ @BaseNodeElement.emits_update_on_write
659
+ def button_align(self, value: ButtonAlignType) -> None:
660
+ self._button_align = value
661
+
594
662
  @property
595
663
  def ui_options(self) -> dict:
596
664
  return self._ui_options
@@ -603,23 +671,46 @@ class ParameterMessage(BaseNodeElement, UIOptionsMixin):
603
671
  def to_dict(self) -> dict[str, Any]:
604
672
  data = super().to_dict()
605
673
 
606
- # Use class-level default titles
674
+ # Use class-level default titles and icons
607
675
  title = self.title or self.DEFAULT_TITLES.get(str(self.variant), "")
608
676
 
677
+ # Handle message_icon logic:
678
+ # - "__DEFAULT__" means use the default icon for the variant
679
+ # - None means explicitly no icon (empty string)
680
+ # - Any other string means use that icon
681
+ if self.message_icon == "__DEFAULT__":
682
+ message_icon = self.DEFAULT_ICONS.get(str(self.variant), "")
683
+ elif self.message_icon is None:
684
+ message_icon = ""
685
+ else:
686
+ message_icon = self.message_icon
687
+
688
+ # Handle button_icon logic:
689
+ # - None means no icon
690
+ # - Empty string means no icon
691
+ # - Any other string means use that icon
692
+ if self.button_icon is None or self.button_icon == "":
693
+ button_icon = ""
694
+ else:
695
+ button_icon = self.button_icon
696
+
609
697
  # Merge the UI options with the message-specific options
698
+ # Always include these fields, even if they're None or empty
699
+ message_ui_options = {
700
+ "title": title,
701
+ "variant": self.variant,
702
+ "message_icon": message_icon,
703
+ "button_link": self.button_link,
704
+ "button_text": self.button_text,
705
+ "button_icon": button_icon,
706
+ "button_variant": self.button_variant,
707
+ "button_align": self.button_align,
708
+ "full_width": self.full_width,
709
+ }
710
+
610
711
  merged_ui_options = {
611
712
  **self.ui_options,
612
- **{
613
- k: v
614
- for k, v in {
615
- "title": title,
616
- "variant": self.variant,
617
- "button_link": self.button_link,
618
- "button_text": self.button_text,
619
- "full_width": self.full_width,
620
- }.items()
621
- if v is not None
622
- },
713
+ **message_ui_options,
623
714
  }
624
715
 
625
716
  data["name"] = self.name
@@ -986,11 +1077,6 @@ class Parameter(BaseNodeElement, UIOptionsMixin):
986
1077
  for trait in traits:
987
1078
  ui_options = ui_options | trait.ui_options_for_trait()
988
1079
  ui_options = ui_options | self._ui_options
989
- if self._parent is not None and isinstance(self._parent, ParameterGroup):
990
- # Access the field value directly for ParameterGroup
991
- parent_ui_options = getattr(self._parent, "ui_options", {})
992
- if isinstance(parent_ui_options, dict):
993
- ui_options = ui_options | parent_ui_options
994
1080
  return ui_options
995
1081
 
996
1082
  @ui_options.setter
@@ -335,7 +335,7 @@ class BaseNode(ABC):
335
335
  msg = f"Failed to add Parameter `{param.name}`. Parameter names cannot currently any whitespace characters. Please see https://github.com/griptape-ai/griptape-nodes/issues/714 to check the status on a remedy for this issue."
336
336
  raise ValueError(msg)
337
337
  if self.does_name_exist(param.name):
338
- msg = "Cannot have duplicate names on parameters."
338
+ msg = f"Cannot have duplicate names on parameters. Encountered two instances of '{param.name}'."
339
339
  raise ValueError(msg)
340
340
  self.add_node_element(param)
341
341
  self._emit_parameter_lifecycle_event(param)
@@ -1254,8 +1254,10 @@ class SuccessFailureNode(BaseNode):
1254
1254
 
1255
1255
  def _create_status_parameters(
1256
1256
  self,
1257
+ *,
1257
1258
  result_details_tooltip: str = "Details about the operation result",
1258
1259
  result_details_placeholder: str = "Details on the operation will be presented here.",
1260
+ parameter_group_initially_collapsed: bool = True,
1259
1261
  ) -> None:
1260
1262
  """Create and add standard status output parameters in a collapsible group.
1261
1263
 
@@ -1270,13 +1272,14 @@ class SuccessFailureNode(BaseNode):
1270
1272
  Args:
1271
1273
  result_details_tooltip: Custom tooltip for result_details parameter
1272
1274
  result_details_placeholder: Custom placeholder text for result_details parameter
1275
+ parameter_group_initially_collapsed: Whether the Status group should start collapsed
1273
1276
  """
1274
1277
  # Create status component with OUTPUT modes for SuccessFailureNode
1275
1278
  self.status_component = ExecutionStatusComponent(
1276
1279
  self,
1277
1280
  was_successful_modes={ParameterMode.OUTPUT},
1278
1281
  result_details_modes={ParameterMode.OUTPUT},
1279
- parameter_group_initially_collapsed=True,
1282
+ parameter_group_initially_collapsed=parameter_group_initially_collapsed,
1280
1283
  result_details_tooltip=result_details_tooltip,
1281
1284
  result_details_placeholder=result_details_placeholder,
1282
1285
  )
@@ -16,11 +16,13 @@ from griptape_nodes.retained_mode.events.base_events import (
16
16
  ExecutionGriptapeNodeEvent,
17
17
  )
18
18
  from griptape_nodes.retained_mode.events.execution_events import (
19
+ CurrentControlNodeEvent,
19
20
  CurrentDataNodeEvent,
20
21
  NodeResolvedEvent,
21
22
  ParameterValueUpdateEvent,
22
23
  )
23
24
  from griptape_nodes.retained_mode.events.parameter_events import SetParameterValueRequest
25
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
24
26
 
25
27
  if TYPE_CHECKING:
26
28
  from griptape_nodes.common.directed_graph import DirectedGraph
@@ -103,7 +105,7 @@ class ExecuteDagState(State):
103
105
  current_node = done_node.node_reference
104
106
 
105
107
  # Check if node was already resolved (shouldn't happen)
106
- if current_node.state == NodeResolutionState.RESOLVED:
108
+ if current_node.state == NodeResolutionState.RESOLVED and not current_node.lock:
107
109
  logger.error(
108
110
  "DUPLICATE COMPLETION DETECTED: Node '%s' was already RESOLVED but handle_done_nodes was called again from network '%s'. This should not happen!",
109
111
  current_node.name,
@@ -129,7 +131,6 @@ class ExecuteDagState(State):
129
131
  data_type = parameter.type
130
132
  if data_type is None:
131
133
  data_type = ParameterTypeBuiltin.NONE.value
132
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
133
134
 
134
135
  await GriptapeNodes.EventManager().aput_event(
135
136
  ExecutionGriptapeNodeEvent(
@@ -149,7 +150,6 @@ class ExecuteDagState(State):
149
150
  library_name = library[0]
150
151
  else:
151
152
  library_name = None
152
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
153
153
 
154
154
  await GriptapeNodes.EventManager().aput_event(
155
155
  ExecutionGriptapeNodeEvent(
@@ -169,8 +169,6 @@ class ExecuteDagState(State):
169
169
  @staticmethod
170
170
  def get_next_control_graph(context: ParallelResolutionContext, node: BaseNode, network_name: str) -> None:
171
171
  """Get next control flow nodes and add them to the DAG graph."""
172
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
173
-
174
172
  flow_manager = GriptapeNodes.FlowManager()
175
173
 
176
174
  # Early returns for various conditions
@@ -224,7 +222,11 @@ class ExecuteDagState(State):
224
222
  }
225
223
  )
226
224
  )
227
-
225
+ GriptapeNodes.EventManager().put_event(
226
+ ExecutionGriptapeNodeEvent(
227
+ wrapped_event=ExecutionEvent(payload=CurrentControlNodeEvent(node_name=next_node.name))
228
+ )
229
+ )
228
230
  ExecuteDagState._add_and_queue_nodes(context, next_node, network_name)
229
231
 
230
232
  @staticmethod
@@ -270,8 +272,6 @@ class ExecuteDagState(State):
270
272
  Args:
271
273
  node_reference (DagOrchestrator.DagNode): The node to collect values for.
272
274
  """
273
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
274
-
275
275
  current_node = node_reference.node_reference
276
276
  connections = GriptapeNodes.FlowManager().get_connections()
277
277
 
@@ -447,16 +447,22 @@ class ExecuteDagState(State):
447
447
  node_reference.node_reference.state = NodeResolutionState.RESOLVING
448
448
 
449
449
  # Send an event that this is a current data node:
450
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
451
450
 
452
451
  await GriptapeNodes.EventManager().aput_event(
453
452
  ExecutionGriptapeNodeEvent(wrapped_event=ExecutionEvent(payload=CurrentDataNodeEvent(node_name=node)))
454
453
  )
455
- # Wait for a task to finish
456
- done, _ = await asyncio.wait(context.task_to_node.keys(), return_when=asyncio.FIRST_COMPLETED)
457
- # Prevent task being removed before return
458
- for task in done:
459
- context.task_to_node.pop(task)
454
+
455
+ # Wait for a task to finish - only if there are tasks running
456
+ if context.task_to_node:
457
+ done, _ = await asyncio.wait(context.task_to_node.keys(), return_when=asyncio.FIRST_COMPLETED)
458
+ # Check for task exceptions and handle them properly
459
+ for task in done:
460
+ if task.exception():
461
+ # Get the actual exception and re-raise it
462
+ exc = task.exception()
463
+ context.task_to_node.pop(task)
464
+ raise RuntimeError(exc)
465
+ context.task_to_node.pop(task)
460
466
  # Once a task has finished, loop back to the top.
461
467
  await ExecuteDagState.pop_done_states(context)
462
468
  # Remove all nodes that are done
@@ -538,8 +544,6 @@ class ParallelResolutionMachine(FSM[ParallelResolutionContext]):
538
544
 
539
545
  async def resolve_node(self, node: BaseNode | None = None) -> None: # noqa: ARG002
540
546
  """Execute the DAG structure using the existing DagBuilder."""
541
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
542
-
543
547
  if self.context.dag_builder is None:
544
548
  self.context.dag_builder = GriptapeNodes.FlowManager().global_dag_builder
545
549
  await self.start(ExecuteDagState)
@@ -69,6 +69,7 @@ class CategoryDefinition(BaseModel):
69
69
  description: str
70
70
  color: str
71
71
  icon: str
72
+ group: str | None = None
72
73
 
73
74
 
74
75
  class NodeDefinition(BaseModel):
@@ -179,10 +180,6 @@ class LibraryRegistry(metaclass=SingletonMeta):
179
180
  logger.error(details)
180
181
  return details
181
182
 
182
- details = f"When registering Node class '{node_class_name}', Nodes with the same class name were already registered from the following Libraries: '{library_collisions}'. This is a collision. If you want to use this Node, you will need to specify the Library name in addition to the Node class name so that it can be disambiguated."
183
- logger.warning(details)
184
- return details
185
-
186
183
  return None
187
184
 
188
185
  @classmethod
@@ -225,10 +225,12 @@ class GetFlowStateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
225
225
  Args:
226
226
  control_node: Name of the current control node (if any)
227
227
  resolving_node: Name of the node currently being resolved (if any)
228
+ involved_nodes: Names of nodes that are queued to be executed or have been executed in the current run.
228
229
  """
229
230
 
230
231
  control_nodes: list[str] | None
231
232
  resolving_node: list[str] | None
233
+ involved_nodes: list[str] | None
232
234
 
233
235
 
234
236
  @dataclass
@@ -1090,14 +1090,17 @@ class FlowManager:
1090
1090
  details = f"Could not get flow state. Error: {err}"
1091
1091
  return GetFlowStateResultFailure(result_details=details)
1092
1092
  try:
1093
- control_nodes, resolving_nodes = self.flow_state(flow)
1093
+ control_nodes, resolving_nodes, involved_nodes = self.flow_state(flow)
1094
1094
  except Exception as e:
1095
1095
  details = f"Failed to get flow state of flow with name {flow_name}. Exception occurred: {e} "
1096
1096
  logger.exception(details)
1097
1097
  return GetFlowStateResultFailure(result_details=details)
1098
1098
  details = f"Successfully got flow state for flow with name {flow_name}."
1099
1099
  return GetFlowStateResultSuccess(
1100
- control_nodes=control_nodes, resolving_node=resolving_nodes, result_details=details
1100
+ control_nodes=control_nodes,
1101
+ resolving_node=resolving_nodes,
1102
+ result_details=details,
1103
+ involved_nodes=involved_nodes,
1101
1104
  )
1102
1105
 
1103
1106
  def on_cancel_flow_request(self, request: CancelFlowRequest) -> ResultPayload:
@@ -1713,28 +1716,27 @@ class FlowManager:
1713
1716
  await machine.start_flow(start_node, debug_mode)
1714
1717
 
1715
1718
  async def resolve_singular_node(self, flow: ControlFlow, node: BaseNode, *, debug_mode: bool = False) -> None:
1716
- # Set that we are only working on one node right now! no other stepping allowed
1719
+ # We are now going to have different behavior depending on how the node is behaving.
1717
1720
  if self.check_for_existing_running_flow():
1718
- # If flow already exists, throw an error
1719
- errormsg = f"This workflow is already in progress. Please wait for the current process to finish before starting {node.name} again."
1720
- raise RuntimeError(errormsg)
1721
-
1722
- self._global_single_node_resolution = True
1723
-
1724
- # Get or create machine
1725
- self._global_control_flow_machine = ControlFlowMachine(flow.name)
1726
- self._global_control_flow_machine.context.current_nodes = [node]
1727
- resolution_machine = self._global_control_flow_machine.resolution_machine
1728
- resolution_machine.change_debug_mode(debug_mode=debug_mode)
1729
- node.state = NodeResolutionState.UNRESOLVED
1730
- # Build the DAG for the node
1731
- if isinstance(resolution_machine, ParallelResolutionMachine):
1732
- self._global_dag_builder.add_node_with_dependencies(node)
1733
- resolution_machine.context.dag_builder = self._global_dag_builder
1734
- await resolution_machine.resolve_node(node)
1735
- if resolution_machine.is_complete():
1736
- self._global_single_node_resolution = False
1737
- self._global_control_flow_machine.context.current_nodes = []
1721
+ # Now we know something is running, it's ParallelResolutionMachine, and that we are in single_node_resolution.
1722
+ self._global_dag_builder.add_node_with_dependencies(node, node.name)
1723
+ else:
1724
+ # Set that we are only working on one node right now!
1725
+ self._global_single_node_resolution = True
1726
+ # Get or create machine
1727
+ self._global_control_flow_machine = ControlFlowMachine(flow.name)
1728
+ self._global_control_flow_machine.context.current_nodes = [node]
1729
+ resolution_machine = self._global_control_flow_machine.resolution_machine
1730
+ resolution_machine.change_debug_mode(debug_mode=debug_mode)
1731
+ node.state = NodeResolutionState.UNRESOLVED
1732
+ # Build the DAG for the node
1733
+ if isinstance(resolution_machine, ParallelResolutionMachine):
1734
+ self._global_dag_builder.add_node_with_dependencies(node)
1735
+ resolution_machine.context.dag_builder = self._global_dag_builder
1736
+ await resolution_machine.resolve_node(node)
1737
+ if resolution_machine.is_complete():
1738
+ self._global_single_node_resolution = False
1739
+ self._global_control_flow_machine.context.current_nodes = []
1738
1740
 
1739
1741
  async def single_execution_step(self, flow: ControlFlow, change_debug_mode: bool) -> None: # noqa: FBT001
1740
1742
  # do a granular step
@@ -1792,12 +1794,12 @@ class FlowManager:
1792
1794
  # Clear entry control parameter for new execution
1793
1795
  node.set_entry_control_parameter(None)
1794
1796
 
1795
- def flow_state(self, flow: ControlFlow) -> tuple[list[str] | None, list[str] | None]: # noqa: ARG002
1797
+ def flow_state(self, flow: ControlFlow) -> tuple[list[str] | None, list[str] | None, list[str] | None]: # noqa: ARG002
1796
1798
  if not self.check_for_existing_running_flow():
1797
1799
  msg = "Flow hasn't started."
1798
1800
  raise RuntimeError(msg)
1799
1801
  if self._global_control_flow_machine is None:
1800
- return None, None
1802
+ return None, None, None
1801
1803
  control_flow_context = self._global_control_flow_machine.context
1802
1804
  current_control_nodes = (
1803
1805
  [control_flow_node.name for control_flow_node in control_flow_context.current_nodes]
@@ -1810,12 +1812,13 @@ class FlowManager:
1810
1812
  node.node_reference.name
1811
1813
  for node in control_flow_context.resolution_machine.context.task_to_node.values()
1812
1814
  ]
1813
- return current_control_nodes, current_resolving_nodes
1815
+ involved_nodes = list(self._global_dag_builder.node_to_reference.keys())
1816
+ return current_control_nodes, current_resolving_nodes, involved_nodes if len(involved_nodes) != 0 else None
1814
1817
  if isinstance(control_flow_context.resolution_machine, SequentialResolutionMachine):
1815
1818
  focus_stack_for_node = control_flow_context.resolution_machine.context.focus_stack
1816
1819
  current_resolving_node = focus_stack_for_node[-1].node.name if len(focus_stack_for_node) else None
1817
- return current_control_nodes, [current_resolving_node] if current_resolving_node else None
1818
- return current_control_nodes, None
1820
+ return current_control_nodes, [current_resolving_node] if current_resolving_node else None, None
1821
+ return current_control_nodes, None, None
1819
1822
 
1820
1823
  def get_start_node_from_node(self, flow: ControlFlow, node: BaseNode) -> BaseNode | None:
1821
1824
  # backwards chain in control outputs.
@@ -116,6 +116,7 @@ console = Console()
116
116
 
117
117
  class LibraryManager:
118
118
  SANDBOX_LIBRARY_NAME = "Sandbox Library"
119
+ LIBRARY_CONFIG_FILENAME = "griptape_nodes_library.json"
119
120
 
120
121
  @dataclass
121
122
  class LibraryInfo:
@@ -480,24 +481,18 @@ class LibraryManager:
480
481
  successful_libraries = []
481
482
  failed_libraries = []
482
483
 
483
- # Load metadata from config libraries
484
- config_mgr = GriptapeNodes.ConfigManager()
485
- user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
486
- libraries_to_register: list[str] = config_mgr.get_config_value(user_libraries_section)
484
+ # Discover library files for metadata loading
485
+ library_files = self._discover_library_files()
487
486
 
488
- if libraries_to_register is not None:
489
- for library_to_register in libraries_to_register:
490
- if library_to_register and library_to_register.endswith(".json"):
491
- # Load metadata for this library file
492
- metadata_request = LoadLibraryMetadataFromFileRequest(file_path=library_to_register)
493
- metadata_result = self.load_library_metadata_from_file_request(metadata_request)
487
+ # Load metadata for all discovered library files
488
+ for library_file in library_files:
489
+ metadata_request = LoadLibraryMetadataFromFileRequest(file_path=str(library_file))
490
+ metadata_result = self.load_library_metadata_from_file_request(metadata_request)
494
491
 
495
- if isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
496
- successful_libraries.append(metadata_result)
497
- else:
498
- failed_libraries.append(cast("LoadLibraryMetadataFromFileResultFailure", metadata_result))
499
- # Note: We skip requirement specifier libraries (non-.json) as they don't have
500
- # JSON files we can load metadata from without installation
492
+ if isinstance(metadata_result, LoadLibraryMetadataFromFileResultSuccess):
493
+ successful_libraries.append(metadata_result)
494
+ else:
495
+ failed_libraries.append(cast("LoadLibraryMetadataFromFileResultFailure", metadata_result))
501
496
 
502
497
  # Generate sandbox library metadata
503
498
  sandbox_result = self._generate_sandbox_library_metadata()
@@ -1504,8 +1499,6 @@ class LibraryManager:
1504
1499
  return node_class
1505
1500
 
1506
1501
  async def load_all_libraries_from_config(self) -> None:
1507
- # Comment out lines 1503-1545 and call the _load libraries from provenance system to test the other functionality.
1508
-
1509
1502
  # Load metadata for all libraries to determine which ones can be safely loaded
1510
1503
  metadata_request = LoadMetadataForAllLibrariesRequest()
1511
1504
  metadata_result = self.load_metadata_for_all_libraries_request(metadata_request)
@@ -1525,9 +1518,6 @@ class LibraryManager:
1525
1518
  )
1526
1519
 
1527
1520
  # Use metadata results to selectively load libraries
1528
- user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
1529
-
1530
- # Load libraries that had successful metadata loading
1531
1521
  for library_result in metadata_result.successful_libraries:
1532
1522
  if library_result.library_schema.name == LibraryManager.SANDBOX_LIBRARY_NAME:
1533
1523
  # Handle sandbox library - use the schema we already have
@@ -1543,7 +1533,7 @@ class LibraryManager:
1543
1533
  if isinstance(register_result, RegisterLibraryFromFileResultFailure):
1544
1534
  # Registration failed - the failure info is already recorded in _library_file_path_to_info
1545
1535
  # by register_library_from_file_request, so we just log it here for visibility
1546
- logger.warning(f"Failed to register library from {library_result.file_path}") # noqa: G004
1536
+ logger.warning("Failed to register library from %s", library_result.file_path)
1547
1537
 
1548
1538
  # Print 'em all pretty
1549
1539
  self.print_library_load_status()
@@ -2041,3 +2031,42 @@ class LibraryManager:
2041
2031
  "Successfully reloaded all libraries. All object state was cleared and previous libraries were unloaded."
2042
2032
  )
2043
2033
  return ReloadAllLibrariesResultSuccess(result_details=ResultDetails(message=details, level=logging.INFO))
2034
+
2035
+ def _discover_library_files(self) -> list[Path]:
2036
+ """Discover library JSON files from config and workspace recursively.
2037
+
2038
+ Returns:
2039
+ List of library file paths found
2040
+ """
2041
+ config_mgr = GriptapeNodes.ConfigManager()
2042
+ user_libraries_section = "app_events.on_app_initialization_complete.libraries_to_register"
2043
+
2044
+ libraries_to_process = []
2045
+
2046
+ # Add from config
2047
+ config_libraries = config_mgr.get_config_value(user_libraries_section, default=[])
2048
+ libraries_to_process.extend(config_libraries)
2049
+
2050
+ # Add from workspace - recursive discovery of library JSON files
2051
+ workspace_path = config_mgr.workspace_path
2052
+ libraries_to_process.append(str(workspace_path))
2053
+
2054
+ library_files = []
2055
+
2056
+ def process_path(path: Path) -> None:
2057
+ """Process a path, handling both files and directories."""
2058
+ if path.is_dir():
2059
+ # Process all library JSON files recursively in the directory
2060
+ library_files.extend(path.rglob(LibraryManager.LIBRARY_CONFIG_FILENAME))
2061
+ elif path.suffix == ".json":
2062
+ library_files.append(path)
2063
+
2064
+ # Process library paths
2065
+ for library_to_process in libraries_to_process:
2066
+ library_path = Path(library_to_process)
2067
+
2068
+ # Handle library config files and directories only (skip requirement specifiers)
2069
+ if library_path.exists():
2070
+ process_path(library_path)
2071
+
2072
+ return library_files
@@ -22,6 +22,7 @@ from griptape_nodes.exe_types.node_types import (
22
22
  StartLoopNode,
23
23
  )
24
24
  from griptape_nodes.exe_types.type_validator import TypeValidator
25
+ from griptape_nodes.machines.sequential_resolution import SequentialResolutionMachine
25
26
  from griptape_nodes.node_library.library_registry import LibraryNameAndVersion, LibraryRegistry
26
27
  from griptape_nodes.retained_mode.events.base_events import (
27
28
  ResultDetails,
@@ -445,7 +446,7 @@ class NodeManager:
445
446
  # get the current node executing / resolving
446
447
  # if it's in connected nodes, cancel flow.
447
448
  # otherwise, leave it.
448
- control_node_names, resolving_node_names = GriptapeNodes.FlowManager().flow_state(parent_flow)
449
+ control_node_names, resolving_node_names, _ = GriptapeNodes.FlowManager().flow_state(parent_flow)
449
450
  connected_nodes = parent_flow.get_all_connected_nodes(node)
450
451
  cancelled = False
451
452
  if control_node_names is not None:
@@ -1856,9 +1857,28 @@ class NodeManager:
1856
1857
  if flow is None:
1857
1858
  details = f'Failed to fetch parent flow for "{node_name}"'
1858
1859
  return ResolveNodeResultFailure(validation_exceptions=[], result_details=details)
1859
- if GriptapeNodes.FlowManager().check_for_existing_running_flow():
1860
- details = f"Failed to resolve from node '{node_name}'. Flow is already running."
1861
- return ResolveNodeResultFailure(validation_exceptions=[], result_details=details)
1860
+
1861
+ # Check for existing running flow
1862
+ flow_mgr = GriptapeNodes.FlowManager()
1863
+ if flow_mgr.check_for_existing_running_flow():
1864
+ # Behavior should stay the same for sequential flows.
1865
+ if flow_mgr._global_control_flow_machine and isinstance(
1866
+ flow_mgr._global_control_flow_machine.resolution_machine, SequentialResolutionMachine
1867
+ ):
1868
+ errormsg = f"This workflow is already in progress. Please wait for the current process to finish before starting {node.name} again."
1869
+ return ResolveNodeResultFailure(validation_exceptions=[RuntimeError(errormsg)], result_details=errormsg)
1870
+ # Behavior should also match if the flow running is a Control Flow, and not a singular node resolution.
1871
+ if not flow_mgr._global_single_node_resolution:
1872
+ errormsg = f"This workflow is already in progress. Please wait for the current control process to finish before starting {node.name} again."
1873
+ return ResolveNodeResultFailure(validation_exceptions=[RuntimeError(errormsg)], result_details=errormsg)
1874
+
1875
+ # Check if the node is already in the DAG - if so, skip this resolution. It's already queued or has been resolved.
1876
+ if node.name in flow_mgr._global_dag_builder.node_to_reference:
1877
+ logger.error("Node %s is already executing. Cannot start execution.", node.name)
1878
+ return ResolveNodeResultFailure(
1879
+ validation_exceptions=[],
1880
+ result_details=f"Node {node.name} is already executing. Cannot start execution.",
1881
+ )
1862
1882
  try:
1863
1883
  GriptapeNodes.FlowManager().get_connections().unresolve_future_nodes(node)
1864
1884
  except Exception as e: