Skip to content

Add Sentry triage agent & MCP - #6705

Draft
jpetto wants to merge 1 commit into
mozilla:masterfrom
jpetto:HNT-2886-agent-to-triage-sentry-alerts
Draft

Add Sentry triage agent & MCP#6705
jpetto wants to merge 1 commit into
mozilla:masterfrom
jpetto:HNT-2886-agent-to-triage-sentry-alerts

WIP

770ae60
Select commit
Loading
Failed to load commit list.
Community-TC Integration / bugbug lint failed Aug 24, 2026 in 2m 17s

Community-TC (pull_request)

bugbug lint

Details

View task in Taskcluster | View logs in Taskcluster | View task group in Taskcluster

Task Status

Started: 2026-08-24T21:12:01.117Z
Resolved: 2026-08-24T21:14:17.632Z
Task Execution Time: 2 minutes, 16 seconds, 515 milliseconds
Task Status: failed
Reason Resolved: failed
TaskId: ceUHvGAwQLKl9Sftm8byQQ
RunId: 0

Artifacts

- public/logs/live_backing.log (1.4 MB)
- public/logs/live.log


[taskcluster 2026-08-24T21:12:01.200Z] Worker Type (proj-bugbug/batch) settings:
[taskcluster 2026-08-24T21:12:01.200Z]   {
[taskcluster 2026-08-24T21:12:01.200Z]     "generic-worker": {
[taskcluster 2026-08-24T21:12:01.200Z]       "config": {
[taskcluster 2026-08-24T21:12:01.200Z]         "headlessTasks": false
[taskcluster 2026-08-24T21:12:01.200Z]       },
[taskcluster 2026-08-24T21:12:01.200Z]       "engine": "multiuser",
[taskcluster 2026-08-24T21:12:01.200Z]       "go-arch": "amd64",
[taskcluster 2026-08-24T21:12:01.200Z]       "go-os": "linux",
[taskcluster 2026-08-24T21:12:01.200Z]       "go-version": "go1.26.2",
[taskcluster 2026-08-24T21:12:01.200Z]       "release": "https://github.com/taskcluster/taskcluster/releases/tag/v99.1.0",
[taskcluster 2026-08-24T21:12:01.200Z]       "revision": "c76d61efe4bdc1a05bcec848739cd41ebf061f01",
[taskcluster 2026-08-24T21:12:01.200Z]       "source": "https://github.com/taskcluster/taskcluster/commits/c76d61efe4bdc1a05bcec848739cd41ebf061f01",
[taskcluster 2026-08-24T21:12:01.200Z]       "version": "99.1.0"
[taskcluster 2026-08-24T21:12:01.200Z]     },
[taskcluster 2026-08-24T21:12:01.200Z]     "image": "projects/community-tc-workers/global/images/generic-worker-ubuntu-24-04-aaynirqnxlbwcojgnbeo",
[taskcluster 2026-08-24T21:12:01.200Z]     "instance-id": "6376971069628157056",
[taskcluster 2026-08-24T21:12:01.200Z]     "instance-type": "projects/757942385826/machineTypes/n2-standard-2",
[taskcluster 2026-08-24T21:12:01.200Z]     "local-ipv4": "10.142.0.28",
[taskcluster 2026-08-24T21:12:01.200Z]     "machine-setup": {

...(101 lines hidden)...

Found 4 errors (4 fixed, 0 remaining).

ruff format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook

warning: Selection `CPY` has no effect because preview is not enabled.
3 files reformatted, 449 files left unchanged

check python ast.........................................................Passed
check docstring is first.................................................Passed
check that executables have shebangs.....................................Passed
check for merge conflicts................................................Passed
check for broken symlinks............................(no files to check)Skipped
debug statements (python)................................................Passed
trim trailing whitespace.................................................Passed
check yaml...............................................................Passed
mixed line ending........................................................Passed
python tests naming......................................................Passed
check json...............................................................Passed
check vcs permalinks.....................................................Passed
codespell................................................................Passed
taskcluster_yml..........................................................Passed
Strip unnecessary `# noqa`s..............................................Passed
mypy-bugbug..............................................................Passed
mypy-bugbug-http.........................................................Passed
Check for useless excludes...............................................Passed
pre-commit hook(s) made changes.
If you are seeing this message in CI, reproduce locally with: `pre-commit run --all-files`.
To run `pre-commit` as part of git workflow, use `pre-commit install`.
All changes made by hooks:
diff --git a/agents/sentry-triage/hackbot_agents/sentry_triage/__main__.py b/agents/sentry-triage/hackbot_agents/sentry_triage/__main__.py
index 0c1b2c01..8e0f1bca 100644
--- a/agents/sentry-triage/hackbot_agents/sentry_triage/__main__.py
+++ b/agents/sentry-triage/hackbot_agents/sentry_triage/__main__.py
@@ -10,7 +10,7 @@ class AgentInputs(BaseSettings):
     model: str | None = None
     max_turns: int | None = None
     effort: str | None = None
-    
+
     model_config = SettingsConfigDict(extra="ignore")
 
 
diff --git a/agents/sentry-triage/hackbot_agents/sentry_triage/agent.py b/agents/sentry-triage/hackbot_agents/sentry_triage/agent.py
index 92cc033b..643e73f4 100644
--- a/agents/sentry-triage/hackbot_agents/sentry_triage/agent.py
+++ b/agents/sentry-triage/hackbot_agents/sentry_triage/agent.py
@@ -69,7 +69,9 @@ def make_alert_investigator() -> AgentDefinition:
     )
 
 
-def parse_sentry_alert_url_for_issue_and_event_ids(sentry_alert_url: str) -> tuple[str, str]:
+def parse_sentry_alert_url_for_issue_and_event_ids(
+    sentry_alert_url: str,
+) -> tuple[str, str]:
     """Parses a Sentry alert URL for the issue and event ids."""
     # TODO: parse the URL for issue_id and event_id
     # e.g. https://mozilla.sentry.io/issues/7325411784/events/16f37d8003bb42f4abff707fb57bfca0/?alert=5775&detection_type=static&notification_uuid=a672e1f5-d561-4f2d-92f4-441dd86e8fd2&openPeriod=589904722&project=4510958917713920&referrer=metric_alert_slack&statsPeriod=12d
@@ -88,19 +90,20 @@ async def run_alert_triage(
     effort: str | None = None,
     verbose: bool = False,
     log: Path | None = None,
-    actions_recorder: ActionsRecorder | None = None
+    actions_recorder: ActionsRecorder | None = None,
 ) -> SentryTriageResult:
     """Triage a single Sentry alert with a claude-agent-sdk agent.
 
     Returns a :class:`SentryTriageResult` on success; raises :class:`AgentError` if the
     agent ends in an error.
     """
-
     # load custom claude rules
     if rules_dir is None:
         rules_dir = HERE / "rules"
 
-    print(f"[sentry_triage] triaging sentry alert at {sentry_alert_url}", file=sys.stderr)
+    print(
+        f"[sentry_triage] triaging sentry alert at {sentry_alert_url}", file=sys.stderr
+    )
 
     # Action-recording MCP server (in-process). Standalone/script runs pass
     # actions_recorder=None and get a local recorder that copies attachments
@@ -109,12 +112,19 @@ async def run_alert_triage(
         actions_recorder, types=TRIAGE_ACTIONS
     )
 
-    sentry_issue_id, sentry_event_id = parse_sentry_alert_url_for_issue_and_event_ids(sentry_alert_url)
+    sentry_issue_id, sentry_event_id = parse_sentry_alert_url_for_issue_and_event_ids(
+        sentry_alert_url
+    )
 
     enabled_action_tools = actions_to_tool_names(TRIAGE_ACTIONS)
 
     system_prompt = render_prompt("system.md", rules_dir=str(rules_dir.resolve()))
-    user_prompt = render_prompt("triage.md", issue_id=sentry_issue_id, event_id=sentry_event_id, rules_path=rules_dir)
+    user_prompt = render_prompt(
+        "triage.md",
+        issue_id=sentry_issue_id,
+        event_id=sentry_event_id,
+        rules_path=rules_dir,
+    )
 
     options = ClaudeAgentOptions(
         add_dirs=[str(rules_dir.resolve())],
@@ -155,7 +165,9 @@ async def run_alert_triage(
                     result_msg = msg
 
     if result_msg is None:
-        raise AgentError(f"sentry alert url {sentry_alert_url}: agent produced no result message")
+        raise AgentError(
+            f"sentry alert url {sentry_alert_url}: agent produced no result message"
+        )
 
     if result_msg.is_error:
         raise AgentError(
diff --git a/agents/sentry-triage/hackbot_agents/sentry_triage/broker.py b/agents/sentry-triage/hackbot_agents/sentry_triage/broker.py
index 7987f6df..7e5bc781 100644
--- a/agents/sentry-triage/hackbot_agents/sentry_triage/broker.py
+++ b/agents/sentry-triage/hackbot_agents/sentry_triage/broker.py
@@ -8,10 +8,10 @@ no credentials:
 - Sentry: the `Sentry` MCP tools over `/mcp` (read-only, live during the run).
 """
 
-import httpx
 import logging
 from contextlib import asynccontextmanager
 
+import httpx
 import uvicorn
 from agent_tools import sentry
 from agent_tools.claude_sdk import build_sdk_server
diff --git a/libs/agent-tools/agent_tools/sentry.py b/libs/agent-tools/agent_tools/sentry.py
index 794f2c82..3ee98dfe 100644
--- a/libs/agent-tools/agent_tools/sentry.py
+++ b/libs/agent-tools/agent_tools/sentry.py
@@ -1,9 +1,10 @@
+from dataclasses import dataclass
+from typing import Annotated
+
 import httpx
+from pydantic import Field
 
 from agent_tools.registry import ToolError, tool, tools_in
-from dataclasses import dataclass
-from pydantic import Field
-from typing import Annotated, Any
 
 
 @dataclass
@@ -24,21 +25,28 @@ def _sentry_error(e: Exception, what: str, http_status_error: str) -> ToolError:
 
 @tool
 async def get_issue_event(
-    ctx: SentryContext, 
-    issue_id: Annotated[
-        str, Field(description="The Sentry issue ID.")
-    ],event_id: str):
+    ctx: SentryContext,
+    issue_id: Annotated[str, Field(description="The Sentry issue ID.")],
+    event_id: str,
+):
     """Retrieve an issue event from Sentry."""
     try:
-        headers = { "Authorization": f"Bearer {ctx.api_token}" }
-        resp = await ctx.client.get(f"{ctx.api_url_base}/organizations/{ctx.org_name}/issues/{issue_id}/events/{event_id}/", headers=headers)
+        headers = {"Authorization": f"Bearer {ctx.api_token}"}
+        resp = await ctx.client.get(
+            f"{ctx.api_url_base}/organizations/{ctx.org_name}/issues/{issue_id}/events/{event_id}/",
+            headers=headers,
+        )
         resp.raise_for_status()
         return resp.json()
     except httpx.HTTPStatusError as e:
         # get the http error, falling back to a generic "sentry_error"
-        http_status_error = {401: "auth_failed", 403: "access_denied", 404: "not_found"}.get(e.response.status_code, "sentry_error")
+        http_status_error = {
+            401: "auth_failed",
+            403: "access_denied",
+            404: "not_found",
+        }.get(e.response.status_code, "sentry_error")
 
         raise _sentry_error(e, "get_issue_event error", http_status_error) from e
 
 
-TOOLS = tools_in(__name__)
\ No newline at end of file
+TOOLS = tools_in(__name__)
[taskcluster 2026-08-24T21:14:15.604Z]                        Exit Code: 1
[taskcluster 2026-08-24T21:14:15.604Z]                        User Time: 49.268ms
[taskcluster 2026-08-24T21:14:15.604Z]                      Kernel Time: 45.163ms
[taskcluster 2026-08-24T21:14:15.604Z]                        Wall Time: 2m11.645465025s
[taskcluster 2026-08-24T21:14:15.604Z]  Average Available System Memory: 6.40 GiB
[taskcluster 2026-08-24T21:14:15.604Z]       Average System Memory Used: 1.35 GiB
[taskcluster 2026-08-24T21:14:15.604Z]          Peak System Memory Used: 1.71 GiB
[taskcluster 2026-08-24T21:14:15.604Z]              Total System Memory: 7.75 GiB
[taskcluster 2026-08-24T21:14:15.604Z]                           Result: FAILED
[taskcluster 2026-08-24T21:14:15.604Z] === Task Finished ===
[taskcluster 2026-08-24T21:14:15.604Z] Task Duration: 2m11.646288791s
[taskcluster:error] exit status 1