Skip to content

Commit 34022c5

Browse files
authored
Merge pull request anthropics#698 from anthropics/cj-ant/sentry-managed-agent
feat(managed_agents): add Sentry triage scheduled agent example
2 parents fe65adf + 3dc70b5 commit 34022c5

18 files changed

Lines changed: 603 additions & 6 deletions

managed_agents/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ report.html
22
__pycache__/
33
*.pyc
44
.env
5+
*.whl

managed_agents/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
description = "Runnable examples for the Claude Managed Agents API"
55
requires-python = ">=3.11,<3.13"
66
dependencies = [
7-
"anthropic>=0.91.0",
7+
"anthropic>=0.109.0",
88
"python-dotenv",
99
]
1010

managed_agents/sentry/.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Anthropic auth, either of:
2+
# - an API key from https://platform.claude.com/
3+
# - nothing here, after signing in once with `ant auth login`
4+
# (the SDK discovers CLI credentials automatically)
5+
ANTHROPIC_API_KEY=sk-ant-...
6+
7+
# Sentry: Settings → Auth Tokens → Create New Token
8+
# with org:read, project:read, and event:read scopes
9+
SENTRY_AUTH_TOKEN=sntrys_... # secret: goes in a vault, nowhere else
10+
SENTRY_ORG=your-org-slug # the slug in your Sentry URL: sentry.io/organizations/<slug>/
11+
SENTRY_PROJECT=your-project # project slug, not the numeric ID
12+
13+
# From `uv run python setup_agent.py`
14+
CLAUDE_VAULT_ID=vlt...
15+
CLAUDE_AGENT_ID=agent_...
16+
CLAUDE_ENVIRONMENT_ID=env_...
17+
18+
# From `uv run python deploy.py`
19+
CLAUDE_DEPLOYMENT_ID=deployment_...
20+
21+
# Optional: override the agent's model (read by agent_config.py)
22+
# COOKBOOK_MODEL=claude-sonnet-4-6

managed_agents/sentry/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.env
2+
.venv/
3+
__pycache__/
4+
*.pyc
5+
uv.lock
6+
TRIAGE_REPORT.md

managed_agents/sentry/CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Sentry triage × Claude Managed Agents
2+
3+
Scheduled deployment: cron → CMA session with `sentry-cli` and a vault env-var credential → triage report in `/mnt/session/outputs/`.
4+
5+
## When the user asks to set this up, get it working, or debug it
6+
7+
1. **Invoke `/claude-api` first.** That skill loads the full Managed Agents API reference (agents, sessions, environments, events, webhooks, deployments, vaults, memory stores). Use it as the source of truth for any SDK call you write or edit. Don't guess field names.
8+
2. **Read `./skill.md`** and walk the user through it step by step. It has the ordered checklist, every gotcha (two separate host allowlists, immutable `secret_name`, replace-only `allowed_hosts`, DST cron semantics, auto-pause on permanent failures), and the debugging table.
9+
3. **After the base schedule works, offer extensions.** Ask the user which (if any) they want, then edit `setup_agent.py` and/or `deploy.py` accordingly:
10+
- **Deliver the report** instead of leaving it in the sandbox: a `session.status_idled` webhook that downloads the report and posts to Slack (see `managed_agents/slack` for the bridge pattern)
11+
- **More CLIs**: add other env-var-authenticated CLIs as additional vault credentials, one credential per token with its own host allowlist
12+
- **Outcomes**: rubric-graded iterate loop (`user.define_outcome` event instead of `user.message` in `initial_events`)
13+
- **Memory store**: track issue history across runs so the agent can flag regressions it has seen before (`resources: [{type: "memory_store", ...}]`)
14+
15+
Pull exact shapes from the `/claude-api` skill's `shared/managed-agents-*.md` docs.
16+
17+
One-time provisioning is `uv run python setup_agent.py`. Schedule with `uv run python deploy.py`. Smoke-test with `uv run python run_now.py`. Push prompt or model changes with `uv run python update_agent.py` (edits live in `agent_config.py`).

managed_agents/sentry/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Sentry triage × Claude Managed Agents
2+
3+
A scheduled [Managed Agent](https://platform.claude.com/docs/en/managed-agents/overview) that pulls the last 24 hours of Sentry issues with `sentry-cli` and writes a prioritized triage report. No host process: the cron schedule lives server-side as a deployment.
4+
5+
```
6+
cron (0 9 * * 1-5) ──▶ deployment ──▶ session (sandbox)
7+
│ sentry-cli / curl with
8+
│ placeholder token
9+
10+
egress proxy: placeholder → real token,
11+
*.sentry.io only
12+
13+
TRIAGE_REPORT.md in /mnt/session/outputs/
14+
```
15+
16+
The Sentry token is an `environment_variable` vault credential. The sandbox holds an opaque placeholder, and the egress proxy substitutes the real token only on requests to `*.sentry.io`. The model never sees the secret. The same pattern works for `gh`, `twilio`, `vercel`, or any other CLI that authenticates via an env var.
17+
18+
## Quickstart
19+
20+
```bash
21+
cd managed_agents/sentry
22+
uv sync
23+
claude "walk me through setting this up."
24+
```
25+
26+
Claude reads [`skill.md`](./skill.md) and drives the config: Sentry token, vault + agent + environment, a manual test run, then the cron deployment.
27+
28+
Authenticate with an `ANTHROPIC_API_KEY` in `.env`, or sign in once with [`ant auth login`](https://platform.claude.com/docs/en/api/sdks/cli). Either works: the SDK discovers CLI credentials automatically when no API key is set.
29+
30+
## Files
31+
32+
| | |
33+
|---|---|
34+
| `cma.py` | Shared client, env loading, event streaming |
35+
| `agent_config.py` | Model and system prompt, shared by setup and update |
36+
| `setup_agent.py` | One-time: vault + credential + agent + environment |
37+
| `update_agent.py` | Push prompt or model changes, re-pin the deployment |
38+
| `deploy.py` | `deployments.create` with a cron schedule |
39+
| `run_now.py` | Manual trigger, stream the session, download the report |
40+
| `runs.py` | Run history and failures |
41+
| `teardown.py` | Pause and archive everything |
42+
| `skill.md` | Setup walkthrough, gotchas, debugging |
43+
44+
Requires `anthropic` ≥ 0.109.0.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Agent configuration: model and system prompt.
2+
3+
Shared by setup_agent.py (first create) and update_agent.py (push changes).
4+
"""
5+
6+
import os
7+
8+
from dotenv import load_dotenv
9+
10+
load_dotenv()
11+
12+
MODEL = os.environ.get("COOKBOOK_MODEL", "claude-opus-4-8")
13+
14+
15+
def build_system_prompt(org: str, project: str) -> str:
16+
# The system prompt carries everything that isn't a secret: org and project
17+
# slugs, the triage method, the report format. Never the token; system
18+
# prompts are stored in the session's event history.
19+
return f"""You are an SRE triage assistant. Each run, you produce a morning \
20+
triage report covering the last 24 hours of Sentry issues for the on-call engineer.
21+
22+
## Sentry access
23+
24+
- `sentry-cli` is installed. It authenticates via the SENTRY_AUTH_TOKEN environment \
25+
variable, which is already set. Never print it, and never pass it as a CLI flag.
26+
- Org: `{org}` Project: `{project}`
27+
- For data the CLI doesn't expose (event counts, user counts, stack traces), call the \
28+
REST API directly, e.g.:
29+
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
30+
"https://sentry.io/api/0/organizations/{org}/issues/?project={project}&query=is:unresolved&statsPeriod=24h&sort=freq"
31+
32+
## Workflow
33+
34+
1. Pull unresolved issues from the last 24 hours (new and escalating).
35+
2. For the highest-impact issues, pull details: event count, users affected, \
36+
first/last seen, culprit, a representative stack trace.
37+
3. Classify each as NEW (first seen <24h), REGRESSION (was resolved, came back), \
38+
ESCALATING (event count accelerating), or ONGOING.
39+
4. Rank by user impact, not raw event count.
40+
41+
## Output
42+
43+
Write the report to /mnt/session/outputs/TRIAGE_REPORT.md:
44+
45+
- **Summary**: 2-3 sentences. New issue count, total users affected, anything on fire.
46+
- **Top issues** (max 5): title, short ID, classification, users affected, event count, \
47+
one-line root-cause hypothesis, suggested next step.
48+
- **Watchlist**: issues that didn't make the top 5 but are worth an eye.
49+
50+
Keep it under one page. The reader is an on-call engineer with five minutes. If there \
51+
are no issues in the window, say so in one line. Do not pad."""

managed_agents/sentry/cma.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Shared client, env loading, and streaming helpers for the triage scripts."""
2+
3+
import os
4+
import sys
5+
import time
6+
7+
from anthropic import Anthropic
8+
from dotenv import load_dotenv
9+
10+
load_dotenv()
11+
12+
# The SDK adds the managed-agents beta header automatically for agents,
13+
# environments, sessions, vaults, and deployments. The files resource doesn't:
14+
# without it, /v1/files rejects scope_id ("unknown field"), so pass it
15+
# explicitly when listing session-scoped files (run_now.py).
16+
BETAS = ["managed-agents-2026-04-01"]
17+
18+
client = Anthropic()
19+
20+
21+
def require_env(name: str) -> str:
22+
value = os.environ.get(name, "")
23+
if not value or value.endswith("..."):
24+
sys.exit(f"{name} is not set in .env (see .env.example)")
25+
return value
26+
27+
28+
def wait_for_idle_status(session_id: str, max_wait: float = 5.0) -> None:
29+
"""Poll until the server-side status is `idle`.
30+
31+
The `session.status_idle` stream event can arrive slightly before
32+
`sessions.retrieve` reports `status == "idle"`, so an `archive()`
33+
issued straight after the stream exits can 400. This absorbs the race.
34+
"""
35+
deadline = time.monotonic() + max_wait
36+
while time.monotonic() < deadline:
37+
if client.beta.sessions.retrieve(session_id).status == "idle":
38+
return
39+
time.sleep(0.25)
40+
41+
42+
def stream_until_end_turn(session_id: str) -> None:
43+
"""Stream a session's events and print until the agent finishes its turn.
44+
45+
`session.status_idle` is not always terminal: with `stop_reason.type ==
46+
"requires_action"` the agent is waiting on a client event (a tool
47+
confirmation or custom tool result), so keep streaming. The terminal stop
48+
reasons are `end_turn` (normal completion) and `retries_exhausted`
49+
(failure). Breaking only on `end_turn` would hang forever on a failed run.
50+
"""
51+
with client.beta.sessions.events.stream(session_id) as stream:
52+
for ev in stream:
53+
match ev.type:
54+
case "agent.message":
55+
for block in ev.content:
56+
if block.type == "text":
57+
print(block.text, end="")
58+
case "agent.tool_use":
59+
print(f"\n[{ev.name}]")
60+
case "session.status_idle" if (
61+
ev.stop_reason and ev.stop_reason.type != "requires_action"
62+
):
63+
if ev.stop_reason.type != "end_turn":
64+
print(f"\nsession stopped: {ev.stop_reason.type}")
65+
break
66+
case "session.status_terminated":
67+
return
68+
wait_for_idle_status(session_id)

managed_agents/sentry/deploy.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Create the scheduled deployment: weekday mornings, 9 AM Eastern.
2+
3+
Copy the printed deployment ID into .env.
4+
"""
5+
6+
from cma import client, require_env
7+
8+
CLAUDE_AGENT_ID = require_env("CLAUDE_AGENT_ID")
9+
CLAUDE_ENVIRONMENT_ID = require_env("CLAUDE_ENVIRONMENT_ID")
10+
CLAUDE_VAULT_ID = require_env("CLAUDE_VAULT_ID")
11+
SENTRY_ORG = require_env("SENTRY_ORG")
12+
SENTRY_PROJECT = require_env("SENTRY_PROJECT")
13+
14+
TRIAGE_PROMPT = (
15+
"Run today's Sentry triage. Pull the last 24 hours of unresolved issues for "
16+
f"{SENTRY_ORG}/{SENTRY_PROJECT}, triage them per your instructions, and write "
17+
"the report to /mnt/session/outputs/TRIAGE_REPORT.md. "
18+
"Reply with the Summary section when you're done."
19+
)
20+
21+
# The schedule is a POSIX cron expression plus an IANA timezone, matched on
22+
# wall-clock time (see skill.md for the DST edges). Sessions start themselves
23+
# on Anthropic infra; nothing keeps running on this machine.
24+
deployment = client.beta.deployments.create(
25+
name="Weekday morning Sentry triage",
26+
agent=CLAUDE_AGENT_ID,
27+
environment_id=CLAUDE_ENVIRONMENT_ID,
28+
vault_ids=[CLAUDE_VAULT_ID],
29+
initial_events=[
30+
{
31+
"type": "user.message",
32+
"content": [{"type": "text", "text": TRIAGE_PROMPT}],
33+
}
34+
],
35+
schedule={
36+
"type": "cron",
37+
"expression": "0 9 * * 1-5", # weekday mornings
38+
"timezone": "America/New_York",
39+
},
40+
)
41+
42+
print(f"deployment: {deployment.id} ({deployment.status})")
43+
print("next runs:")
44+
for ts in deployment.schedule.upcoming_runs_at:
45+
print(f" {ts}")
46+
print("\nAdd to .env:")
47+
print(f"CLAUDE_DEPLOYMENT_ID={deployment.id}")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "sentry-triage-cma"
3+
version = "0.1.0"
4+
description = "Scheduled Sentry triage with a Claude Managed Agent"
5+
requires-python = ">=3.11,<3.13"
6+
dependencies = [
7+
"anthropic>=0.109.0",
8+
"python-dotenv",
9+
]

0 commit comments

Comments
 (0)