1414 AiAutofixAgentHandoffEvent ,
1515 AiAutofixCodeChangesCompletedEvent ,
1616 AiAutofixCodeChangesStartedEvent ,
17+ AiAutofixIterationCompletedEvent ,
18+ AiAutofixIterationStartedEvent ,
1719 AiAutofixPhaseEvent ,
1820 AiAutofixPrCreatedStartedEvent ,
1921 AiAutofixRootCauseCompletedEvent ,
3234from sentry .seer .autofix .constants import AutofixReferrer
3335from sentry .seer .autofix .prompts import (
3436 code_changes_prompt ,
37+ pr_iteration_prompt ,
3538 root_cause_prompt ,
3639 solution_prompt ,
3740)
@@ -133,16 +136,29 @@ def __init__(
133136 started_event = AiAutofixCodeChangesStartedEvent ,
134137 completed_event = AiAutofixCodeChangesCompletedEvent ,
135138 ),
139+ AutofixStep .PR_ITERATION : StepConfig (
140+ artifact_schema = None , # Iteration changes read from file_patches
141+ prompt_fn = pr_iteration_prompt ,
142+ enable_coding = True ,
143+ started_event = AiAutofixIterationStartedEvent ,
144+ completed_event = AiAutofixIterationCompletedEvent ,
145+ ),
136146}
137147
138148
139- def build_step_prompt (step : AutofixStep , group : Group , user_context : str | None = None ) -> str :
149+ def build_step_prompt (
150+ step : AutofixStep ,
151+ group : Group ,
152+ user_context : str | None = None ,
153+ run_state : SeerRunState | None = None ,
154+ ) -> str :
140155 """
141156 Build the prompt for a step using issue details.
142157
143158 Args:
144159 step: The autofix step to build prompt for
145160 group: The Sentry group (issue) being analyzed
161+ run_state: The current run state, used to surface PR links for iteration
146162
147163 Returns:
148164 Formatted prompt string
@@ -153,6 +169,7 @@ def build_step_prompt(step: AutofixStep, group: Group, user_context: str | None
153169 title = group .title or "Unknown error" ,
154170 culprit = group .culprit or "unknown" ,
155171 artifact_key = step .value ,
172+ run_state = run_state ,
156173 )
157174
158175 parts = [prompt ]
@@ -189,6 +206,37 @@ def get_step_webhook_action_type(step: AutofixStep, is_completed: bool) -> SeerA
189206 return step_to_action_type [step ][is_completed ]
190207
191208
209+ def get_latest_iteration_index (state : SeerRunState ) -> int :
210+ for block in reversed (state .blocks ):
211+ metadata = block .message .metadata or {}
212+ if metadata .get ("step" ) == AutofixStep .PR_ITERATION .value :
213+ return int (metadata ["iteration_index" ])
214+ return 0
215+
216+
217+ def get_iteration_for_insert_index (state : SeerRunState , insert_index : int ) -> int :
218+ block = state .blocks [insert_index ]
219+ metadata = block .message .metadata or {}
220+ return int (metadata ["iteration_index" ])
221+
222+
223+ def recover_iteration_feedback (state : SeerRunState , insert_index : int ) -> str | None :
224+ """
225+ Recover the user feedback that originally triggered a PR iteration.
226+
227+ When a PR iteration is retried, the frontend truncates the run at the
228+ iteration's first block (``insert_index``). That block carries the original
229+ feedback in its metadata, so we reuse it to retry with the same feedback
230+ rather than dropping it.
231+ """
232+ if insert_index < 0 or insert_index >= len (state .blocks ):
233+ return None
234+ metadata = state .blocks [insert_index ].message .metadata
235+ if metadata is None :
236+ return None
237+ return metadata .get ("feedback" )
238+
239+
192240def get_autofix_agent_client (
193241 group : Group ,
194242 intelligence_level : Literal ["low" , "medium" , "high" ] = "medium" ,
@@ -297,15 +345,28 @@ def trigger_autofix_agent(
297345 else reasoning_effort
298346 )
299347
348+ pr_iteration_enabled = features .has ("organizations:autofix-pr-iteration" , group .organization )
349+
300350 client = get_autofix_agent_client (
301351 group ,
302352 intelligence_level = resolved_intelligence_level ,
303353 reasoning_effort = resolved_reasoning_effort ,
304354 enable_coding = config .enable_coding ,
305355 code_review_enabled = _code_review_enabled (group .organization , config .enable_coding ),
306356 )
357+ run_state : SeerRunState | None = None
307358 if run_id is not None :
308- _get_group_run_state (client , group , run_id )
359+ run_state = _get_group_run_state (client , group , run_id )
360+
361+ if run_state is not None and run_state .metadata :
362+ pr_iteration_enabled = run_state .metadata .get ("pr_iteration_enabled" , pr_iteration_enabled )
363+
364+ iteration_index : int | None = None
365+ if step == AutofixStep .PR_ITERATION and run_state is not None :
366+ if insert_index is not None :
367+ iteration_index = get_iteration_for_insert_index (run_state , insert_index )
368+ else :
369+ iteration_index = get_latest_iteration_index (run_state ) + 1
309370
310371 if config .started_event is not None :
311372 analytics .record (
@@ -314,21 +375,30 @@ def trigger_autofix_agent(
314375 project_id = group .project_id ,
315376 group_id = group .id ,
316377 referrer = referrer .value ,
378+ iteration_index = iteration_index ,
317379 )
318380 )
319381
320- prompt = build_step_prompt (step , group , user_context )
382+ prompt = build_step_prompt (step , group , user_context , run_state = run_state )
321383 prompt_metadata = {
322384 "step" : step .value ,
323385 "referrer" : referrer .value ,
324386 "has_user_context" : "no" if user_context is None else "yes" ,
325387 "is_retry" : "no" if insert_index is None else "yes" ,
326388 }
389+ if step == AutofixStep .PR_ITERATION and user_context is not None :
390+ prompt_metadata ["feedback" ] = user_context
391+ if iteration_index is not None :
392+ prompt_metadata ["iteration_index" ] = str (iteration_index )
327393 artifact_key = step .value if config .artifact_schema else None
328394 artifact_schema = config .artifact_schema
329395
330396 if run_id is None :
331- metadata = {"referrer" : referrer .value }
397+ metadata : dict [str , Any ] = {
398+ "group_id" : group .id ,
399+ "referrer" : referrer .value ,
400+ "pr_iteration_enabled" : pr_iteration_enabled , # value of the option since we're creating a new one
401+ }
332402 if stopping_point :
333403 metadata ["stopping_point" ] = stopping_point .value
334404 run_id = client .start_run (
@@ -353,10 +423,12 @@ def trigger_autofix_agent(
353423 insert_index = insert_index ,
354424 )
355425
356- payload = {
426+ payload : dict [ str , Any ] = {
357427 "run_id" : run_id ,
358428 "group_id" : group .id ,
359429 }
430+ if iteration_index is not None :
431+ payload ["iteration_index" ] = iteration_index
360432
361433 webhook_action_type = get_step_webhook_action_type (step , is_completed = False )
362434 event_name = webhook_action_type .value
@@ -398,7 +470,14 @@ def trigger_autofix_agent(
398470 },
399471 )
400472
401- metrics .incr ("autofix.explorer.trigger" , tags = {"step" : step .value , "referrer" : referrer .value })
473+ metrics .incr (
474+ "autofix.explorer.trigger" ,
475+ tags = {
476+ "step" : step .value ,
477+ "referrer" : referrer .value ,
478+ "iteration_index" : iteration_index ,
479+ },
480+ )
402481
403482 return run_id
404483
0 commit comments