diff --git a/module_renderer.py b/module_renderer.py index 1e5f63b..a192892 100644 --- a/module_renderer.py +++ b/module_renderer.py @@ -275,4 +275,5 @@ def render_module(self) -> None: last_module_name = self.filename.replace(plain_file.PLAIN_SOURCE_FILE_EXTENSION, "") rendered_code_path = f"{os.path.join(self.args.build_folder, last_module_name)}/" + self.run_state.set_render_generated_code_path(rendered_code_path) self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path)) diff --git a/plain2code.py b/plain2code.py index 06dd21f..95b1b04 100644 --- a/plain2code.py +++ b/plain2code.py @@ -5,7 +5,9 @@ import signal import sys import threading +import traceback from pathlib import Path +from types import TracebackType from typing import Optional import yaml @@ -47,7 +49,7 @@ get_log_file_path, ) from plain2code_state import RunState -from plain2code_utils import print_dry_run_output +from plain2code_utils import format_duration_hms, print_dry_run_output from system_config import system_config from tui.plain2code_tui import Plain2CodeTUI @@ -55,6 +57,30 @@ RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7 +def print_exit_summary( + run_state: RunState, + spec_filename: str, + error_message: Optional[str] = None, + verbose: bool = False, + exc_info: Optional[tuple[type[BaseException] | None, BaseException | None, TracebackType | None]] = None, +) -> None: + console.quiet = False + """Print render outcome after the TUI exits (terminal restored).""" + + msg = "\n[#79FC96]✓ rendering completed\n\n" if run_state.render_succeeded else "[#FF6B6B]✗ rendering failed\n\n" + msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n" + msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n" + msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n" + msg += f"[#8E8F91]functionalities [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]used credits [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]render time [#FFFFFF]{format_duration_hms(run_state.render_time)}\n" + console.info(msg) + + if not run_state.render_succeeded and error_message: + console.error(error_message) + if verbose and exc_info and exc_info[0] is not None: + console.error("".join(traceback.format_exception(*exc_info))) + console.quiet = True + + def get_render_range(render_range, plain_source): render_range = render_range.split(",") range_end = render_range[1] if len(render_range) == 2 else render_range[0] @@ -186,8 +212,6 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI): def render(args, run_state: RunState, event_bus: EventBus): # noqa: C901 template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS) - console.info(f"Rendering {args.filename} to target code.") - # Compute render range from either --render-range or --render-from render_range = None if args.render_range or args.render_from: @@ -219,14 +243,16 @@ def render(args, run_state: RunState, event_bus: EventBus): # noqa: C901 ) render_error: list[Exception] = [] + run_state.render_succeeded = False def run_render(): try: module_renderer.render_module() - console.info(f"[#79FC96]Render {run_state.render_id} completed successfully.[/#79FC96]") + run_state.set_render_succeeded(True) except RenderCancelledError: pass # TUI already closed, nothing to report except Exception as e: + run_state.set_render_succeeded(False) render_error.append(e) event_bus.publish(RenderFailed(error_message=str(e))) @@ -234,7 +260,9 @@ def run_render(): console.info(f"Render started. Render ID: {run_state.render_id}") try: module_renderer.render_module() + run_state.set_render_succeeded(True) except RenderCancelledError: + run_state.set_render_succeeded(False) pass return else: @@ -251,6 +279,7 @@ def run_render(): css_path="styles.css", ) app.run() + stop_event.set() render_thread.join(timeout=RENDER_THREAD_SHUTDOWN_TIMEOUT) @@ -298,85 +327,72 @@ def main(): # noqa: C901 setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename, args.headless) exc_info = None + error_message = None + try: # Validate API key is present if not args.api_key: raise MissingAPIKey( "API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument." ) - - console.info(f"Render ID: {run_state.render_id}") render(args, run_state, event_bus) except InvalidFridArgument as e: - exc_info = sys.exc_info() - console.error(f"Invalid FRID argument: {str(e)}.\n") + error_message = f"Invalid FRID argument: {str(e)}.\n" except FileNotFoundError as e: - exc_info = sys.exc_info() - console.error(f"File not found: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"File not found: {str(e)}\n" + except MissingResource as e: + error_message = f"Missing resource: {str(e)}\n" except TemplateNotFoundError as e: - exc_info = sys.exc_info() - console.error(f"Template not found: {str(e)}\n") - console.error(system_config.get_error_message("template_not_found")) + error_message = f"""Template not found: {str(e)}\n +The required template could not be found. Templates are searched in the following order (highest to lowest precedence): + + 1. The directory containing your .plain file + 2. The directory specified by --template-dir (if provided) + 3. The built-in 'standard_template_library' directory + +Please ensure that the missing template exists in one of these locations, or specify the correct --template-dir if using custom templates. + """ except PlainSyntaxError as e: - exc_info = sys.exc_info() - console.error(f"Plain syntax error: {str(e)}\n") + error_message = f"Plain syntax error: {str(e)}\n" except KeyboardInterrupt: - exc_info = sys.exc_info() - console.error("Keyboard interrupt") - console.debug(f"Render ID: {run_state.render_id}") + error_message = "Keyboard interrupt" except RequestException as e: - exc_info = sys.exc_info() - console.error(f"Error rendering plain code: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Error rendering plain code: {str(e)}\n" except MissingPreviousFunctionalitiesError as e: - exc_info = sys.exc_info() - console.error(f"Error rendering plain code: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Error rendering plain code: {str(e)}\n" except MissingAPIKey as e: - console.error(f"Missing API key: {str(e)}\n") + error_message = f"Missing API key: {str(e)}\n" except InvalidAPIKey as e: - console.error(f"Invalid API key: {str(e)}\n") + error_message = f"Invalid API key: {str(e)}\n" except OutdatedClientVersion as e: - console.error(f"Outdated client version: {str(e)}\n") + error_message = f"Outdated client version: {str(e)}\n" except (InternalServerError, InternalClientError): exc_info = sys.exc_info() - console.error( - f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file." - ) - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file." except ConflictingRequirements as e: - exc_info = sys.exc_info() - console.error(f"Conflicting requirements: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Conflicting requirements: {str(e)}\n" except RenderingCreditBalanceTooLow as e: - exc_info = sys.exc_info() - console.error(f"Credit balance too low: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Credit balance too low: {str(e)}\n" except LLMInternalError as e: exc_info = sys.exc_info() - console.error(f"LLM internal error: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") - except MissingResource as e: - exc_info = sys.exc_info() - console.error(f"Missing resource: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"LLM internal error: {str(e)}\n" except NetworkConnectionError as e: - exc_info = sys.exc_info() - console.error(f"Connection error: {str(e)}\n") - console.error("Please check that your internet connection is working.") + error_message = f"Connection error: {str(e)}\n\nPlease check that your internet connection is working." except ModuleDoesNotExistError as e: - exc_info = sys.exc_info() - console.error(f"Module does not exist: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = str(e) except Exception as e: exc_info = sys.exc_info() - console.error(f"Error rendering plain code: {str(e)}\n") - console.debug(f"Render ID: {run_state.render_id}") + error_message = f"Error rendering plain code: {str(e)}\n" finally: + print_exit_summary( + run_state, + args.filename, + error_message=error_message, + verbose=args.verbose, + exc_info=exc_info, + ) if exc_info: - # Log traceback using the logging system - logging.error("Render crashed with exception:", exc_info=exc_info) + # Log traceback dump_crash_logs(args) diff --git a/plain2code_state.py b/plain2code_state.py index a66b7e1..6a6ddfa 100644 --- a/plain2code_state.py +++ b/plain2code_state.py @@ -1,5 +1,6 @@ """Contains all state and context information we need for the rendering process.""" +import time import uuid from typing import Optional @@ -9,6 +10,9 @@ class RunState: def __init__(self, spec_filename: str, replay_with: Optional[str] = None): self.replay: bool = replay_with is not None + self.render_succeeded: bool = False + self.render_generated_code_path: Optional[str] = None + self.rendered_functionalities: int = 0 if replay_with: self.render_id: str = replay_with else: @@ -17,6 +21,8 @@ def __init__(self, spec_filename: str, replay_with: Optional[str] = None): self.call_count: int = 0 self.unittest_batch_id: int = 0 self.frid_render_anaysis: dict[str, str] = {} + self.render_time: int = 0 + self.last_render_resume_timestamp: float = time.monotonic() def increment_call_count(self): self.call_count += 1 @@ -27,6 +33,21 @@ def increment_unittest_batch_id(self): def add_rendering_analysis_for_frid(self, frid, rendering_analysis) -> None: self.frid_render_anaysis[frid] = rendering_analysis + def set_render_succeeded(self, succeeded: bool): + self.render_succeeded = succeeded + + def set_render_generated_code_path(self, generated_code_path: str): + self.render_generated_code_path = generated_code_path + + def increment_rendered_functionalities(self): + self.rendered_functionalities += 1 + + def add_to_render_time(self): + self.render_time += int(time.monotonic() - self.last_render_resume_timestamp) + + def set_last_render_resume_timestamp(self): + self.last_render_resume_timestamp = time.monotonic() + def to_dict(self): return { "render_id": self.render_id, diff --git a/plain2code_utils.py b/plain2code_utils.py index d500d52..9a7cf41 100644 --- a/plain2code_utils.py +++ b/plain2code_utils.py @@ -3,6 +3,22 @@ import plain_spec from plain2code_console import console + +def format_duration_hms(total_seconds: int) -> str: + """Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``).""" + if total_seconds < 0: + total_seconds = 0 + h = int(total_seconds // 3600) + m = int((total_seconds % 3600) // 60) + s = total_seconds % 60 + if h: + return f"{h}h {m}m {s}s" + if m: + return f"{m}m {s}s" + text = f"{s}".rstrip("0").rstrip(".") + return f"{text}s" if text else "0s" + + AMBIGUITY_CAUSES = { "reference_resource_ambiguity": "Ambiguity is in the reference resources", "definition_ambiguity": "Ambiguity is in the definitions", diff --git a/render_machine/actions/fix_conformance_test.py b/render_machine/actions/fix_conformance_test.py index 076c195..066a3fb 100644 --- a/render_machine/actions/fix_conformance_test.py +++ b/render_machine/actions/fix_conformance_test.py @@ -31,11 +31,6 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | render_context.conformance_tests_running_context.current_testing_frid ) - if render_context.conformance_tests_running_context.current_testing_frid == render_context.frid_context.frid: - console_message = f"Fixing conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name}." - else: - console_message = f"While implementing functionality {render_context.frid_context.frid}, conformance tests for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name} broke. Fixing them..." - existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files( render_context.build_folder ) @@ -78,7 +73,6 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | style=console.INPUT_STYLE, ) - with console.status(console_message): [conformance_tests_fixed, response_files] = render_context.codeplain_api.fix_conformance_tests_issue( render_context.frid_context.frid, render_context.conformance_tests_running_context.current_testing_frid, diff --git a/render_machine/actions/fix_unit_tests.py b/render_machine/actions/fix_unit_tests.py index 977a7dd..db09753 100644 --- a/render_machine/actions/fix_unit_tests.py +++ b/render_machine/actions/fix_unit_tests.py @@ -33,19 +33,16 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | render_context, existing_files_content, "Files sent as input to unit tests fixing:" ) - with console.status( - f"[{console.INFO_STYLE}]Fixing unit tests issue for functionality {render_context.frid_context.frid}...\n" - ): - response_files = render_context.codeplain_api.fix_unittests_issue( - render_context.frid_context.frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - previous_unittests_issue, - run_state=render_context.run_state, - ) + response_files = render_context.codeplain_api.fix_unittests_issue( + render_context.frid_context.frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + previous_unittests_issue, + run_state=render_context.run_state, + ) _, changed_files = file_utils.update_build_folder_with_rendered_files( render_context.build_folder, existing_files, response_files diff --git a/render_machine/actions/refactor_code.py b/render_machine/actions/refactor_code.py index 728fe00..2ad26a7 100644 --- a/render_machine/actions/refactor_code.py +++ b/render_machine/actions/refactor_code.py @@ -26,15 +26,13 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | existing_files_content, style=console.INPUT_STYLE, ) - with console.status( - f"[{console.INFO_STYLE}]Refactoring the generated code for functionality {render_context.frid_context.frid}..." - ): - response_files = render_context.codeplain_api.refactor_source_files_if_needed( - frid=render_context.frid_context.frid, - files_to_check=render_context.frid_context.changed_files, - existing_files_content=existing_files_content, - run_state=render_context.run_state, - ) + + response_files = render_context.codeplain_api.refactor_source_files_if_needed( + frid=render_context.frid_context.frid, + files_to_check=render_context.frid_context.changed_files, + existing_files_content=existing_files_content, + run_state=render_context.run_state, + ) if len(response_files) == 0: if render_context.verbose: diff --git a/render_machine/actions/render_conformance_tests.py b/render_machine/actions/render_conformance_tests.py index af36215..5fb7daa 100644 --- a/render_machine/actions/render_conformance_tests.py +++ b/render_machine/actions/render_conformance_tests.py @@ -33,21 +33,18 @@ def _render_conformance_tests(self, render_context: RenderContext): ) if not render_context.conformance_tests_running_context.current_conformance_tests_exist(): - with console.status( - f"[{console.INFO_STYLE}]Generating folder name for conformance tests for functionality {render_context.conformance_tests_running_context.current_testing_frid}...\n" - ): - fr_subfolder_name = render_context.codeplain_api.generate_folder_name_from_functional_requirement( - frid=render_context.conformance_tests_running_context.current_testing_frid, - functional_requirement=render_context.conformance_tests_running_context.current_testing_frid_specifications[ - plain_spec.FUNCTIONAL_REQUIREMENTS - ][ - -1 - ], - existing_folder_names=render_context.conformance_tests.fetch_existing_conformance_test_folder_names( - render_context.conformance_tests_running_context.current_testing_module_name - ), - run_state=render_context.run_state, - ) + fr_subfolder_name = render_context.codeplain_api.generate_folder_name_from_functional_requirement( + frid=render_context.conformance_tests_running_context.current_testing_frid, + functional_requirement=render_context.conformance_tests_running_context.current_testing_frid_specifications[ + plain_spec.FUNCTIONAL_REQUIREMENTS + ][ + -1 + ], + existing_folder_names=render_context.conformance_tests.fetch_existing_conformance_test_folder_names( + render_context.conformance_tests_running_context.current_testing_module_name + ), + run_state=render_context.run_state, + ) conformance_tests_folder_name = os.path.join( render_context.conformance_tests.get_module_conformance_tests_folder(render_context.module_name), @@ -95,25 +92,23 @@ def _render_conformance_tests(self, render_context: RenderContext): ) all_acceptance_tests = render_context.frid_context.specifications.get(plain_spec.ACCEPTANCE_TESTS, []) - with console.status( - f"[{console.INFO_STYLE}]Rendering conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid}...\n" - ): - response_files, implementation_plan_summary = render_context.codeplain_api.render_conformance_tests( - render_context.frid_context.frid, - render_context.conformance_tests_running_context.current_testing_frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - memory_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - conformance_tests_folder_name, - render_context.conformance_tests_running_context.get_conformance_tests_json( - render_context.conformance_tests_running_context.current_testing_module_name - ), - all_acceptance_tests, - run_state=render_context.run_state, - ) + + response_files, implementation_plan_summary = render_context.codeplain_api.render_conformance_tests( + render_context.frid_context.frid, + render_context.conformance_tests_running_context.current_testing_frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + memory_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + conformance_tests_folder_name, + render_context.conformance_tests_running_context.get_conformance_tests_json( + render_context.conformance_tests_running_context.current_testing_module_name + ), + all_acceptance_tests, + run_state=render_context.run_state, + ) render_context.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan = ( implementation_plan_summary @@ -151,21 +146,18 @@ def _render_acceptance_test(self, render_context: RenderContext): if render_context.verbose: console.info(f"Generating acceptance test:\n {acceptance_test}") - with console.status( - f"[{console.INFO_STYLE}]Generating acceptance test for functionality {render_context.frid_context.frid}...\n" - ): - response_files = render_context.codeplain_api.render_acceptance_tests( - render_context.frid_context.frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - memory_files_content, - conformance_tests_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - acceptance_test, - run_state=render_context.run_state, - ) + response_files = render_context.codeplain_api.render_acceptance_tests( + render_context.frid_context.frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + memory_files_content, + conformance_tests_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + acceptance_test, + run_state=render_context.run_state, + ) conformance_tests_folder_name = ( render_context.conformance_tests_running_context.get_current_conformance_test_folder_name() ) diff --git a/render_machine/actions/render_functional_requirement.py b/render_machine/actions/render_functional_requirement.py index 786e50d..c2ab751 100644 --- a/render_machine/actions/render_functional_requirement.py +++ b/render_machine/actions/render_functional_requirement.py @@ -40,20 +40,17 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | render_context, existing_files_content, "Files sent as input to code generation:" ) - with console.status( - f"[{console.INFO_STYLE}]Generating functionality {render_context.frid_context.frid}...\n" - ): - response_files = render_context.codeplain_api.render_functional_requirement( - render_context.frid_context.frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - memory_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - render_context.should_run_unit_tests(), - render_context.run_state, - ) + response_files = render_context.codeplain_api.render_functional_requirement( + render_context.frid_context.frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + memory_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + render_context.should_run_unit_tests(), + render_context.run_state, + ) except FunctionalRequirementTooComplex as e: error_message = f"The functionality:\n{render_context.frid_context.functional_requirement_text}\n is too complex to be implemented. Please break down the functionality into smaller parts ({str(e)})." if e.proposed_breakdown: diff --git a/render_machine/actions/summarize_conformance_tests.py b/render_machine/actions/summarize_conformance_tests.py index e400d12..14e4d71 100644 --- a/render_machine/actions/summarize_conformance_tests.py +++ b/render_machine/actions/summarize_conformance_tests.py @@ -20,18 +20,15 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) ) - with console.status( - f"[{console.INFO_STYLE}]Summarizing finished conformance tests for functionality {render_context.frid_context.frid}...\n" - ): - summary = render_context.codeplain_api.summarize_finished_conformance_tests( - frid=render_context.frid_context.frid, - plain_source_tree=render_context.plain_source_tree, - linked_resources=render_context.frid_context.linked_resources, - conformance_test_files_content=existing_conformance_test_files_content, - module_name=render_context.module_name, - required_modules=render_context.get_required_modules_functionalities(), - run_state=render_context.run_state, - ) + summary = render_context.codeplain_api.summarize_finished_conformance_tests( + frid=render_context.frid_context.frid, + plain_source_tree=render_context.plain_source_tree, + linked_resources=render_context.frid_context.linked_resources, + conformance_test_files_content=existing_conformance_test_files_content, + module_name=render_context.module_name, + required_modules=render_context.get_required_modules_functionalities(), + run_state=render_context.run_state, + ) render_context.conformance_tests_running_context.set_conformance_tests_summary(summary) diff --git a/render_machine/code_renderer.py b/render_machine/code_renderer.py index 30ead21..3a98140 100644 --- a/render_machine/code_renderer.py +++ b/render_machine/code_renderer.py @@ -42,9 +42,11 @@ def run(self): while True: if self.render_context.enter_pause_event.is_set(): self.render_context.event_bus.publish(RenderPaused()) + self.render_context.run_state.add_to_render_time() while self.render_context.enter_pause_event.is_set(): time.sleep(PAUSE_POLL_INTERVAL_SECONDS) + self.render_context.run_state.set_last_render_resume_timestamp() self.render_context.event_bus.publish( RenderStateUpdated( diff --git a/render_machine/render_context.py b/render_machine/render_context.py index c523364..e4fb084 100644 --- a/render_machine/render_context.py +++ b/render_machine/render_context.py @@ -191,6 +191,7 @@ def check_frid_iteration_limit(self): def finish_implementing_frid(self): self.functional_requirements_render_attempts_failed_unit_during_conformance_tests = 0 + self.run_state.increment_rendered_functionalities() def should_run_unit_tests(self) -> bool: return self.unittests_script is not None @@ -453,3 +454,11 @@ def finish_fixing_conformance_tests(self): console.info( f"Running conformance tests attempt {self.conformance_tests_running_context.fix_attempts + 1}." ) + + def start_render_completed(self): + self.run_state.set_render_succeeded(True) + self.run_state.add_to_render_time() + + def start_render_failed(self): + self.run_state.set_render_succeeded(False) + self.run_state.add_to_render_time() diff --git a/render_machine/state_machine_config.py b/render_machine/state_machine_config.py index 8614078..d1afe40 100644 --- a/render_machine/state_machine_config.py +++ b/render_machine/state_machine_config.py @@ -233,8 +233,8 @@ def get_states(self, render_context: RenderContext) -> List[Any]: States.FRID_FULLY_IMPLEMENTED.value, ], }, - States.RENDER_COMPLETED.value, - States.RENDER_FAILED.value, + {"name": States.RENDER_COMPLETED.value, "on_enter": "start_render_completed"}, + {"name": States.RENDER_FAILED.value, "on_enter": "start_render_failed"}, ] def get_transitions(self) -> List[Dict[str, Any]]: