+ );
+};
export default SelectboxWidget;
diff --git a/preswald/engine/base_service.py b/preswald/engine/base_service.py
index 7c32988a..26bccace 100644
--- a/preswald/engine/base_service.py
+++ b/preswald/engine/base_service.py
@@ -1,6 +1,8 @@
import logging
import os
import time
+import contextvars
+
from collections.abc import Callable
from threading import Lock
from typing import Any, Callable, Dict, Optional
@@ -47,6 +49,8 @@ def __init__(self):
self._workflow = Workflow(service=self)
self._current_atom: Optional[str] = None
self._reactivity_enabled = True
+ self._reactivity_stack = []
+ self._render_tracking_suppressed: contextvars.ContextVar[bool] = contextvars.ContextVar("render_tracking_suppressed", default=False)
# Initialize session tracking
self.script_runners: dict[str, ScriptRunner] = {}
@@ -103,6 +107,10 @@ def script_path(self, path: str):
self._script_path = os.path.abspath(path)
self._initialize_data_manager(path)
+ @property
+ def is_render_tracking_suppressed(self) -> bool:
+ return self._render_tracking_suppressed.get()
+
@property
def is_reactivity_enabled(self):
return self._reactivity_enabled
@@ -212,11 +220,50 @@ def clear_components(self):
def disable_reactivity(self):
self._reactivity_enabled = False
- logger.info("[SERVICE] Reactivity disabled for fallback execution")
+ logger.debug("[SERVICE] Reactivity disabled for fallback execution")
def enable_reactivity(self):
self._reactivity_enabled = True
- logger.info("[SERVICE] Reactivity re-enabled")
+ logger.debug("[SERVICE] Reactivity re-enabled")
+
+ @contextmanager
+ def reactivity_disabled(self):
+ """
+ Context manager to temporarily disable reactivity and restore it on exit.
+
+ Useful for wrapping small blocks of code that should not trigger reactive behavior,
+ such as fallback rendering or side-effect only computation.
+
+ Example:
+ with service.reactivity_disabled():
+ ...
+ """
+ self._reactivity_stack.append(self._reactivity_enabled)
+ self._reactivity_enabled = False
+ logger.debug("[SERVICE] Reactivity temporarily disabled (context)")
+
+ try:
+ yield
+ finally:
+ previous_state = self._reactivity_stack.pop() if self._reactivity_stack else True
+ self._reactivity_enabled = previous_state
+ logger.debug(f"[SERVICE] Reactivity restored to {self._reactivity_enabled}")
+
+ @contextmanager
+ def render_tracking_suppressed(self):
+ """
+ Context manager to temporarily suppress render tracking effects
+ from @with_render_tracking.
+
+ This is useful when falling back to rebuild a component and just want the raw output.
+ """
+ token = self._render_tracking_suppressed.set(True)
+ logger.debug("[SERVICE] Render tracking temporarily suppressed")
+ try:
+ yield
+ finally:
+ self._render_tracking_suppressed.reset(token)
+ logger.debug("[SERVICE] Render tracking restored")
def force_recompute(self, atom_names: set[str]) -> None:
"""
diff --git a/preswald/engine/render_tracking.py b/preswald/engine/render_tracking.py
index c8fc720f..d36d5853 100644
--- a/preswald/engine/render_tracking.py
+++ b/preswald/engine/render_tracking.py
@@ -61,13 +61,16 @@ def wrapper(*args, **kwargs):
logger.debug(f"[with_render_tracking] Using provided component_id {component_id}:{atom_name}")
else:
identifier = kwargs.get("identifier")
- callsite_hint = kwargs.get("callsite_hint")
- component_id = generate_stable_id(component_type, callsite_hint=callsite_hint, identifier=identifier)
+ hint = kwargs.get('hint')
+ component_id = generate_stable_id(component_type, callsite_hint=hint, identifier=identifier)
atom_name = generate_stable_atom_name_from_component_id(component_id)
kwargs["component_id"] = component_id
logger.debug(f"[with_render_tracking] Generated component_id {component_id}:{atom_name}")
service = PreswaldService.get_instance()
+ if service.is_render_tracking_suppressed:
+ return func(*args, **kwargs)
+
if not service.is_reactivity_enabled:
result = func(*args, **kwargs)
diff --git a/preswald/engine/runner.py b/preswald/engine/runner.py
index 7289a1e4..c95f68ea 100644
--- a/preswald/engine/runner.py
+++ b/preswald/engine/runner.py
@@ -92,9 +92,8 @@ async def start(self, script_path: str):
self._run_count = 0
if reactivity_explicitly_disabled():
- self._service.disable_reactivity()
- else:
logger.info("[ScriptRunner] Reactivity is disabled by configuration")
+ self._service.disable_reactivity()
try:
await self.run_script()
diff --git a/preswald/engine/transformers/reactive_runtime.py b/preswald/engine/transformers/reactive_runtime.py
index d872a21d..96959263 100644
--- a/preswald/engine/transformers/reactive_runtime.py
+++ b/preswald/engine/transformers/reactive_runtime.py
@@ -2287,7 +2287,7 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: # noqa: N
# Attach atom decorator
callsite_metadata = self._build_callsite_metadata(node, self.filename)
- atom_name = generate_stable_id("_auto_atom", callsite_hint=callsite_metadata["callsite_hint"])
+ atom_name = generate_stable_id("_auto_atom", callsite_hint=callsite_metadata["hint"])
decorator = self._create_workflow_atom_decorator(atom_name, callsite_deps=[], callsite_metadata=callsite_metadata)
node.decorator_list.insert(0, decorator)
@@ -2483,24 +2483,49 @@ def _build_atom_function(
- Explicit return override via `return_target`
Any other AST node types passed as `call_expr` will result in a safe transformation error.
+
+ Args:
+ atom_name (str):
+ The name to assign to the generated atom function.
+
+ component_id (str):
+ The stable component ID used to uniquely identify the output component
+ associated with this atom.
+
+ callsite_deps (list[str]):
+ The list of atom names this atom depends on. These are used to
+ generate the function parameters as `param0`, `param1`, etc.
+
+ call_expr (ast.AST | list[ast.stmt]):
+ The lifted expression or block of statements that form the body of the atom.
+ Typically this is an `ast.Call`, `ast.Expr`, `ast.Assign`, or list of such nodes.
+ Used to build the body and return value of the generated function.
+
+ return_target (str | list[str] | tuple[str, ...] | ast.expr | None, optional):
+ Optional override for the return value of the function.
+ If provided, this value is returned instead of whatever is in `call_expr`.
+
+ callsite_node (ast.AST | None, optional):
+ The original AST node from the user script that corresponds to the component call.
+ Used to extract human readable metadata such as filename, line number, and source.
+ Unlike `call_expr`, this reflects the original user authored code before rewriting.
"""
# Create function parameters: (param0, param1, ...)
args_ast = self._make_param_args(callsite_deps)
- callsite_metadata = self._build_callsite_metadata(callsite_node, self.filename)
- decorator = self._create_workflow_atom_decorator(
- atom_name,
- callsite_deps,
- callsite_metadata=callsite_metadata
- )
-
# Normalize call_expr into a body list
if isinstance(call_expr, list):
body = call_expr
elif isinstance(call_expr, ast.Assign) or isinstance(call_expr, ast.Expr):
body = [call_expr]
elif isinstance(call_expr, ast.Call):
+ callsite_metadata = self._build_callsite_metadata(callsite_node, self.filename, call_expr=call_expr)
+ decorator = self._create_workflow_atom_decorator(
+ atom_name,
+ callsite_deps,
+ callsite_metadata=callsite_metadata
+ )
return_stmt = ast.Return(value=call_expr)
return ast.FunctionDef(
name=atom_name,
@@ -2517,6 +2542,13 @@ def _build_atom_function(
)
return None
+ callsite_metadata = self._build_callsite_metadata(callsite_node, self.filename)
+ decorator = self._create_workflow_atom_decorator(
+ atom_name,
+ callsite_deps,
+ callsite_metadata=callsite_metadata
+ )
+
# Append appropriate return statement
if isinstance(return_target, str):
body.append(ast.Return(value=ast.Name(id=return_target, ctx=ast.Load())))
@@ -2710,12 +2742,27 @@ def generate_component_and_atom_name(self, func_name: str, stmt: ast.stmt | None
logger.debug(f"[AST] Generated names {func_name=} {callsite_hint=} {component_id=} {atom_name=}")
return component_id, atom_name
- def _build_callsite_metadata(self, node: ast.AST, filename: str) -> dict:
+ def _build_callsite_metadata(self, node: ast.AST, filename: str, *, call_expr: ast.Call = None) -> dict:
"""
- Constructs callsite metadata (filename, lineno, source) for a given AST node.
+ Construct metadata describing the source location of an AST callsite.
+
+ This function extracts the filename, line number, and source code line associated
+ with the given AST node. If `call_expr` is provided, it also includes the full
+ unparsed source string of the lifted expression, which can be used for fallback
+ reconstruction or debugging.
+
+ Args:
+ node: The AST node for which to generate location metadata.
+ filename: The absolute or relative path to the source file.
+ call_expr: Optional `ast.Call` expression to include full source reconstruction.
Returns:
- A dict with keys: callsite_filename, callsite_lineno, callsite_source
+ A dictionary containing:
+ - "file": The source filename.
+ - "lineno": The line number in the source file (1-based).
+ - "src": The stripped source line at the given location, if available.
+ - "hint": A string in the form "filename:lineno" for human readable logging.
+ - "lifted_component_src" (optional): The full unparsed source string of the call expression.
"""
lineno = getattr(node, "lineno", None)
source = ""
@@ -2724,13 +2771,18 @@ def _build_callsite_metadata(self, node: ast.AST, filename: str) -> dict:
if 0 < lineno <= len(self._source_lines):
source = self._source_lines[lineno - 1].strip()
- return {
- "callsite_filename": filename,
- "callsite_lineno": lineno,
- "callsite_source": source,
- "callsite_hint": f"{filename}:{lineno}" if filename and lineno else None,
+ callsite_metadata = {
+ "file": filename,
+ "lineno": lineno,
+ "src": source, # human readable debugging hint
+ "hint": f"{filename}:{lineno}" if filename and lineno else None,
}
+ if call_expr is not None and isinstance(call_expr, ast.Call):
+ callsite_metadata['lifted_component_src'] = ast.unparse(call_expr).strip()
+
+ return callsite_metadata
+
def annotate_parents(tree: ast.AST) -> ast.AST:
"""
diff --git a/preswald/engine/transformers/transformer_utils.py b/preswald/engine/transformers/transformer_utils.py
new file mode 100644
index 00000000..b4859ef5
--- /dev/null
+++ b/preswald/engine/transformers/transformer_utils.py
@@ -0,0 +1,194 @@
+import ast
+import logging
+import collections.abc
+from typing import Any
+
+from preswald.interfaces.component_return import ComponentReturn
+from preswald.utils import (
+ generate_stable_id,
+)
+
+
+logger = logging.getLogger(__name__)
+
+
+def get_call_func_name(call_node: ast.Call) -> str | None:
+ """
+ Extract the function name from an AST call node.
+
+ This utility handles both direct function calls like `slider(...)`
+ and attribute based calls like `preswald.slider(...)`.
+
+ Args:
+ call_node: An `ast.Call` node representing a function invocation.
+
+ Returns:
+ The name of the function being called, or None if it cannot be
+ determined.
+ """
+ if isinstance(call_node.func, ast.Name):
+ return call_node.func.id
+ elif isinstance(call_node.func, ast.Attribute):
+ return call_node.func.attr
+ return None
+
+def extract_call_args(call_node: ast.Call) -> tuple[list[Any], dict[str, Any]]:
+ """
+ Extract positional and keyword arguments from an AST call node.
+
+ This function attempts to statically extract values from `ast.Call` nodes
+ for common literal types like constants and f-strings. Non literal values
+ are returned as AST dumps for diagnostic or fallback purposes.
+
+ Args:
+ call_node: An `ast.Call` node representing a function invocation.
+
+ Returns:
+ A tuple containing:
+ - A list of extracted positional arguments.
+ - A dictionary of extracted keyword arguments.
+ """
+ args = []
+ kwargs = {}
+ for arg in call_node.args:
+ if isinstance(arg, ast.Constant):
+ args.append(arg.value)
+ elif isinstance(arg, ast.JoinedStr):
+ # fallback to unparsed string representation
+ args.append(ast.unparse(arg).strip())
+ else:
+ args.append(ast.dump(arg)) # or handle other node types more gracefully
+
+ for kw in call_node.keywords:
+ if isinstance(kw.value, ast.Constant):
+ kwargs[kw.arg] = kw.value.value
+ elif isinstance(kw.value, ast.JoinedStr):
+ kwargs[kw.arg] = ast.unparse(kw.value).strip()
+ else:
+ kwargs[kw.arg] = ast.dump(kw.value)
+
+ return args, kwargs
+
+
+def build_component_from_args(name: str, args: list, kwargs: dict) -> ComponentReturn:
+ """
+ Reconstruct a Preswald component by invoking its registered constructor.
+
+ This function is used to rebuild a component from a known function name,
+ positional arguments, and keyword arguments. It is typically called during
+ runtime error recovery or fallback rendering when the original component
+ failed to compute.
+
+ Only components defined in `preswald.interfaces.components` and marked with
+ the `_preswald_component_type` attribute are considered valid. The function
+ must return a `ComponentReturn`, which encapsulates both the visible value
+ and the component metadata.
+
+ If reconstruction fails at any point, the function is not a valid component,
+ or the return type is incorrect, a fallback error component is returned instead.
+
+ Args:
+ name: Name of the component function
+ args: Positional arguments for the component constructor
+ kwargs: Keyword arguments for the component constructor
+
+ Returns:
+ ComponentReturn: A fully constructed component if successful,
+ or a fallback error component otherwise.
+ """
+ from preswald.interfaces import components
+
+ try:
+ fn = getattr(components, name, None)
+ if fn is None or not callable(fn):
+ raise ValueError(f"No component function found with name '{name}'")
+
+ _preswald_component_type = getattr(fn, "_preswald_component_type", None)
+ if _preswald_component_type is None:
+ raise ValueError(f"Name matched function that is not a preswald component type: '{name=}'")
+
+ result = fn(*args, **kwargs)
+
+ if not isinstance(result, ComponentReturn):
+ raise ValueError(f"The Result of named function is not a ComponentReturn type: '{name=}'")
+
+ return result
+
+ except Exception as e:
+ component_id = kwargs.get('component_id', None)
+ if not component_id:
+ component_id = generate_stable_id(prefix=name, callsite_hint=callsite_hint)
+ component = {
+ "id": component_id,
+ "type": name,
+ "error": f"[Rebuild Error] {e!s}"
+ }
+ component.update(_filter_kwargs_for_fallback(kwargs))
+ return ComponentReturn(None, component)
+
+
+def rebuild_component_from_source(
+ lifted_component_src: str,
+ callsite_hint: str,
+ *,
+ force_render: bool=False
+) -> ComponentReturn:
+ """
+ Reconstruct a Preswald component from its lifted AST call expression.
+
+ This function is primarily used during runtime error recovery to reconstruct
+ a component from source code that was previously extracted and stored from the
+ original callsite. It parses the expression, extracts its arguments, and invokes
+ the appropriate Preswald component function.
+
+ The reconstructed component is wrapped in a `ComponentReturn` and can be safely
+ sent to the frontend, even when the original component computation failed.
+
+ Render tracking is temporarily suppressed during reconstruction to avoid duplicate
+ layout registration or state interference.
+
+ Args:
+ lifted_component_src: Source code string of the original component call expression.
+ callsite_hint: A 'filename:lineno' style hint used to generate a stable component ID.
+
+ Keyword Args:
+ force_render: If True, the returned component will include `'shouldRender': True`.
+
+ Returns:
+ ComponentReturn: The reconstructed component wrapped in a `ComponentReturn` object.
+
+ Raises:
+ ValueError: If the source is not a valid single call expression.
+ Exception: If component reconstruction fails for any reason.
+ """
+ expr_module = ast.parse(lifted_component_src, mode="exec")
+ if (
+ len(expr_module.body) == 1 and
+ isinstance(expr_module.body[0], ast.Expr) and
+ isinstance(expr_module.body[0].value, ast.Call)
+ ):
+ expr_ast = expr_module.body[0].value
+ name = get_call_func_name(expr_ast)
+ args, kwargs = extract_call_args(expr_ast)
+
+ kwargs["component_id"] = generate_stable_id(prefix=name, callsite_hint=callsite_hint)
+
+ from preswald.engine.service import PreswaldService
+
+ service = PreswaldService.get_instance()
+ with service.render_tracking_suppressed():
+ component_return = build_component_from_args(name, args, kwargs)
+ if force_render:
+ component_return.component['shouldRender'] = True
+
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.debug(f"[transform_utils.rebuild_component_from_source] Reconstructed component: {component_return=}")
+ return component_return
+
+ raise ValueError("Invalid lifted_component_src: unable to parse call expression.")
+
+def _safe_for_fallback(value):
+ return isinstance(value, (str, int, float, bool, type(None)))
+
+def _filter_kwargs_for_fallback(kwargs):
+ return {k: v for k, v in kwargs.items() if isinstance(k, str) and _safe_for_fallback(v)}
diff --git a/preswald/interfaces/component_return.py b/preswald/interfaces/component_return.py
index 36bbafa5..a7a99f3e 100644
--- a/preswald/interfaces/component_return.py
+++ b/preswald/interfaces/component_return.py
@@ -8,6 +8,10 @@ def __init__(self, value, component):
self.value = value
self._preswald_component = component
+ @property
+ def component(self) -> dict:
+ return self._preswald_component
+
def __str__(self): return str(self.value)
def __float__(self): return float(self.value)
def __bool__(self): return bool(self.value)
diff --git a/preswald/interfaces/components.py b/preswald/interfaces/components.py
index f0338faf..a000bf6b 100644
--- a/preswald/interfaces/components.py
+++ b/preswald/interfaces/components.py
@@ -378,7 +378,8 @@ def json_viewer(
parsed_data = data
serializable_data = convert_to_serializable(parsed_data)
except Exception as e:
- serializable_data = {"error": f"Invalid JSON: {e!s}"}
+ serializable_data = {"error": "Invalid JSON"}
+ raise
component = {
"type": "json_viewer",
@@ -401,6 +402,7 @@ def matplotlib(fig: plt.Figure | None = None, label: str = "plot", component_id:
ax.plot([0, 1, 2], [0, 1, 4])
# Save the figure as a base64-encoded PNG
+
buf = io.BytesIO()
fig.savefig(buf, format="png")
buf.seek(0)
@@ -413,9 +415,7 @@ def matplotlib(fig: plt.Figure | None = None, label: str = "plot", component_id:
"image": img_b64, # Store the image data
}
- return ComponentReturn(
- component_id, component
- ) # Returning ID for potential tracking
+ return ComponentReturn(component_id, component)
@with_render_tracking("playground")
diff --git a/preswald/interfaces/workflow.py b/preswald/interfaces/workflow.py
index 5eed74e0..142ea684 100644
--- a/preswald/interfaces/workflow.py
+++ b/preswald/interfaces/workflow.py
@@ -18,6 +18,7 @@
from preswald.interfaces.component_return import ComponentReturn
from preswald.interfaces.tracked_value import TrackedValue
from preswald.interfaces.render.error_registry import register_error
+from preswald.engine.transformers.transformer_utils import rebuild_component_from_source
logger = logging.getLogger(__name__)
@@ -163,44 +164,14 @@ def wrapped_func(*args, **kwargs):
f"Atom {self.name} failed with error: {e!s}", exc_info=True
)
- callsite_filename = self.callsite_metadata.get('callsite_filename')
- callsite_lineno = self.callsite_metadata.get('callsite_lineno')
- callsite_source = self.callsite_metadata.get('callsite_source')
- # if callsite info was not provided, attempt to
- # capture this info where the atom is defined
- if not callsite_filename or not callsite_lineno:
- callsite_filename, callsite_lineno = get_user_code_callsite(e)
- self.callsite_metadata['callsite_filename'] = callsite_filename
- self.callsite_metadata['callsite_lineno'] = callsite_lineno
-
-
- # if callsite source was not provided, attempt to
- # capture this info where the atom was defined
- #
- # TODO(preswald): Centralize source line buffering per filename in the service layer.
- # This would allow both the AST transformer and Atom class to fetch source snippets
- # without reopening the file. Until then, skip this fallback to avoid redundant I/O.
- #
- # if not callsite_source and callsite_filename:
- # try:
- # with open(callsite_filename, 'r') as f:
- # lines = f.readlines()
- # lineno = callsite_lineno or 0
- # callsite_source = lines[lineno - 1].strip() if 0 < lineno <= len(lines) else ""
- # self.callsite_metadata['callsite_source'] = callsite_source
-
- # except Exception:
- # pass
-
register_error(
type="runtime",
- filename=callsite_filename or "",
- lineno=callsite_lineno or 0,
- source=callsite_source or "",
+ filename=self.callsite_metadata.get("file", ""),
+ lineno=self.callsite_metadata.get("lineno", 0),
+ source=self.callsite_metadata.get("src", ""),
message=str(e),
atom_name=self.name,
)
-
raise
finally:
end_time = time.time()
@@ -313,6 +284,7 @@ def decorator(func):
force_recompute=force_recompute,
callsite_metadata=callsite_metadata or {},
)
+
self.atoms[atom_name] = atom
logger.info(f"[DAG] Atom registration complete {atom_name=} -> {atom_deps=}")
@@ -344,12 +316,19 @@ def execute(
logger.info(f"[DAG] Atoms to recompute {atoms_to_recompute=}")
+ failed_atoms: set[str] = set()
+
for atom_name in execution_order:
if self._is_rerun and recompute_atoms and atom_name not in atoms_to_recompute:
logger.info(f"[DAG] Skipping atom (not affected) {atom_name=}")
continue
+ # Skip if any dependency failed
atom = self.atoms[atom_name]
+ if any(dep in failed_atoms for dep in atom.dependencies):
+ logger.warning(f"[DAG] Skipping atom due to failed dependency {atom_name=}")
+ continue
+
if atom_name in atoms_to_recompute:
atom.force_recompute = True
@@ -358,8 +337,8 @@ def execute(
atom.force_recompute = False
if result.status == AtomStatus.FAILED:
- logger.error(f"[DAG] Execution halted due to failure {atom_name=}")
- break
+ logger.error(f"[DAG] Atom failed during execution {atom_name=}")
+ failed_atoms.add(atom_name)
return self.context.results
finally:
@@ -396,6 +375,15 @@ def register_component_producer(self, component_id: str, atom_name: str):
else:
logger.warning(f"[DAG] Skipping producer registration for unknown atom {atom_name=}")
+ def _register_component_producer(self, atom: Atom, candidate: Any) -> None:
+ if self._service:
+ if isinstance(candidate, ComponentReturn):
+ self.register_component_producer(candidate.component['id'], atom.name)
+ elif isinstance(candidate, tuple):
+ for item in candidate:
+ if isinstance(item, ComponentReturn):
+ self.register_component_producer(item.component['id'], atom.name)
+
def _get_affected_atoms(self, changed_atoms: set[str]) -> set[str]:
"""
Computes the full set of atoms that must be recomputed when a given set of atoms change.
@@ -568,15 +556,7 @@ def _execute_atom_inner(self, atom: Atom, dependency_values: dict[str, Any], inp
result = atom.func(*args)
- if self._service:
- if isinstance(result, ComponentReturn):
- logger.info('[DEBUG] - register_component_producer from workflow _execute_inner')
- self.register_component_producer(result.component_id, atom.name)
- elif isinstance(result, tuple):
- for item in result:
- if isinstance(item, ComponentReturn):
- logger.info('[DEBUG] - register_component_producer from workflow _execute_inner. result is tuple.')
- self.register_component_producer(item.component_id, atom.name)
+ self._register_component_producer(atom, result)
end_time = time.time()
atom_result = AtomResult(
@@ -597,9 +577,27 @@ def _execute_atom_inner(self, atom: Atom, dependency_values: dict[str, Any], inp
logger.warning(f"[DAG] Atom execution failed, retrying {atom.name=} {attempts=} delay={delay:.2f}")
time.sleep(delay)
else:
+ component = None
+ lifted_component_src = atom.callsite_metadata.get("lifted_component_src")
+ if lifted_component_src:
+ try:
+ callsite_hint = atom.callsite_metadata.get("hint")
+ component_return = rebuild_component_from_source(lifted_component_src, callsite_hint, force_render=True)
+ component_return.component['error'] = str(e)
+ self._register_component_producer(atom, component_return)
+ component = component_return.component
+ except Exception as rebuild_error:
+ logger.error(f"[DAG] Failed to rebuild component for {atom.name}: {rebuild_error=}")
+
+ component = {
+ "type": "unkown",
+ "error": f"[Runtime Error] {str(e)}"
+ }
+
return AtomResult(
status=AtomStatus.FAILED,
error=e,
+ value=component,
attempts=attempts,
start_time=start_time,
end_time=current_time,