Skip to content

Commit eed3d35

Browse files
joseph-sentryclaude
andcommitted
feat(autofix): Trigger PR iteration runs and surface PR links in prompts
Add the autofix-pr-iteration feature flag and wire the trigger path for the PR_ITERATION step. trigger_autofix_agent now persists the flag value in run metadata on creation, computes the iteration index (from the insert index on retry, otherwise the latest + 1), and includes it in the started webhook payload, prompt metadata, and trigger metric. build_step_prompt threads the run state into the prompt builders so pr_iteration_prompt can surface the open pull request links. Adds get_latest_iteration_index, get_iteration_for_insert_index, and recover_iteration_feedback helpers. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent 46d270f commit eed3d35

4 files changed

Lines changed: 299 additions & 10 deletions

File tree

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
281281
manager.add("organizations:seer-autofix-high-intelligence-high-reasoning", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
282282
# Expose the code review tool to autofix coding runs
283283
manager.add("organizations:seer-autofix-code-review", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
284+
# Enable the PR iteration feedback flow in the explorer autofix drawer
285+
manager.add("organizations:autofix-pr-iteration", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
284286
# Show Seer run ID in Slack notification footers
285287
manager.add("organizations:seer-run-id-in-slack", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
286288
# Gate display of Seer action events in the issue activity timeline

src/sentry/seer/autofix/autofix_agent.py

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
AiAutofixAgentHandoffEvent,
1515
AiAutofixCodeChangesCompletedEvent,
1616
AiAutofixCodeChangesStartedEvent,
17+
AiAutofixIterationCompletedEvent,
18+
AiAutofixIterationStartedEvent,
1719
AiAutofixPhaseEvent,
1820
AiAutofixPrCreatedStartedEvent,
1921
AiAutofixRootCauseCompletedEvent,
@@ -32,6 +34,7 @@
3234
from sentry.seer.autofix.constants import AutofixReferrer
3335
from 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+
192240
def 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

src/sentry/seer/autofix/prompts.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
33
"""
44

55
from textwrap import dedent
6+
from typing import TYPE_CHECKING
67

8+
if TYPE_CHECKING:
9+
from sentry.seer.agent.client_models import SeerRunState
710

8-
def root_cause_prompt(*, short_id: str, title: str, culprit: str, artifact_key: str | None) -> str:
11+
12+
def root_cause_prompt(
13+
*,
14+
short_id: str,
15+
title: str,
16+
culprit: str,
17+
artifact_key: str | None,
18+
run_state: "SeerRunState | None" = None,
19+
) -> str:
920
return dedent(
1021
f"""\
1122
Analyze issue {short_id}: "{title}" (culprit: {culprit})
@@ -30,7 +41,14 @@ def root_cause_prompt(*, short_id: str, title: str, culprit: str, artifact_key:
3041
)
3142

3243

33-
def solution_prompt(*, short_id: str, title: str, culprit: str, artifact_key: str | None) -> str:
44+
def solution_prompt(
45+
*,
46+
short_id: str,
47+
title: str,
48+
culprit: str,
49+
artifact_key: str | None,
50+
run_state: "SeerRunState | None" = None,
51+
) -> str:
3452
return dedent(
3553
f"""\
3654
Plan a solution for issue {short_id}: "{title}" (culprit: {culprit})
@@ -58,7 +76,12 @@ def solution_prompt(*, short_id: str, title: str, culprit: str, artifact_key: st
5876

5977

6078
def code_changes_prompt(
61-
*, short_id: str, title: str, culprit: str, artifact_key: str | None
79+
*,
80+
short_id: str,
81+
title: str,
82+
culprit: str,
83+
artifact_key: str | None,
84+
run_state: "SeerRunState | None" = None,
6285
) -> str:
6386
return dedent(
6487
f"""\
@@ -78,6 +101,55 @@ def code_changes_prompt(
78101
)
79102

80103

104+
def pr_iteration_prompt(
105+
*,
106+
short_id: str,
107+
title: str,
108+
culprit: str,
109+
artifact_key: str | None,
110+
run_state: "SeerRunState | None" = None,
111+
) -> str:
112+
prompt = dedent(
113+
f"""\
114+
Iterate on the pull request for issue {short_id}: "{title}" (culprit: {culprit})
115+
116+
Review the existing pull request, previous code changes, and any available feedback. Update the codebase to address the requested revisions while preserving the original fix.
117+
118+
Steps:
119+
1. Inspect the existing pull request and prior file patches
120+
2. Identify the smallest set of follow-up changes needed
121+
3. Use the code editing tools to revise the implementation
122+
123+
Use your coding tools to make changes directly to the codebase.
124+
"""
125+
)
126+
127+
pr_links = pr_links_section(run_state)
128+
if pr_links:
129+
prompt = f"{prompt}\n{pr_links}"
130+
131+
return prompt
132+
133+
134+
def pr_links_section(run_state: "SeerRunState | None") -> str:
135+
"""Render a section linking the open pull request URLs for the run, if any."""
136+
if run_state is None:
137+
return ""
138+
139+
lines = [
140+
f"- {pr.repo_name}: {pr.pr_url}" for pr in run_state.repo_pr_states.values() if pr.pr_url
141+
]
142+
if not lines:
143+
return ""
144+
145+
header = (
146+
"We've created/updated the following pull request(s) as a result of your changes. "
147+
"Review each one — including its description, diff, and review comments — "
148+
"before making further changes:"
149+
)
150+
return "\n".join([header, *lines])
151+
152+
81153
def artifact_tool_str(artifact_key: str | None) -> str:
82154
if not artifact_key:
83155
return "with"

0 commit comments

Comments
 (0)