44import os
55import re
66import shutil
7- from typing import AsyncGenerator
7+ from typing import AsyncGenerator , Optional
88
99from google .adk .agents import BaseAgent
1010from google .adk .agents .invocation_context import InvocationContext
@@ -26,6 +26,8 @@ class AutonomousPipelineAgent(BaseAgent):
2626 autotune_agent : BaseAgent
2727 profile_agent : BaseAgent
2828 max_iterations : int = 2
29+ session_dir : Optional [str ] = None
30+ end_agent : Optional [str ] = None
2931
3032 def __init__ (
3133 self ,
@@ -38,6 +40,8 @@ def __init__(
3840 autotune_agent : BaseAgent ,
3941 profile_agent : BaseAgent ,
4042 max_iterations : int = 2 ,
43+ session_dir : Optional [str ] = None ,
44+ end_agent : Optional [str ] = None ,
4145 ):
4246 super ().__init__ (
4347 name = name ,
@@ -49,6 +53,8 @@ def __init__(
4953 autotune_agent = autotune_agent ,
5054 profile_agent = profile_agent ,
5155 max_iterations = max_iterations ,
56+ session_dir = session_dir ,
57+ end_agent = end_agent ,
5258 )
5359
5460 async def _run_async_impl (
@@ -68,10 +74,19 @@ async def _run_async_impl(
6874 async for event in self .plan_agent .run_async (ctx ):
6975 yield event
7076
77+ self ._clear_iteration_metrics (ctx )
78+
79+ if self ._should_end_at_step (ctx , iteration , "plan" ):
80+ iteration += 1
81+ continue
82+
7183 # Step 2: Implement
7284 logging .info (f"[{ self .name } ] Running ImplementKernelAgent..." )
7385 async for event in self .implement_agent .run_async (ctx ):
7486 yield event
87+ if self ._should_end_at_step (ctx , iteration , "implement" ):
88+ iteration += 1
89+ continue
7590
7691 # Step 3: Validate
7792 logging .info (f"[{ self .name } ] Running ValidateKernelCompilationAgent..." )
@@ -86,12 +101,16 @@ async def _run_async_impl(
86101 logging .error (
87102 f"[{ self .name } ] Compilation failed. Looping back to planning."
88103 )
89- self ._save_iteration_files (
90- ctx , iteration , keys_to_save = [ "optimized_kernel_path" ]
104+ self ._save_iteration_files_and_snapshot (
105+ ctx , iteration , step_name = "validate"
91106 )
92107 iteration += 1
93108 continue
94109
110+ if self ._should_end_at_step (ctx , iteration , "validate" ):
111+ iteration += 1
112+ continue
113+
95114 # Step 4: Test Gen
96115 logging .info (f"[{ self .name } ] Running ValidatedTestGenerationAgent..." )
97116 async for event in self .test_gen_agent .run_async (ctx ):
@@ -103,14 +122,16 @@ async def _run_async_impl(
103122 logging .error (
104123 f"[{ self .name } ] Test generation/validation failed. Looping back to planning."
105124 )
106- self ._save_iteration_files (
107- ctx ,
108- iteration ,
109- keys_to_save = ["optimized_kernel_path" , "test_file_path" ],
125+ self ._save_iteration_files_and_snapshot (
126+ ctx , iteration , step_name = "test_gen"
110127 )
111128 iteration += 1
112129 continue
113130
131+ if self ._should_end_at_step (ctx , iteration , "test_gen" ):
132+ iteration += 1
133+ continue
134+
114135 # Step 5: Test Run
115136 logging .info (f"[{ self .name } ] Running UnifiedTestAgent..." )
116137 async for event in self .test_run_agent .run_async (ctx ):
@@ -120,62 +141,40 @@ async def _run_async_impl(
120141 test_results = ctx .session .state .get ("test_results" , {})
121142 if not test_results .get ("success" , False ):
122143 logging .error (f"[{ self .name } ] Tests failed. Looping back to planning." )
123- self ._save_iteration_files (
124- ctx ,
125- iteration ,
126- keys_to_save = ["optimized_kernel_path" , "test_file_path" ],
144+ self ._save_iteration_files_and_snapshot (
145+ ctx , iteration , step_name = "test_run"
127146 )
128147 iteration += 1
129148 continue
130149
150+ if self ._should_end_at_step (ctx , iteration , "test_run" ):
151+ iteration += 1
152+ continue
153+
131154 # Step 6: Autotune
132155 logging .info (f"[{ self .name } ] Running AutotuneAgent..." )
133156 async for event in self .autotune_agent .run_async (ctx ):
134157 yield event
158+ if self ._should_end_at_step (ctx , iteration , "autotune" ):
159+ iteration += 1
160+ continue
135161
136162 # Step 7: Profile
137163 logging .info (f"[{ self .name } ] Running ProfileAgentOrchestrator..." )
138164 async for event in self .profile_agent .run_async (ctx ):
139165 yield event
166+ if self ._should_end_at_step (ctx , iteration , "profile" ):
167+ iteration += 1
168+ continue
140169
141- # Snapshot intermediate result
142- kernel_path = ctx .session .state .get ("optimized_kernel_path" )
143- kernel_code = ""
144- if kernel_path and os .path .exists (kernel_path ):
145- try :
146- with open (kernel_path , "r" ) as f :
147- kernel_code = f .read ()
148- except Exception as e :
149- logging .error (
150- f"[{ self .name } ] Failed to read kernel file for snapshot: { e } "
151- )
152-
153- # Extract latency
154- latency = self ._extract_latency (ctx )
155-
156- snapshot = {
157- "iteration" : iteration ,
158- "kernel_code" : kernel_code ,
159- "compilation_status" : ctx .session .state .get (
160- "kernel_compilation_status" , {}
161- ),
162- "test_status" : ctx .session .state .get ("test_results" , {}),
163- "latency_ms" : latency ,
164- "profiling_summary" : ctx .session .state .get ("profiling_summary" , "" ),
165- }
166- current_history = ctx .session .state .get ("history" , [])
167- updated_history = current_history + [snapshot ]
168- ctx .session .state ["history" ] = (
169- updated_history # Ensure local consistency within the loop
170- )
170+ self ._save_iteration_files_and_snapshot (ctx , iteration )
171171
172172 yield Event (
173173 author = self .name ,
174- actions = EventActions (state_delta = {"history" : updated_history }),
174+ actions = EventActions (
175+ state_delta = {"history" : ctx .session .state .get ("history" , [])}
176+ ),
175177 )
176- logging .info (f"[{ self .name } ] Saved snapshot for iteration { iteration } " )
177-
178- self ._save_iteration_files (ctx , iteration )
179178
180179 # Step 7: Check if improvement is needed
181180 # needs_improvement = ctx.session.state.get("needs_improvement", False)
@@ -209,20 +208,83 @@ async def _run_async_impl(
209208 ),
210209 )
211210
212- def _save_iteration_files (
211+ def _should_end_at_step (
212+ self , ctx : InvocationContext , iteration : int , step_name : str
213+ ) -> bool :
214+ """Checks if the pipeline should terminate at the current step."""
215+ if self .end_agent == step_name :
216+ logging .info (
217+ f"[{ self .name } ] Ending iteration { iteration } early at step '{ step_name } '."
218+ )
219+ self ._save_iteration_files_and_snapshot (
220+ ctx , iteration , step_name = step_name
221+ )
222+ return True
223+ return False
224+
225+ def _record_history_snapshot (self , ctx : InvocationContext , iteration : int ):
226+ """Records the iteration snapshot into the session history."""
227+ current_history = ctx .session .state .get ("history" , [])
228+ if any (s .get ("iteration" ) == iteration for s in current_history ):
229+ return
230+
231+ kernel_path = ctx .session .state .get ("optimized_kernel_path" )
232+ kernel_code = ""
233+ if kernel_path and os .path .exists (kernel_path ):
234+ try :
235+ with open (kernel_path , "r" ) as f :
236+ kernel_code = f .read ()
237+ except Exception as e :
238+ logging .error (
239+ f"[{ self .name } ] Failed to read kernel file for snapshot: { e } "
240+ )
241+
242+ latency = self ._extract_latency (ctx )
243+
244+ snapshot = {
245+ "iteration" : iteration ,
246+ "kernel_code" : kernel_code ,
247+ "compilation_status" : ctx .session .state .get (
248+ "kernel_compilation_status" , {}
249+ ),
250+ "test_status" : ctx .session .state .get ("test_results" , {}),
251+ "latency_ms" : latency ,
252+ "profiling_summary" : ctx .session .state .get ("profiling_summary" , "" ),
253+ }
254+
255+ updated_history = current_history + [snapshot ]
256+ ctx .session .state ["history" ] = updated_history
257+ logging .info (f"[{ self .name } ] Saved snapshot for iteration { iteration } " )
258+
259+ def _save_iteration_files_and_snapshot (
213260 self ,
214261 ctx : InvocationContext ,
215262 iteration : int ,
216- keys_to_save : list [str ] | None = None ,
263+ step_name : Optional [str ] = None ,
217264 ):
218265 """Saves artifacts with an iteration suffix."""
219- if keys_to_save is None :
220- keys_to_save = [
221- "optimized_kernel_path" ,
222- "test_file_path" ,
223- "autotune_specs_path" ,
224- "autotune_results_path" ,
225- ]
266+ all_keys = [
267+ "kernel_plan_path" ,
268+ "optimized_kernel_path" ,
269+ "test_file_path" ,
270+ "autotune_specs_path" ,
271+ "autotune_results_path" ,
272+ ]
273+ if step_name is not None :
274+ step_to_last_key = {
275+ "plan" : "kernel_plan_path" ,
276+ "implement" : "optimized_kernel_path" ,
277+ "validate" : "optimized_kernel_path" ,
278+ "test_gen" : "test_file_path" ,
279+ "test_run" : "test_file_path" ,
280+ "autotune" : "autotune_results_path" ,
281+ "profile" : "autotune_results_path" ,
282+ }
283+ last_key = step_to_last_key .get (step_name , "autotune_results_path" )
284+ keys_to_save = all_keys [: all_keys .index (last_key ) + 1 ]
285+ else :
286+ keys_to_save = all_keys
287+
226288 for path_key in keys_to_save :
227289 path = ctx .session .state .get (path_key )
228290 if path and os .path .exists (path ):
@@ -238,14 +300,36 @@ def _save_iteration_files(
238300 f"[{ self .name } ] Failed to copy { path_key } to { new_path } : { e } "
239301 )
240302
303+ self ._record_history_snapshot (ctx , iteration )
304+
305+ def _clear_iteration_metrics (self , ctx : InvocationContext ):
306+ """Clears iteration-specific metrics to avoid carrying over stale data."""
307+ for key in [
308+ "kernel_compilation_status" ,
309+ "validation_loop_status" ,
310+ "test_results" ,
311+ "autotune_results" ,
312+ "profiling_summary" ,
313+ ]:
314+ ctx .session .state .pop (key , None )
315+
241316 def _initialize_state (self , ctx : InvocationContext ) -> Event :
242317 """Initializes session state with standard paths and returns the event."""
243318 # Initialize history
244319 if "history" not in ctx .session .state :
245320 ctx .session .state ["history" ] = []
246321
247322 # Path related states
248- session_dir = os .path .join (WORKDIR , ctx .session .id )
323+ session_dir = self .session_dir or os .path .join (WORKDIR , ctx .session .id )
324+
325+ # Ensure session_dir is under WORKDIR
326+ abs_session_dir = os .path .abspath (session_dir )
327+ abs_workdir = os .path .abspath (WORKDIR )
328+ if os .path .commonpath ([abs_workdir , abs_session_dir ]) != abs_workdir :
329+ raise ValueError (
330+ f"session_dir ({ session_dir } ) must be a subdirectory of WORKDIR ({ WORKDIR } )"
331+ )
332+
249333 os .makedirs (session_dir , exist_ok = True )
250334
251335 if "workdir" not in ctx .session .state :
@@ -406,34 +490,32 @@ async def _apply_best_solution(self, ctx: InvocationContext):
406490 f"[{ self .name } ] Best solution found from iteration { best_solution ['iteration' ]} "
407491 )
408492
409- # Rollback if needed
410- current_code = ""
411- kernel_path = ctx .session .state .get ("optimized_kernel_path" )
412- if kernel_path and os .path .exists (kernel_path ):
413- try :
414- with open (kernel_path , "r" ) as f :
415- current_code = f .read ()
416- except Exception as e :
417- logging .error (
418- f"[{ self .name } ] Failed to read current kernel file: { e } "
419- )
493+ # Rollback all relevant files to the best iteration's state
494+ best_iter = best_solution ["iteration" ]
495+ keys_to_rollback = [
496+ "optimized_kernel_path" ,
497+ "kernel_plan_path" ,
498+ "test_file_path" ,
499+ "autotune_specs_path" ,
500+ "autotune_results_path" ,
501+ ]
420502
421- if best_solution [ "kernel_code" ] != current_code :
422- logging . info (
423- f"[ { self . name } ] Reverting kernel file to best solution from iteration { best_solution [ 'iteration' ] } "
424- )
425- if kernel_path :
426- try :
427- with open ( kernel_path , "w" ) as f :
428- f . write ( best_solution [ "kernel_code" ] )
429- except Exception as e :
430- logging . error (
431- f"[ { self . name } ] Failed to write best solution to file: { e } "
432- )
433- else :
434- logging . info (
435- f"[ { self . name } ] Current file is already the best solution."
436- )
503+ logging . info (
504+ f"[ { self . name } ] Restoring files to match best solution from iteration { best_iter } "
505+ )
506+
507+ for path_key in keys_to_rollback :
508+ base_path = ctx . session . state . get ( path_key )
509+ if base_path :
510+ directory , filename = os . path . split ( base_path )
511+ name , ext = os . path . splitext ( filename )
512+ suffixed_path = os . path . join ( directory , f" { name } _ { best_iter } { ext } " )
513+
514+ if os . path . exists ( suffixed_path ):
515+ try :
516+ shutil . copy2 ( suffixed_path , base_path )
517+ except Exception as e :
518+ logging . error ( f"[ { self . name } ] Failed to rollback { path_key } : { e } " )
437519
438520 return best_solution
439521
0 commit comments