griptape-nodes 0.52.1__py3-none-any.whl → 0.54.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 (71) hide show
  1. griptape_nodes/__init__.py +8 -942
  2. griptape_nodes/__main__.py +6 -0
  3. griptape_nodes/app/app.py +48 -86
  4. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +35 -5
  5. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +15 -1
  6. griptape_nodes/cli/__init__.py +1 -0
  7. griptape_nodes/cli/commands/__init__.py +1 -0
  8. griptape_nodes/cli/commands/config.py +74 -0
  9. griptape_nodes/cli/commands/engine.py +80 -0
  10. griptape_nodes/cli/commands/init.py +550 -0
  11. griptape_nodes/cli/commands/libraries.py +96 -0
  12. griptape_nodes/cli/commands/models.py +504 -0
  13. griptape_nodes/cli/commands/self.py +120 -0
  14. griptape_nodes/cli/main.py +56 -0
  15. griptape_nodes/cli/shared.py +75 -0
  16. griptape_nodes/common/__init__.py +1 -0
  17. griptape_nodes/common/directed_graph.py +71 -0
  18. griptape_nodes/drivers/storage/base_storage_driver.py +40 -20
  19. griptape_nodes/drivers/storage/griptape_cloud_storage_driver.py +24 -29
  20. griptape_nodes/drivers/storage/local_storage_driver.py +23 -14
  21. griptape_nodes/exe_types/core_types.py +60 -2
  22. griptape_nodes/exe_types/node_types.py +257 -38
  23. griptape_nodes/exe_types/param_components/__init__.py +1 -0
  24. griptape_nodes/exe_types/param_components/execution_status_component.py +138 -0
  25. griptape_nodes/machines/control_flow.py +195 -94
  26. griptape_nodes/machines/dag_builder.py +207 -0
  27. griptape_nodes/machines/fsm.py +10 -1
  28. griptape_nodes/machines/parallel_resolution.py +558 -0
  29. griptape_nodes/machines/{node_resolution.py → sequential_resolution.py} +30 -57
  30. griptape_nodes/node_library/library_registry.py +34 -1
  31. griptape_nodes/retained_mode/events/app_events.py +5 -1
  32. griptape_nodes/retained_mode/events/base_events.py +9 -9
  33. griptape_nodes/retained_mode/events/config_events.py +30 -0
  34. griptape_nodes/retained_mode/events/execution_events.py +2 -2
  35. griptape_nodes/retained_mode/events/model_events.py +296 -0
  36. griptape_nodes/retained_mode/events/node_events.py +4 -3
  37. griptape_nodes/retained_mode/griptape_nodes.py +34 -12
  38. griptape_nodes/retained_mode/managers/agent_manager.py +23 -5
  39. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +3 -1
  40. griptape_nodes/retained_mode/managers/config_manager.py +44 -3
  41. griptape_nodes/retained_mode/managers/context_manager.py +6 -5
  42. griptape_nodes/retained_mode/managers/event_manager.py +8 -2
  43. griptape_nodes/retained_mode/managers/flow_manager.py +150 -206
  44. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +1 -1
  45. griptape_nodes/retained_mode/managers/library_manager.py +35 -25
  46. griptape_nodes/retained_mode/managers/model_manager.py +1107 -0
  47. griptape_nodes/retained_mode/managers/node_manager.py +102 -220
  48. griptape_nodes/retained_mode/managers/object_manager.py +11 -5
  49. griptape_nodes/retained_mode/managers/os_manager.py +28 -13
  50. griptape_nodes/retained_mode/managers/secrets_manager.py +8 -4
  51. griptape_nodes/retained_mode/managers/settings.py +116 -7
  52. griptape_nodes/retained_mode/managers/static_files_manager.py +85 -12
  53. griptape_nodes/retained_mode/managers/sync_manager.py +17 -9
  54. griptape_nodes/retained_mode/managers/workflow_manager.py +186 -192
  55. griptape_nodes/retained_mode/retained_mode.py +19 -0
  56. griptape_nodes/servers/__init__.py +1 -0
  57. griptape_nodes/{mcp_server/server.py → servers/mcp.py} +1 -1
  58. griptape_nodes/{app/api.py → servers/static.py} +43 -40
  59. griptape_nodes/traits/add_param_button.py +1 -1
  60. griptape_nodes/traits/button.py +334 -6
  61. griptape_nodes/traits/color_picker.py +66 -0
  62. griptape_nodes/traits/multi_options.py +188 -0
  63. griptape_nodes/traits/numbers_selector.py +77 -0
  64. griptape_nodes/traits/options.py +93 -2
  65. griptape_nodes/traits/traits.json +4 -0
  66. griptape_nodes/utils/async_utils.py +31 -0
  67. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/METADATA +4 -1
  68. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/RECORD +71 -48
  69. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/WHEEL +1 -1
  70. /griptape_nodes/{mcp_server → servers}/ws_request_manager.py +0 -0
  71. {griptape_nodes-0.52.1.dist-info → griptape_nodes-0.54.0.dist-info}/entry_points.txt +0 -0
@@ -29,6 +29,9 @@ from griptape_nodes.retained_mode.events.app_events import (
29
29
  GetEngineVersionRequest,
30
30
  GetEngineVersionResultSuccess,
31
31
  )
32
+
33
+ # Runtime imports for ResultDetails since it's used at runtime
34
+ from griptape_nodes.retained_mode.events.base_events import ResultDetail, ResultDetails
32
35
  from griptape_nodes.retained_mode.events.flow_events import (
33
36
  CreateFlowRequest,
34
37
  GetTopLevelFlowRequest,
@@ -314,17 +317,22 @@ class WorkflowManager:
314
317
 
315
318
  def on_libraries_initialization_complete(self) -> None:
316
319
  # All of the libraries have loaded, and any workflows they came with have been registered.
317
- # See if there are USER workflow JSONs to load.
320
+ # Discover workflows from both config and workspace.
318
321
  default_workflow_section = "app_events.on_app_initialization_complete.workflows_to_register"
322
+ config_mgr = GriptapeNodes.ConfigManager()
319
323
 
320
- # Use the request/response pattern for workflow registration
321
- from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
324
+ workflows_to_register = []
325
+
326
+ # Add from config
327
+ config_workflows = config_mgr.get_config_value(default_workflow_section, default=[])
328
+ workflows_to_register.extend(config_workflows)
322
329
 
323
- register_request = RegisterWorkflowsFromConfigRequest(config_section=default_workflow_section)
324
- register_result = GriptapeNodes.handle_request(register_request)
330
+ # Add from workspace (avoiding duplicates)
331
+ workspace_path = config_mgr.workspace_path
332
+ workflows_to_register.extend([workspace_path])
325
333
 
326
- if not isinstance(register_result, RegisterWorkflowsFromConfigResultSuccess):
327
- logger.warning("Failed to register workflows from configuration during library initialization")
334
+ # Register all discovered workflows at once if any were found
335
+ self._process_workflows_for_registration(workflows_to_register)
328
336
 
329
337
  # Print it all out nicely.
330
338
  self.print_workflow_load_status()
@@ -337,7 +345,6 @@ class WorkflowManager:
337
345
  paths_to_remove.add(workflow_path.lower())
338
346
 
339
347
  if paths_to_remove:
340
- config_mgr = GriptapeNodes.ConfigManager()
341
348
  workflows_to_register = config_mgr.get_config_value(default_workflow_section)
342
349
  if workflows_to_register:
343
350
  workflows_to_register = [
@@ -535,7 +542,6 @@ class WorkflowManager:
535
542
  complete_file_path = WorkflowRegistry.get_complete_file_path(relative_file_path=relative_file_path)
536
543
  if not Path(complete_file_path).is_file():
537
544
  details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
538
- logger.error(details)
539
545
  return RunWorkflowFromScratchResultFailure(result_details=details)
540
546
 
541
547
  # Start with a clean slate.
@@ -543,14 +549,12 @@ class WorkflowManager:
543
549
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
544
550
  if not clear_all_result.succeeded():
545
551
  details = f"Failed to clear the existing object state when trying to run '{complete_file_path}'."
546
- logger.error(details)
547
552
  return RunWorkflowFromScratchResultFailure(result_details=details)
548
553
 
549
554
  # Run the file, goddamn it
550
555
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
551
556
  if execution_result.execution_successful:
552
- logger.debug(execution_result.execution_details)
553
- return RunWorkflowFromScratchResultSuccess()
557
+ return RunWorkflowFromScratchResultSuccess(result_details=execution_result.execution_details)
554
558
 
555
559
  logger.error(execution_result.execution_details)
556
560
  return RunWorkflowFromScratchResultFailure(result_details=execution_result.execution_details)
@@ -560,13 +564,11 @@ class WorkflowManager:
560
564
  complete_file_path = WorkflowRegistry.get_complete_file_path(relative_file_path=relative_file_path)
561
565
  if not Path(complete_file_path).is_file():
562
566
  details = f"Failed to find file. Path '{complete_file_path}' doesn't exist."
563
- logger.error(details)
564
567
  return RunWorkflowWithCurrentStateResultFailure(result_details=details)
565
568
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
566
569
 
567
570
  if execution_result.execution_successful:
568
- logger.debug(execution_result.execution_details)
569
- return RunWorkflowWithCurrentStateResultSuccess()
571
+ return RunWorkflowWithCurrentStateResultSuccess(result_details=execution_result.execution_details)
570
572
  logger.error(execution_result.execution_details)
571
573
  return RunWorkflowWithCurrentStateResultFailure(result_details=execution_result.execution_details)
572
574
 
@@ -576,13 +578,12 @@ class WorkflowManager:
576
578
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
577
579
  except KeyError:
578
580
  details = f"Failed to get workflow '{request.workflow_name}' from registry."
579
- logger.error(details)
580
581
  return RunWorkflowFromRegistryResultFailure(result_details=details)
581
582
 
582
583
  # Update current context for workflow.
584
+ context_warning = None
583
585
  if GriptapeNodes.ContextManager().has_current_workflow():
584
- details = f"Started a new workflow '{request.workflow_name}' but a workflow '{GriptapeNodes.ContextManager().get_current_workflow_name()}' was already in the Current Context. Replacing the old with the new."
585
- logger.warning(details)
586
+ context_warning = f"Started a new workflow '{request.workflow_name}' but a workflow '{GriptapeNodes.ContextManager().get_current_workflow_name()}' was already in the Current Context. Replacing the old with the new."
586
587
 
587
588
  # get file_path from workflow
588
589
  relative_file_path = workflow.file_path
@@ -595,7 +596,6 @@ class WorkflowManager:
595
596
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
596
597
  if not clear_all_result.succeeded():
597
598
  details = f"Failed to clear the existing object state when preparing to run workflow '{request.workflow_name}'."
598
- logger.error(details)
599
599
  return RunWorkflowFromRegistryResultFailure(result_details=details)
600
600
 
601
601
  # Let's run under the assumption that this Workflow will become our Current Context; if we fail, it will revert.
@@ -604,18 +604,24 @@ class WorkflowManager:
604
604
  execution_result = self.run_workflow(relative_file_path=relative_file_path)
605
605
 
606
606
  if not execution_result.execution_successful:
607
- logger.error(execution_result.execution_details)
607
+ result_messages = []
608
+ if context_warning:
609
+ result_messages.append(ResultDetail(message=context_warning, level=logging.WARNING))
610
+ result_messages.append(ResultDetail(message=execution_result.execution_details, level=logging.ERROR))
608
611
 
609
612
  # Attempt to clear everything out, as we modified the engine state getting here.
610
613
  clear_all_request = ClearAllObjectStateRequest(i_know_what_im_doing=True)
611
614
  clear_all_result = GriptapeNodes.handle_request(clear_all_request)
612
615
 
613
616
  # The clear-all above here wipes the ContextManager, so no need to do a pop_workflow().
614
- return RunWorkflowFromRegistryResultFailure(result_details=execution_result.execution_details)
617
+ return RunWorkflowFromRegistryResultFailure(result_details=ResultDetails(*result_messages))
615
618
 
616
619
  # Success!
617
- logger.debug(execution_result.execution_details)
618
- return RunWorkflowFromRegistryResultSuccess()
620
+ result_messages = []
621
+ if context_warning:
622
+ result_messages.append(ResultDetail(message=context_warning, level=logging.WARNING))
623
+ result_messages.append(ResultDetail(message=execution_result.execution_details, level=logging.DEBUG))
624
+ return RunWorkflowFromRegistryResultSuccess(result_details=ResultDetails(*result_messages))
619
625
 
620
626
  def on_register_workflow_request(self, request: RegisterWorkflowRequest) -> ResultPayload:
621
627
  try:
@@ -625,9 +631,14 @@ class WorkflowManager:
625
631
  workflow = WorkflowRegistry.generate_new_workflow(metadata=request.metadata, file_path=request.file_name)
626
632
  except Exception as e:
627
633
  details = f"Failed to register workflow with name '{request.metadata.name}'. Error: {e}"
628
- logger.error(details)
629
634
  return RegisterWorkflowResultFailure(result_details=details)
630
- return RegisterWorkflowResultSuccess(workflow_name=workflow.metadata.name)
635
+ return RegisterWorkflowResultSuccess(
636
+ workflow_name=workflow.metadata.name,
637
+ result_details=ResultDetails(
638
+ message=f"Successfully registered workflow: {workflow.metadata.name}",
639
+ level=logging.DEBUG,
640
+ ),
641
+ )
631
642
 
632
643
  def on_import_workflow_request(self, request: ImportWorkflowRequest) -> ResultPayload:
633
644
  # First, attempt to load metadata from the file
@@ -641,7 +652,10 @@ class WorkflowManager:
641
652
  workflow_name = load_metadata_result.metadata.name
642
653
  if WorkflowRegistry.has_workflow_with_name(workflow_name):
643
654
  # Workflow already exists - no need to re-register
644
- return ImportWorkflowResultSuccess(workflow_name=workflow_name)
655
+ return ImportWorkflowResultSuccess(
656
+ workflow_name=workflow_name,
657
+ result_details=f"Workflow '{workflow_name}' already exists - no need to re-import.",
658
+ )
645
659
 
646
660
  # Now register the workflow with the extracted metadata
647
661
  register_request = RegisterWorkflowRequest(metadata=load_metadata_result.metadata, file_name=request.file_path)
@@ -659,30 +673,34 @@ class WorkflowManager:
659
673
 
660
674
  return ImportWorkflowResultFailure(result_details=details)
661
675
 
662
- return ImportWorkflowResultSuccess(workflow_name=register_result.workflow_name)
676
+ return ImportWorkflowResultSuccess(
677
+ workflow_name=register_result.workflow_name,
678
+ result_details=ResultDetails(
679
+ message=f"Successfully imported workflow: {register_result.workflow_name}", level=logging.INFO
680
+ ),
681
+ )
663
682
 
664
683
  def on_list_all_workflows_request(self, _request: ListAllWorkflowsRequest) -> ResultPayload:
665
684
  try:
666
685
  workflows = WorkflowRegistry.list_workflows()
667
686
  except Exception:
668
687
  details = "Failed to list all workflows."
669
- logger.error(details)
670
688
  return ListAllWorkflowsResultFailure(result_details=details)
671
- return ListAllWorkflowsResultSuccess(workflows=workflows)
689
+ return ListAllWorkflowsResultSuccess(
690
+ workflows=workflows, result_details=f"Successfully retrieved {len(workflows)} workflows."
691
+ )
672
692
 
673
693
  def on_delete_workflows_request(self, request: DeleteWorkflowRequest) -> ResultPayload:
674
694
  try:
675
695
  workflow = WorkflowRegistry.delete_workflow_by_name(request.name)
676
696
  except Exception as e:
677
697
  details = f"Failed to remove workflow from registry with name '{request.name}'. Exception: {e}"
678
- logger.error(details)
679
698
  return DeleteWorkflowResultFailure(result_details=details)
680
699
  config_manager = GriptapeNodes.ConfigManager()
681
700
  try:
682
701
  config_manager.delete_user_workflow(workflow.file_path)
683
702
  except Exception as e:
684
703
  details = f"Failed to remove workflow from user config with name '{request.name}'. Exception: {e}"
685
- logger.error(details)
686
704
  return DeleteWorkflowResultFailure(result_details=details)
687
705
  # delete the actual file
688
706
  full_path = config_manager.workspace_path.joinpath(workflow.file_path)
@@ -690,25 +708,28 @@ class WorkflowManager:
690
708
  full_path.unlink()
691
709
  except Exception as e:
692
710
  details = f"Failed to delete workflow file with path '{workflow.file_path}'. Exception: {e}"
693
- logger.error(details)
694
711
  return DeleteWorkflowResultFailure(result_details=details)
695
- return DeleteWorkflowResultSuccess()
712
+ return DeleteWorkflowResultSuccess(
713
+ result_details=ResultDetails(message=f"Successfully deleted workflow: {request.name}", level=logging.INFO)
714
+ )
696
715
 
697
716
  def on_rename_workflow_request(self, request: RenameWorkflowRequest) -> ResultPayload:
698
717
  save_workflow_request = GriptapeNodes.handle_request(SaveWorkflowRequest(file_name=request.requested_name))
699
718
 
700
719
  if isinstance(save_workflow_request, SaveWorkflowResultFailure):
701
720
  details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to save."
702
- logger.error(details)
703
721
  return RenameWorkflowResultFailure(result_details=details)
704
722
 
705
723
  delete_workflow_result = GriptapeNodes.handle_request(DeleteWorkflowRequest(name=request.workflow_name))
706
724
  if isinstance(delete_workflow_result, DeleteWorkflowResultFailure):
707
725
  details = f"Attempted to rename workflow '{request.workflow_name}' to '{request.requested_name}'. Failed while attempting to remove the original file name from the registry."
708
- logger.error(details)
709
726
  return RenameWorkflowResultFailure(result_details=details)
710
727
 
711
- return RenameWorkflowResultSuccess()
728
+ return RenameWorkflowResultSuccess(
729
+ result_details=ResultDetails(
730
+ message=f"Successfully renamed workflow to: {request.requested_name}", level=logging.INFO
731
+ )
732
+ )
712
733
 
713
734
  def on_move_workflow_request(self, request: MoveWorkflowRequest) -> ResultPayload: # noqa: PLR0911
714
735
  try:
@@ -716,7 +737,6 @@ class WorkflowManager:
716
737
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
717
738
  except KeyError:
718
739
  details = f"Failed to move workflow '{request.workflow_name}' because it does not exist."
719
- logger.error(details)
720
740
  return MoveWorkflowResultFailure(result_details=details)
721
741
 
722
742
  config_manager = GriptapeNodes.ConfigManager()
@@ -727,7 +747,6 @@ class WorkflowManager:
727
747
  details = (
728
748
  f"Failed to move workflow '{request.workflow_name}': File path '{current_file_path}' does not exist."
729
749
  )
730
- logger.error(details)
731
750
  return MoveWorkflowResultFailure(result_details=details)
732
751
 
733
752
  # Clean and validate target directory
@@ -742,7 +761,6 @@ class WorkflowManager:
742
761
  target_dir_path.mkdir(parents=True, exist_ok=True)
743
762
  except OSError as e:
744
763
  details = f"Failed to create target directory '{target_dir_path}': {e!s}"
745
- logger.error(details)
746
764
  return MoveWorkflowResultFailure(result_details=details)
747
765
 
748
766
  # Create new file path
@@ -755,7 +773,6 @@ class WorkflowManager:
755
773
  details = (
756
774
  f"Failed to move workflow '{request.workflow_name}': Target file '{new_absolute_path}' already exists."
757
775
  )
758
- logger.error(details)
759
776
  return MoveWorkflowResultFailure(result_details=details)
760
777
 
761
778
  try:
@@ -770,28 +787,29 @@ class WorkflowManager:
770
787
  config_manager.save_user_workflow_json(str(new_absolute_path))
771
788
 
772
789
  except OSError as e:
773
- details = f"Failed to move workflow file '{current_file_path}' to '{new_absolute_path}': {e!s}"
774
- logger.error(details)
790
+ error_messages = []
791
+ main_error = f"Failed to move workflow file '{current_file_path}' to '{new_absolute_path}': {e!s}"
792
+ error_messages.append(ResultDetail(message=main_error, level=logging.ERROR))
775
793
 
776
794
  # Attempt to rollback if file was moved but registry update failed
777
795
  if new_absolute_path.exists() and not Path(current_file_path).exists():
778
796
  try:
779
797
  new_absolute_path.rename(current_file_path)
780
- details = f"Rolled back file move for workflow '{request.workflow_name}'"
781
- logger.info(details)
798
+ rollback_message = f"Rolled back file move for workflow '{request.workflow_name}'"
799
+ error_messages.append(ResultDetail(message=rollback_message, level=logging.INFO))
782
800
  except OSError:
783
- details = f"Failed to rollback file move for workflow '{request.workflow_name}'"
784
- logger.error(details)
801
+ rollback_failure = f"Failed to rollback file move for workflow '{request.workflow_name}'"
802
+ error_messages.append(ResultDetail(message=rollback_failure, level=logging.ERROR))
785
803
 
786
- return MoveWorkflowResultFailure(result_details=details)
804
+ return MoveWorkflowResultFailure(result_details=ResultDetails(*error_messages))
787
805
  except Exception as e:
788
806
  details = f"Failed to move workflow '{request.workflow_name}': {e!s}"
789
- logger.error(details)
790
807
  return MoveWorkflowResultFailure(result_details=details)
791
808
  else:
792
809
  details = f"Successfully moved workflow '{request.workflow_name}' to '{new_relative_path}'"
793
- logger.info(details)
794
- return MoveWorkflowResultSuccess(moved_file_path=new_relative_path)
810
+ return MoveWorkflowResultSuccess(
811
+ moved_file_path=new_relative_path, result_details=ResultDetails(message=details, level=logging.INFO)
812
+ )
795
813
 
796
814
  def on_load_workflow_metadata_request( # noqa: C901, PLR0912, PLR0915
797
815
  self, request: LoadWorkflowMetadata
@@ -810,7 +828,6 @@ class WorkflowManager:
810
828
  ],
811
829
  )
812
830
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}. Failed because no file could be found at that path."
813
- logger.error(details)
814
831
  return LoadWorkflowMetadataResultFailure(result_details=details)
815
832
 
816
833
  # Find the metadata block.
@@ -827,7 +844,6 @@ class WorkflowManager:
827
844
  ],
828
845
  )
829
846
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed as it had {len(matches)} sections titled '{block_name}', and we expect exactly 1 such section."
830
- logger.error(details)
831
847
  return LoadWorkflowMetadataResultFailure(result_details=details)
832
848
 
833
849
  # Now attempt to parse out the metadata section, stripped of comment prefixes.
@@ -847,7 +863,6 @@ class WorkflowManager:
847
863
  problems=[f"Failed because the metadata was not valid TOML: {err}"],
848
864
  )
849
865
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the metadata was not valid TOML: {err}"
850
- logger.error(details)
851
866
  return LoadWorkflowMetadataResultFailure(result_details=details)
852
867
 
853
868
  tool_header = "tool"
@@ -863,7 +878,6 @@ class WorkflowManager:
863
878
  problems=[f"Failed because the '[{tool_header}.{griptape_nodes_header}]' section could not be found."],
864
879
  )
865
880
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the '[{tool_header}.{griptape_nodes_header}]' section could not be found: {err}"
866
- logger.error(details)
867
881
  return LoadWorkflowMetadataResultFailure(result_details=details)
868
882
 
869
883
  try:
@@ -881,7 +895,6 @@ class WorkflowManager:
881
895
  ],
882
896
  )
883
897
  details = f"Attempted to load workflow metadata for a file at '{complete_file_path}'. Failed because the metadata in the '[{tool_header}.{griptape_nodes_header}]' section did not match the requisite schema with error: {err}"
884
- logger.error(details)
885
898
  return LoadWorkflowMetadataResultFailure(result_details=details)
886
899
 
887
900
  # We have valid dependencies, etc.
@@ -1037,7 +1050,9 @@ class WorkflowManager:
1037
1050
  workflow_dependencies=dependency_infos,
1038
1051
  problems=problems,
1039
1052
  )
1040
- return LoadWorkflowMetadataResultSuccess(metadata=workflow_metadata)
1053
+ return LoadWorkflowMetadataResultSuccess(
1054
+ metadata=workflow_metadata, result_details="Workflow metadata loaded successfully."
1055
+ )
1041
1056
 
1042
1057
  def register_workflows_from_config(self, config_section: str) -> None:
1043
1058
  workflows_to_register = GriptapeNodes.ConfigManager().get_config_value(config_section)
@@ -1045,21 +1060,7 @@ class WorkflowManager:
1045
1060
  self.register_list_of_workflows(workflows_to_register)
1046
1061
 
1047
1062
  def register_list_of_workflows(self, workflows_to_register: list[str]) -> None:
1048
- for workflow_to_register in workflows_to_register:
1049
- path = Path(workflow_to_register)
1050
-
1051
- if path.is_dir():
1052
- # If it's a directory, register all the workflows in it.
1053
- for workflow_file in path.glob("*.py"):
1054
- # Check that the python file has script metadata
1055
- metadata_blocks = self.get_workflow_metadata(
1056
- workflow_file, block_name=WorkflowManager.WORKFLOW_METADATA_HEADER
1057
- )
1058
- if len(metadata_blocks) == 1:
1059
- self._register_workflow(str(workflow_file))
1060
- else:
1061
- # If it's a file, register it directly.
1062
- self._register_workflow(str(path))
1063
+ self._process_workflows_for_registration(workflows_to_register)
1063
1064
 
1064
1065
  def _register_workflow(self, workflow_to_register: str) -> bool:
1065
1066
  """Registers a workflow from a file.
@@ -1159,7 +1160,6 @@ class WorkflowManager:
1159
1160
  engine_version_result = GriptapeNodes.handle_request(request=engine_version_request)
1160
1161
  if not isinstance(engine_version_result, GetEngineVersionResultSuccess):
1161
1162
  details = f"Failed getting the engine version for workflow '{file_name}'."
1162
- logger.error(details)
1163
1163
  raise TypeError(details)
1164
1164
  try:
1165
1165
  engine_version_success = cast("GetEngineVersionResultSuccess", engine_version_result)
@@ -1168,7 +1168,6 @@ class WorkflowManager:
1168
1168
  )
1169
1169
  except Exception as err:
1170
1170
  details = f"Failed getting the engine version for workflow '{file_name}': {err}"
1171
- logger.error(details)
1172
1171
  raise ValueError(details) from err
1173
1172
 
1174
1173
  # Keep track of all of the nodes we create and the generated variable names for them.
@@ -1182,7 +1181,6 @@ class WorkflowManager:
1182
1181
  top_level_flow_result = GriptapeNodes.handle_request(top_level_flow_request)
1183
1182
  if not isinstance(top_level_flow_result, GetTopLevelFlowResultSuccess):
1184
1183
  details = f"Failed when requesting to get top level flow for workflow '{file_name}'."
1185
- logger.error(details)
1186
1184
  raise TypeError(details)
1187
1185
  top_level_flow_name = top_level_flow_result.flow_name
1188
1186
  serialized_flow_request = SerializeFlowToCommandsRequest(
@@ -1191,7 +1189,6 @@ class WorkflowManager:
1191
1189
  serialized_flow_result = GriptapeNodes.handle_request(serialized_flow_request)
1192
1190
  if not isinstance(serialized_flow_result, SerializeFlowToCommandsResultSuccess):
1193
1191
  details = f"Failed when serializing flow for workflow '{file_name}'."
1194
- logger.error(details)
1195
1192
  raise TypeError(details)
1196
1193
  serialized_flow_commands = serialized_flow_result.serialized_flow_commands
1197
1194
 
@@ -1214,7 +1211,6 @@ class WorkflowManager:
1214
1211
  )
1215
1212
  if workflow_metadata is None:
1216
1213
  details = f"Failed to generate metadata for workflow '{file_name}'."
1217
- logger.error(details)
1218
1214
  raise ValueError(details)
1219
1215
 
1220
1216
  # Set the image if provided
@@ -1224,7 +1220,6 @@ class WorkflowManager:
1224
1220
  metadata_block = self._generate_workflow_metadata_header(workflow_metadata=workflow_metadata)
1225
1221
  if metadata_block is None:
1226
1222
  details = f"Failed to generate metadata block for workflow '{file_name}'."
1227
- logger.error(details)
1228
1223
  raise ValueError(details)
1229
1224
 
1230
1225
  import_recorder = ImportRecorder()
@@ -1423,7 +1418,6 @@ class WorkflowManager:
1423
1418
  )
1424
1419
  except Exception as err:
1425
1420
  details = f"Attempted to save workflow '{relative_file_path}', but {err}"
1426
- logger.error(details)
1427
1421
  return SaveWorkflowResultFailure(result_details=details)
1428
1422
 
1429
1423
  # Create the pathing and write the file
@@ -1431,7 +1425,6 @@ class WorkflowManager:
1431
1425
  file_path.parent.mkdir(parents=True, exist_ok=True)
1432
1426
  except OSError as e:
1433
1427
  details = f"Attempted to save workflow '{file_name}'. Failed when creating directory: {e}"
1434
- logger.error(details)
1435
1428
  return SaveWorkflowResultFailure(result_details=details)
1436
1429
 
1437
1430
  # Check disk space before writing
@@ -1440,7 +1433,6 @@ class WorkflowManager:
1440
1433
  if not OSManager.check_available_disk_space(file_path.parent, min_space_gb):
1441
1434
  error_msg = OSManager.format_disk_space_error(file_path.parent)
1442
1435
  details = f"Attempted to save workflow '{file_name}' (requires {min_space_gb:.1f} GB). Failed: {error_msg}"
1443
- logger.error(details)
1444
1436
  return SaveWorkflowResultFailure(result_details=details)
1445
1437
 
1446
1438
  try:
@@ -1448,7 +1440,6 @@ class WorkflowManager:
1448
1440
  file.write(final_code_output)
1449
1441
  except OSError as e:
1450
1442
  details = f"Attempted to save workflow '{file_name}'. Failed when writing file: {e}"
1451
- logger.error(details)
1452
1443
  return SaveWorkflowResultFailure(result_details=details)
1453
1444
 
1454
1445
  # save the created workflow as an entry in the JSON config file.
@@ -1460,15 +1451,15 @@ class WorkflowManager:
1460
1451
  GriptapeNodes.ConfigManager().save_user_workflow_json(str(file_path))
1461
1452
  except OSError as e:
1462
1453
  details = f"Attempted to save workflow '{file_name}'. Failed when saving configuration: {e}"
1463
- logger.error(details)
1464
1454
  return SaveWorkflowResultFailure(result_details=details)
1465
1455
  WorkflowRegistry.generate_new_workflow(metadata=workflow_metadata, file_path=relative_file_path)
1466
1456
  # Update existing workflow's metadata in the registry
1467
1457
  existing_workflow = WorkflowRegistry.get_workflow_by_name(file_name)
1468
1458
  existing_workflow.metadata = workflow_metadata
1469
1459
  details = f"Successfully saved workflow to: {file_path}"
1470
- logger.info(details)
1471
- return SaveWorkflowResultSuccess(file_path=str(file_path))
1460
+ return SaveWorkflowResultSuccess(
1461
+ file_path=str(file_path), result_details=ResultDetails(message=details, level=logging.INFO)
1462
+ )
1472
1463
 
1473
1464
  def _generate_workflow_metadata( # noqa: PLR0913
1474
1465
  self,
@@ -1615,7 +1606,7 @@ class WorkflowManager:
1615
1606
  ),
1616
1607
  )
1617
1608
 
1618
- # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor()
1609
+ # Create conditional logic: workflow_executor = workflow_executor or LocalWorkflowExecutor(storage_backend=storage_backend_enum)
1619
1610
  executor_assign = ast.Assign(
1620
1611
  targets=[ast.Name(id="workflow_executor", ctx=ast.Store())],
1621
1612
  value=ast.BoolOp(
@@ -1625,31 +1616,45 @@ class WorkflowManager:
1625
1616
  ast.Call(
1626
1617
  func=ast.Name(id="LocalWorkflowExecutor", ctx=ast.Load()),
1627
1618
  args=[],
1628
- keywords=[],
1619
+ keywords=[
1620
+ ast.keyword(
1621
+ arg="storage_backend", value=ast.Name(id="storage_backend_enum", ctx=ast.Load())
1622
+ ),
1623
+ ],
1629
1624
  ),
1630
1625
  ],
1631
1626
  ),
1632
1627
  )
1633
- run_call = ast.Expr(
1634
- value=ast.Await(
1635
- value=ast.Call(
1636
- func=ast.Attribute(
1637
- value=ast.Name(id="workflow_executor", ctx=ast.Load()),
1638
- attr="arun",
1639
- ctx=ast.Load(),
1640
- ),
1641
- args=[],
1642
- keywords=[
1643
- ast.keyword(arg="workflow_name", value=ast.Constant(flow_name)),
1644
- ast.keyword(arg="flow_input", value=ast.Name(id="input", ctx=ast.Load())),
1645
- ast.keyword(arg="storage_backend", value=ast.Name(id="storage_backend_enum", ctx=ast.Load())),
1646
- ],
1628
+ # Use async context manager for workflow execution
1629
+ with_stmt = ast.AsyncWith(
1630
+ items=[
1631
+ ast.withitem(
1632
+ context_expr=ast.Name(id="workflow_executor", ctx=ast.Load()),
1633
+ optional_vars=ast.Name(id="executor", ctx=ast.Store()),
1647
1634
  )
1648
- )
1635
+ ],
1636
+ body=[
1637
+ ast.Expr(
1638
+ value=ast.Await(
1639
+ value=ast.Call(
1640
+ func=ast.Attribute(
1641
+ value=ast.Name(id="executor", ctx=ast.Load()),
1642
+ attr="arun",
1643
+ ctx=ast.Load(),
1644
+ ),
1645
+ args=[],
1646
+ keywords=[
1647
+ ast.keyword(arg="workflow_name", value=ast.Constant(flow_name)),
1648
+ ast.keyword(arg="flow_input", value=ast.Name(id="input", ctx=ast.Load())),
1649
+ ],
1650
+ )
1651
+ )
1652
+ )
1653
+ ],
1649
1654
  )
1650
1655
  return_stmt = ast.Return(
1651
1656
  value=ast.Attribute(
1652
- value=ast.Name(id="workflow_executor", ctx=ast.Load()),
1657
+ value=ast.Name(id="executor", ctx=ast.Load()),
1653
1658
  attr="output",
1654
1659
  ctx=ast.Load(),
1655
1660
  )
@@ -1659,7 +1664,7 @@ class WorkflowManager:
1659
1664
  async_func_def = ast.AsyncFunctionDef(
1660
1665
  name="aexecute_workflow",
1661
1666
  args=args,
1662
- body=[ensure_context_call, storage_backend_convert, executor_assign, run_call, return_stmt],
1667
+ body=[ensure_context_call, storage_backend_convert, executor_assign, with_stmt, return_stmt],
1663
1668
  decorator_list=[],
1664
1669
  returns=return_annotation,
1665
1670
  type_params=[],
@@ -3122,12 +3127,10 @@ class WorkflowManager:
3122
3127
  result = flow_manager.on_get_top_level_flow_request(GetTopLevelFlowRequest())
3123
3128
  if result.failed():
3124
3129
  details = f"Workflow '{workflow_name}' does not have a top-level flow."
3125
- logger.error(details)
3126
3130
  raise ValueError(details)
3127
3131
  flow_name = cast("GetTopLevelFlowResultSuccess", result).flow_name
3128
3132
  if flow_name is None:
3129
3133
  details = f"Workflow '{workflow_name}' does not have a top-level flow."
3130
- logger.error(details)
3131
3134
  raise ValueError(details)
3132
3135
 
3133
3136
  control_flow = flow_manager.get_flow_by_name(flow_name)
@@ -3195,22 +3198,21 @@ class WorkflowManager:
3195
3198
  file_name=workflow_file.name,
3196
3199
  )
3197
3200
  )
3201
+ result_messages = []
3198
3202
  if isinstance(register_workflow_result, RegisterWorkflowResultSuccess):
3199
- logger.info(
3200
- "Successfully registered new workflow with file '%s'.",
3201
- workflow_file.name,
3202
- )
3203
+ success_message = f"Successfully registered new workflow with file '{workflow_file.name}'."
3204
+ result_messages.append(ResultDetail(message=success_message, level=logging.INFO))
3203
3205
  else:
3204
- logger.warning(
3205
- "Failed to register workflow with file '%s': %s",
3206
- workflow_file.name,
3207
- cast("RegisterWorkflowResultFailure", register_workflow_result).exception,
3208
- )
3206
+ failure_message = f"Failed to register workflow with file '{workflow_file.name}': {cast('RegisterWorkflowResultFailure', register_workflow_result).exception}"
3207
+ result_messages.append(ResultDetail(message=failure_message, level=logging.WARNING))
3209
3208
  else:
3210
- logger.warning(
3211
- "Failed to load metadata for workflow file '%s'. Not registering workflow.",
3212
- workflow_file.name,
3209
+ metadata_failure_message = (
3210
+ f"Failed to load metadata for workflow file '{workflow_file.name}'. Not registering workflow."
3213
3211
  )
3212
+ result_messages = [ResultDetail(message=metadata_failure_message, level=logging.WARNING)]
3213
+
3214
+ # Log all messages through consolidated ResultDetails
3215
+ ResultDetails(*result_messages)
3214
3216
 
3215
3217
  def on_import_workflow_as_referenced_sub_flow_request(
3216
3218
  self, request: ImportWorkflowAsReferencedSubFlowRequest
@@ -3240,7 +3242,6 @@ class WorkflowManager:
3240
3242
  workflow = self._get_workflow_by_name(request.workflow_name)
3241
3243
  except KeyError:
3242
3244
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow is not registered"
3243
- logger.error(details)
3244
3245
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3245
3246
 
3246
3247
  # Check workflow version - Schema version 0.6.0+ required for referenced workflow imports
@@ -3249,7 +3250,6 @@ class WorkflowManager:
3249
3250
  workflow_version = Version.from_string(workflow.metadata.schema_version)
3250
3251
  if workflow_version is None or workflow_version < required_version:
3251
3252
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow version '{workflow.metadata.schema_version}' is less than required version '0.6.0'. To remedy, open the workflow you are attempting to import and save it again to upgrade it to the latest version."
3252
- logger.error(details)
3253
3253
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3254
3254
 
3255
3255
  # Check target flow
@@ -3257,7 +3257,6 @@ class WorkflowManager:
3257
3257
  if flow_name is None:
3258
3258
  if not GriptapeNodes.ContextManager().has_current_flow():
3259
3259
  details = f"Attempted to import workflow '{request.workflow_name}' into Current Context. Failed because Current Context was empty"
3260
- logger.error(details)
3261
3260
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3262
3261
  else:
3263
3262
  # Validate that the specified flow exists
@@ -3266,7 +3265,6 @@ class WorkflowManager:
3266
3265
  flow_manager.get_flow_by_name(flow_name)
3267
3266
  except KeyError:
3268
3267
  details = f"Attempted to import workflow '{request.workflow_name}' into flow '{flow_name}'. Failed because target flow does not exist"
3269
- logger.error(details)
3270
3268
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3271
3269
 
3272
3270
  return None
@@ -3290,7 +3288,6 @@ class WorkflowManager:
3290
3288
 
3291
3289
  if not workflow_result.execution_successful:
3292
3290
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because workflow execution failed: {workflow_result.execution_details}"
3293
- logger.error(details)
3294
3291
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3295
3292
 
3296
3293
  # Get flows after importing to find the new referenced sub flow
@@ -3299,7 +3296,6 @@ class WorkflowManager:
3299
3296
 
3300
3297
  if not new_flows:
3301
3298
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because no new flow was created"
3302
- logger.error(details)
3303
3299
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3304
3300
 
3305
3301
  # For now, use the first created flow as the main imported flow
@@ -3323,17 +3319,18 @@ class WorkflowManager:
3323
3319
 
3324
3320
  if not isinstance(set_metadata_result, SetFlowMetadataResultSuccess):
3325
3321
  details = f"Attempted to import workflow '{request.workflow_name}' as referenced sub flow. Failed because metadata could not be applied to created flow '{created_flow_name}'"
3326
- logger.error(details)
3327
3322
  return ImportWorkflowAsReferencedSubFlowResultFailure(result_details=details)
3328
3323
 
3329
3324
  logger.debug(
3330
3325
  "Applied imported flow metadata to '%s': %s", created_flow_name, request.imported_flow_metadata
3331
3326
  )
3332
3327
 
3333
- logger.info(
3334
- "Successfully imported workflow '%s' as referenced sub flow '%s'", request.workflow_name, created_flow_name
3328
+ details = (
3329
+ f"Successfully imported workflow '{request.workflow_name}' as referenced sub flow '{created_flow_name}'"
3330
+ )
3331
+ return ImportWorkflowAsReferencedSubFlowResultSuccess(
3332
+ created_flow_name=created_flow_name, result_details=details
3335
3333
  )
3336
- return ImportWorkflowAsReferencedSubFlowResultSuccess(created_flow_name=created_flow_name)
3337
3334
 
3338
3335
  def on_branch_workflow_request(self, request: BranchWorkflowRequest) -> ResultPayload:
3339
3336
  """Create a branch (copy) of an existing workflow with branch tracking."""
@@ -3342,7 +3339,6 @@ class WorkflowManager:
3342
3339
  source_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3343
3340
  except KeyError:
3344
3341
  details = f"Failed to branch workflow '{request.workflow_name}' because it does not exist"
3345
- logger.error(details)
3346
3342
  return BranchWorkflowResultFailure(result_details=details)
3347
3343
 
3348
3344
  # Generate branch name if not provided
@@ -3358,7 +3354,6 @@ class WorkflowManager:
3358
3354
  # Check if branch name already exists
3359
3355
  if WorkflowRegistry.has_workflow_with_name(branch_name):
3360
3356
  details = f"Failed to branch workflow '{request.workflow_name}' because branch name '{branch_name}' already exists"
3361
- logger.error(details)
3362
3357
  return BranchWorkflowResultFailure(result_details=details)
3363
3358
 
3364
3359
  try:
@@ -3387,7 +3382,6 @@ class WorkflowManager:
3387
3382
  source_file_path = WorkflowRegistry.get_complete_file_path(source_workflow.file_path)
3388
3383
  if not Path(source_file_path).exists():
3389
3384
  details = f"Failed to branch workflow '{request.workflow_name}': File path '{source_file_path}' does not exist. The workflow may have been moved or the workspace configuration may have changed."
3390
- logger.error(details)
3391
3385
  return BranchWorkflowResultFailure(result_details=details)
3392
3386
 
3393
3387
  source_content = Path(source_file_path).read_text(encoding="utf-8")
@@ -3396,7 +3390,6 @@ class WorkflowManager:
3396
3390
  branch_content = self._replace_workflow_metadata_header(source_content, branch_metadata)
3397
3391
  if branch_content is None:
3398
3392
  details = f"Failed to replace metadata header for branch workflow '{branch_name}'"
3399
- logger.error(details)
3400
3393
  return BranchWorkflowResultFailure(result_details=details)
3401
3394
 
3402
3395
  # Write branch workflow file to disk BEFORE registering in registry
@@ -3410,14 +3403,15 @@ class WorkflowManager:
3410
3403
  config_manager = GriptapeNodes.ConfigManager()
3411
3404
  config_manager.save_user_workflow_json(branch_full_path)
3412
3405
 
3413
- logger.info("Successfully branched workflow '%s' as '%s'", request.workflow_name, branch_name)
3406
+ details = f"Successfully branched workflow '{request.workflow_name}' as '{branch_name}'"
3414
3407
  return BranchWorkflowResultSuccess(
3415
- branched_workflow_name=branch_name, original_workflow_name=request.workflow_name
3408
+ branched_workflow_name=branch_name,
3409
+ original_workflow_name=request.workflow_name,
3410
+ result_details=ResultDetails(message=details, level=logging.INFO),
3416
3411
  )
3417
3412
 
3418
3413
  except Exception as e:
3419
3414
  details = f"Failed to branch workflow '{request.workflow_name}': {e!s}"
3420
- logger.error(details)
3421
3415
  import traceback
3422
3416
 
3423
3417
  traceback.print_exc()
@@ -3430,14 +3424,12 @@ class WorkflowManager:
3430
3424
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3431
3425
  except KeyError as e:
3432
3426
  details = f"Failed to merge workflow branch because it does not exist: {e!s}"
3433
- logger.error(details)
3434
3427
  return MergeWorkflowBranchResultFailure(result_details=details)
3435
3428
 
3436
3429
  # Get source workflow name from branch metadata
3437
3430
  source_workflow_name = branch_workflow.metadata.branched_from
3438
3431
  if not source_workflow_name:
3439
3432
  details = f"Failed to merge workflow branch '{request.workflow_name}' because it has no source workflow"
3440
- logger.error(details)
3441
3433
  return MergeWorkflowBranchResultFailure(result_details=details)
3442
3434
 
3443
3435
  # Validate source workflow exists
@@ -3445,7 +3437,6 @@ class WorkflowManager:
3445
3437
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3446
3438
  except KeyError:
3447
3439
  details = f"Failed to merge workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3448
- logger.error(details)
3449
3440
  return MergeWorkflowBranchResultFailure(result_details=details)
3450
3441
 
3451
3442
  try:
@@ -3475,7 +3466,6 @@ class WorkflowManager:
3475
3466
  merged_content = self._replace_workflow_metadata_header(branch_content, merged_metadata)
3476
3467
  if merged_content is None:
3477
3468
  details = f"Failed to replace metadata header for merged workflow '{source_workflow_name}'"
3478
- logger.error(details)
3479
3469
  return MergeWorkflowBranchResultFailure(result_details=details)
3480
3470
 
3481
3471
  # Write the updated content to the source workflow file
@@ -3486,28 +3476,28 @@ class WorkflowManager:
3486
3476
  source_workflow.metadata = merged_metadata
3487
3477
 
3488
3478
  # Remove the branch workflow from registry and delete file
3479
+ result_messages = []
3489
3480
  try:
3490
3481
  WorkflowRegistry.delete_workflow_by_name(request.workflow_name)
3491
3482
  Path(branch_content_file_path).unlink()
3492
- logger.info("Deleted branch workflow file and registry entry for '%s'", request.workflow_name)
3483
+ cleanup_message = f"Deleted branch workflow file and registry entry for '{request.workflow_name}'"
3484
+ result_messages.append(ResultDetail(message=cleanup_message, level=logging.INFO))
3493
3485
  except Exception as delete_error:
3494
- logger.warning(
3495
- "Failed to fully clean up branch workflow '%s': %s",
3496
- request.workflow_name,
3497
- str(delete_error),
3486
+ warning_message = (
3487
+ f"Failed to fully clean up branch workflow '{request.workflow_name}': {delete_error!s}"
3498
3488
  )
3489
+ result_messages.append(ResultDetail(message=warning_message, level=logging.WARNING))
3499
3490
  # Continue anyway - the merge was successful even if cleanup failed
3500
3491
 
3501
- logger.info(
3502
- "Successfully merged branch workflow '%s' into source workflow '%s'",
3503
- request.workflow_name,
3504
- source_workflow_name,
3492
+ success_message = f"Successfully merged branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}'"
3493
+ result_messages.append(ResultDetail(message=success_message, level=logging.INFO))
3494
+
3495
+ return MergeWorkflowBranchResultSuccess(
3496
+ merged_workflow_name=source_workflow_name, result_details=ResultDetails(*result_messages)
3505
3497
  )
3506
- return MergeWorkflowBranchResultSuccess(merged_workflow_name=source_workflow_name)
3507
3498
 
3508
3499
  except Exception as e:
3509
3500
  details = f"Failed to merge branch workflow '{request.workflow_name}' into source workflow '{source_workflow_name}': {e!s}"
3510
- logger.error(details)
3511
3501
  return MergeWorkflowBranchResultFailure(result_details=details)
3512
3502
 
3513
3503
  def on_reset_workflow_branch_request(self, request: ResetWorkflowBranchRequest) -> ResultPayload:
@@ -3517,14 +3507,12 @@ class WorkflowManager:
3517
3507
  branch_workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3518
3508
  except KeyError as e:
3519
3509
  details = f"Failed to reset workflow branch because it does not exist: {e!s}"
3520
- logger.error(details)
3521
3510
  return ResetWorkflowBranchResultFailure(result_details=details)
3522
3511
 
3523
3512
  # Get source workflow name from branch metadata
3524
3513
  source_workflow_name = branch_workflow.metadata.branched_from
3525
3514
  if not source_workflow_name:
3526
3515
  details = f"Failed to reset workflow branch '{request.workflow_name}' because it has no source workflow"
3527
- logger.error(details)
3528
3516
  return ResetWorkflowBranchResultFailure(result_details=details)
3529
3517
 
3530
3518
  # Validate source workflow exists
@@ -3532,7 +3520,6 @@ class WorkflowManager:
3532
3520
  source_workflow = WorkflowRegistry.get_workflow_by_name(source_workflow_name)
3533
3521
  except KeyError:
3534
3522
  details = f"Failed to reset workflow branch '{request.workflow_name}' because source workflow '{source_workflow_name}' does not exist"
3535
- logger.error(details)
3536
3523
  return ResetWorkflowBranchResultFailure(result_details=details)
3537
3524
 
3538
3525
  try:
@@ -3562,7 +3549,6 @@ class WorkflowManager:
3562
3549
  reset_content = self._replace_workflow_metadata_header(source_content, reset_metadata)
3563
3550
  if reset_content is None:
3564
3551
  details = f"Failed to replace metadata header for reset branch workflow '{request.workflow_name}'"
3565
- logger.error(details)
3566
3552
  return ResetWorkflowBranchResultFailure(result_details=details)
3567
3553
 
3568
3554
  # Write the updated content to the branch workflow file
@@ -3574,15 +3560,13 @@ class WorkflowManager:
3574
3560
 
3575
3561
  except Exception as e:
3576
3562
  details = f"Failed to reset branch workflow '{request.workflow_name}' to source workflow '{source_workflow_name}': {e!s}"
3577
- logger.error(details)
3578
3563
  return ResetWorkflowBranchResultFailure(result_details=details)
3579
3564
  else:
3580
- logger.info(
3581
- "Successfully reset branch workflow '%s' to match source workflow '%s'",
3582
- request.workflow_name,
3583
- source_workflow_name,
3565
+ details = f"Successfully reset branch workflow '{request.workflow_name}' to match source workflow '{source_workflow_name}'"
3566
+ return ResetWorkflowBranchResultSuccess(
3567
+ reset_workflow_name=request.workflow_name,
3568
+ result_details=ResultDetails(message=details, level=logging.INFO),
3584
3569
  )
3585
- return ResetWorkflowBranchResultSuccess(reset_workflow_name=request.workflow_name)
3586
3570
 
3587
3571
  def on_compare_workflows_request(self, request: CompareWorkflowsRequest) -> ResultPayload:
3588
3572
  """Compare two workflows to determine if one is ahead, behind, or up-to-date relative to the other."""
@@ -3591,7 +3575,6 @@ class WorkflowManager:
3591
3575
  workflow = WorkflowRegistry.get_workflow_by_name(request.workflow_name)
3592
3576
  except KeyError:
3593
3577
  details = f"Failed to compare workflow '{request.workflow_name}' because it does not exist"
3594
- logger.error(details)
3595
3578
  return CompareWorkflowsResultFailure(result_details=details)
3596
3579
 
3597
3580
  # Use the provided compare_workflow_name
@@ -3612,6 +3595,7 @@ class WorkflowManager:
3612
3595
  else None,
3613
3596
  source_last_modified=None,
3614
3597
  details=details,
3598
+ result_details="Workflow comparison completed successfully.",
3615
3599
  )
3616
3600
 
3617
3601
  # Compare last modified dates
@@ -3629,6 +3613,7 @@ class WorkflowManager:
3629
3613
  workflow_last_modified=workflow_last_modified.isoformat() if workflow_last_modified else None,
3630
3614
  source_last_modified=source_last_modified.isoformat() if source_last_modified else None,
3631
3615
  details=details,
3616
+ result_details="Workflow comparison completed successfully.",
3632
3617
  )
3633
3618
 
3634
3619
  # Compare timestamps to determine status
@@ -3651,6 +3636,7 @@ class WorkflowManager:
3651
3636
  workflow_last_modified=workflow_last_modified.isoformat(),
3652
3637
  source_last_modified=source_last_modified.isoformat(),
3653
3638
  details=details,
3639
+ result_details="Workflow comparison completed successfully.",
3654
3640
  )
3655
3641
 
3656
3642
  def _walk_object_tree(
@@ -3810,18 +3796,26 @@ class WorkflowManager:
3810
3796
  try:
3811
3797
  workflows_to_register = GriptapeNodes.ConfigManager().get_config_value(request.config_section)
3812
3798
  if not workflows_to_register:
3813
- logger.info("No workflows found in configuration section '%s'", request.config_section)
3814
- return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=[], failed_workflows=[])
3799
+ details = f"No workflows found in configuration section '{request.config_section}'"
3800
+ return RegisterWorkflowsFromConfigResultSuccess(
3801
+ succeeded_workflows=[], failed_workflows=[], result_details=details
3802
+ )
3815
3803
 
3816
3804
  # Process all workflows and track results
3817
3805
  succeeded, failed = self._process_workflows_for_registration(workflows_to_register)
3818
3806
 
3819
3807
  except Exception as e:
3820
3808
  details = f"Failed to register workflows from configuration section '{request.config_section}': {e!s}"
3821
- logger.error(details)
3822
3809
  return RegisterWorkflowsFromConfigResultFailure(result_details=details)
3823
3810
  else:
3824
- return RegisterWorkflowsFromConfigResultSuccess(succeeded_workflows=succeeded, failed_workflows=failed)
3811
+ return RegisterWorkflowsFromConfigResultSuccess(
3812
+ succeeded_workflows=succeeded,
3813
+ failed_workflows=failed,
3814
+ result_details=ResultDetails(
3815
+ message=f"Successfully processed workflows: {len(succeeded)} succeeded, {len(failed)} failed.",
3816
+ level=logging.INFO,
3817
+ ),
3818
+ )
3825
3819
 
3826
3820
  def _process_workflows_for_registration(self, workflows_to_register: list[str]) -> WorkflowRegistrationResult:
3827
3821
  """Process a list of workflow paths for registration.
@@ -3832,33 +3826,9 @@ class WorkflowManager:
3832
3826
  succeeded = []
3833
3827
  failed = []
3834
3828
 
3835
- for workflow_to_register in workflows_to_register:
3836
- path = Path(workflow_to_register)
3837
-
3838
- if path.is_dir():
3839
- dir_result = self._process_workflow_directory(path)
3840
- succeeded.extend(dir_result.succeeded)
3841
- failed.extend(dir_result.failed)
3842
- elif path.suffix == ".py":
3843
- workflow_name = self._process_single_workflow_file(path)
3844
- if workflow_name:
3845
- succeeded.append(workflow_name)
3846
- else:
3847
- failed.append(str(path))
3848
-
3849
- return WorkflowRegistrationResult(succeeded=succeeded, failed=failed)
3850
-
3851
- def _process_workflow_directory(self, directory_path: Path) -> WorkflowRegistrationResult:
3852
- """Process all workflow files in a directory.
3853
-
3854
- Returns:
3855
- WorkflowRegistrationResult with succeeded and failed workflow names
3856
- """
3857
- succeeded = []
3858
- failed = []
3859
-
3860
- for workflow_file in directory_path.glob("*.py"):
3861
- # Check that the python file has script metadata
3829
+ def process_workflow_file(workflow_file: Path) -> None:
3830
+ """Process a single workflow file for registration."""
3831
+ # Check if the file has workflow metadata before processing
3862
3832
  metadata_blocks = self.get_workflow_metadata(
3863
3833
  workflow_file, block_name=WorkflowManager.WORKFLOW_METADATA_HEADER
3864
3834
  )
@@ -3869,6 +3839,18 @@ class WorkflowManager:
3869
3839
  else:
3870
3840
  failed.append(str(workflow_file))
3871
3841
 
3842
+ def process_path(path: Path) -> None:
3843
+ """Process a path, handling both files and directories."""
3844
+ if path.is_dir():
3845
+ # Process all Python files recursively in the directory
3846
+ for workflow_file in path.rglob("*.py"):
3847
+ process_workflow_file(workflow_file)
3848
+ elif path.suffix == ".py":
3849
+ process_workflow_file(path)
3850
+
3851
+ for workflow_to_register in workflows_to_register:
3852
+ process_path(Path(workflow_to_register))
3853
+
3872
3854
  return WorkflowRegistrationResult(succeeded=succeeded, failed=failed)
3873
3855
 
3874
3856
  def _process_single_workflow_file(self, workflow_file: Path) -> str | None:
@@ -3877,6 +3859,8 @@ class WorkflowManager:
3877
3859
  Returns:
3878
3860
  Workflow name if registered successfully, None if failed or skipped
3879
3861
  """
3862
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
3863
+
3880
3864
  # Parse metadata once and use it for both registration check and actual registration
3881
3865
  load_metadata_request = LoadWorkflowMetadata(file_name=str(workflow_file))
3882
3866
  load_metadata_result = self.on_load_workflow_metadata_request(load_metadata_request)
@@ -3892,10 +3876,20 @@ class WorkflowManager:
3892
3876
  logger.debug("Skipping already registered workflow: %s", workflow_file)
3893
3877
  return None
3894
3878
 
3879
+ # Convert to relative path if the workflow is under workspace_path
3880
+ config_mgr = GriptapeNodes.ConfigManager()
3881
+ workspace_path = config_mgr.workspace_path
3882
+
3883
+ if workflow_file.is_relative_to(workspace_path):
3884
+ relative_path = workflow_file.relative_to(workspace_path)
3885
+ file_path_to_register = str(relative_path)
3886
+ else:
3887
+ file_path_to_register = str(workflow_file)
3888
+
3895
3889
  # Register workflow using existing method with parsed metadata available
3896
3890
  # The _register_workflow method will re-parse metadata, but this is acceptable
3897
3891
  # since we've already validated it's parseable and the duplicate work is minimal
3898
- if self._register_workflow(str(workflow_file)):
3892
+ if self._register_workflow(file_path_to_register):
3899
3893
  return workflow_metadata.name
3900
3894
  return None
3901
3895