1616from pydantic import BaseModel
1717from uipath .core .tracing .context import UiPathTraceContext
1818
19- from uipath .runtime .result import UiPathRuntimeResult
19+ from uipath .runtime .errors import (
20+ UiPathErrorCategory ,
21+ UiPathErrorCode ,
22+ UiPathErrorContract ,
23+ UiPathRuntimeError ,
24+ )
25+ from uipath .runtime .logging ._interceptor import UiPathRuntimeLogsInterceptor
26+ from uipath .runtime .result import UiPathRuntimeResult , UiPathRuntimeStatus
27+
28+ logger = logging .getLogger (__name__ )
2029
2130C = TypeVar ("C" , bound = "UiPathRuntimeContext" )
2231
@@ -28,7 +37,6 @@ class UiPathRuntimeContext(BaseModel):
2837 input : Optional [Any ] = None
2938 resume : bool = False
3039 job_id : Optional [str ] = None
31- execution_id : Optional [str ] = None
3240 trace_context : Optional [UiPathTraceContext ] = None
3341 config_path : str = "uipath.json"
3442 runtime_dir : Optional [str ] = "__uipath"
@@ -38,13 +46,148 @@ class UiPathRuntimeContext(BaseModel):
3846 output_file : Optional [str ] = None
3947 trace_file : Optional [str ] = None
4048 logs_file : Optional [str ] = "execution.log"
41- log_handler : Optional [logging .Handler ] = None
4249 logs_min_level : Optional [str ] = "INFO"
4350 breakpoints : Optional [List [str ] | Literal ["*" ]] = None
4451 result : Optional [UiPathRuntimeResult ] = None
4552
4653 model_config = {"arbitrary_types_allowed" : True , "extra" : "allow" }
4754
55+ def __enter__ (self ):
56+ """Async enter method called when entering the 'async with' block.
57+
58+ Initializes and prepares the runtime contextual environment.
59+
60+ Returns:
61+ The runtime context instance
62+ """
63+ # Read the input from file if provided
64+ if self .input_file :
65+ _ , file_extension = os .path .splitext (self .input_file )
66+ if file_extension != ".json" :
67+ raise UiPathRuntimeError (
68+ code = UiPathErrorCode .INVALID_INPUT_FILE_EXTENSION ,
69+ title = "Invalid Input File Extension" ,
70+ detail = "The provided input file must be in JSON format." ,
71+ )
72+ with open (self .input_file ) as f :
73+ self .input = f .read ()
74+
75+ try :
76+ if isinstance (self .input , str ):
77+ if self .input .strip ():
78+ self .input = json .loads (self .input )
79+ else :
80+ self .input = {}
81+ elif self .input is None :
82+ self .input = {}
83+ # else: leave it as-is (already a dict, list, bool, etc.)
84+ except json .JSONDecodeError as e :
85+ raise UiPathRuntimeError (
86+ UiPathErrorCode .INPUT_INVALID_JSON ,
87+ "Invalid JSON input" ,
88+ f"The input data is not valid JSON: { str (e )} " ,
89+ UiPathErrorCategory .USER ,
90+ ) from e
91+
92+ # Intercept all stdout/stderr/logs
93+ # Write to file (runtime), stdout (debug) or log handler (if provided)
94+ self .logs_interceptor = UiPathRuntimeLogsInterceptor (
95+ min_level = self .logs_min_level ,
96+ dir = self .runtime_dir ,
97+ file = self .logs_file ,
98+ job_id = self .job_id ,
99+ )
100+ self .logs_interceptor .setup ()
101+
102+ return self
103+
104+ def __exit__ (self , exc_type , exc_val , exc_tb ):
105+ """Async exit method called when exiting the 'async with' block.
106+
107+ Cleans up resources and handles any exceptions.
108+
109+ Always writes output file regardless of whether execution was successful,
110+ suspended, or encountered an error.
111+ """
112+ try :
113+ if self .result is None :
114+ execution_result = UiPathRuntimeResult ()
115+ else :
116+ execution_result = self .result
117+
118+ if exc_type :
119+ # Create error info from exception
120+ if isinstance (exc_val , UiPathRuntimeError ):
121+ error_info = exc_val .error_info
122+ else :
123+ # Generic error
124+ error_info = UiPathErrorContract (
125+ code = f"ERROR_{ exc_type .__name__ } " ,
126+ title = f"Runtime error: { exc_type .__name__ } " ,
127+ detail = str (exc_val ),
128+ category = UiPathErrorCategory .UNKNOWN ,
129+ )
130+
131+ execution_result .status = UiPathRuntimeStatus .FAULTED
132+ execution_result .error = error_info
133+
134+ content = execution_result .to_dict ()
135+
136+ # Always write output file at runtime, except for inner runtimes
137+ # Inner runtimes have execution_id
138+ if self .job_id :
139+ with open (self .result_file_path , "w" ) as f :
140+ json .dump (content , f , indent = 2 , default = str )
141+
142+ # Write the execution output to file if requested
143+ if self .output_file :
144+ with open (self .output_file , "w" ) as f :
145+ f .write (content .get ("output" , "{}" ))
146+
147+ # Don't suppress exceptions
148+ return False
149+
150+ except Exception as e :
151+ logger .error (f"Error during runtime shutdown: { str (e )} " )
152+
153+ # Create a fallback error result if we fail during cleanup
154+ if not isinstance (e , UiPathRuntimeError ):
155+ error_info = UiPathErrorContract (
156+ code = "RUNTIME_SHUTDOWN_ERROR" ,
157+ title = "Runtime shutdown failed" ,
158+ detail = f"Error: { str (e )} " ,
159+ category = UiPathErrorCategory .SYSTEM ,
160+ )
161+ else :
162+ error_info = e .error_info
163+
164+ # Last-ditch effort to write error output
165+ try :
166+ error_result = UiPathRuntimeResult (
167+ status = UiPathRuntimeStatus .FAULTED , error = error_info
168+ )
169+ error_result_content = error_result .to_dict ()
170+ if self .job_id :
171+ with open (self .result_file_path , "w" ) as f :
172+ json .dump (error_result_content , f , indent = 2 , default = str )
173+ except Exception as write_error :
174+ logger .error (f"Failed to write error output file: { str (write_error )} " )
175+ raise
176+
177+ # Re-raise as RuntimeError if it's not already a UiPathRuntimeError
178+ if not isinstance (e , UiPathRuntimeError ):
179+ raise RuntimeError (
180+ error_info .code ,
181+ error_info .title ,
182+ error_info .detail ,
183+ error_info .category ,
184+ ) from e
185+ raise
186+ finally :
187+ # Restore original logging
188+ if hasattr (self , "logs_interceptor" ):
189+ self .logs_interceptor .teardown ()
190+
48191 @cached_property
49192 def result_file_path (self ) -> str :
50193 """Get the full path to the result file."""
0 commit comments