Appearance
Build a PR reviewer with Agent SDK
Build a PR reviewer with the Agent SDK. It classifies changes as trivial, moderate, or large, then approves safe changes or requests human review.
Prerequisites
Node 22.13 or newer.
The
@grok/agent-sdkpackage:
text
npm install -g @grok/agent-sdk- A Cursor credential for model turns. Sign in once:
text
agent-sdk loginCreate and run the project
The preferred Agent SDK workflow uses agent-sdk init and agent-sdk dev.
text
agent-sdk init ./pr-reviewer
cd pr-reviewer
agent-sdk devKeep agent-sdk dev running. In a second terminal, run:
text
agent-sdk run --dir . --message "Introduce yourself in one sentence."Confirm the agent replies.
Add review instructions
Replace bot/instructions.md:
md
# PR reviewer
You review GitHub pull requests. Be specific and brief.
For every pull request:
1. Call `inspect_pr` first. Never judge a change you haven't fetched.
2. Match your review to the complexity it reports:
- `trivial`: read the patches. If the diff does what the title says
and nothing looks risky, call `submit_review` with verdict
`approve`.
- `moderate`: read every patch. Approve only when you understand the
whole change and see no risk. Otherwise ask for a human review and
say which files worry you.
- `large`: call `submit_review` with verdict `request_human_review`
right away. Use the stats you already have to point the reviewer at
the biggest files; don't dig further.
3. Never approve a draft. Point out anything surprising, even when you
approve.
End with one sentence: the verdict and why.Add the inspect tool
Create bot/tools/inspect_pr.ts:
ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
export type Complexity = "trivial" | "moderate" | "large";
function rateComplexity(linesChanged: number, changedFiles: number): Complexity {
if (linesChanged <= 25 && changedFiles <= 2) {
return "trivial";
}
if (linesChanged <= 400 && changedFiles <= 15) {
return "moderate";
}
return "large";
}
/** Keep one oversized file from flooding the model's context. */
function trimPatch(patch: string | undefined): string | undefined {
if (patch === undefined || patch.length <= 3000) {
return patch;
}
return `${patch.slice(0, 3000)}\n[... patch trimmed ...]`;
}
export default defineTool({
description:
"Fetch a pull request's title, stats, and per-file patches, plus a deterministic complexity rating (trivial, moderate, or large). Call this before any review decision.",
inputSchema: z.object({
prUrl: z
.string()
.describe("Pull request URL: https://github.com/owner/repo/pull/123"),
}),
async execute({ prUrl }, ctx) {
const url = new URL(prUrl);
const [owner, repo, pulls, number] = url.pathname.split("/").filter(Boolean);
const pull_number = Number.parseInt(number ?? "", 10);
if (pulls !== "pull" || owner === undefined || Number.isNaN(pull_number)) {
throw new Error(`Not a pull request URL: ${prUrl}`);
}
const octokit = await ctx.host.github.getOctokit();
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo,
pull_number,
});
const { data: files } = await octokit.rest.pulls.listFiles({
owner,
repo,
pull_number,
per_page: 100,
});
const complexity = rateComplexity(
pr.additions + pr.deletions,
pr.changed_files
);
return {
title: pr.title,
author: pr.user?.login,
state: pr.state,
draft: pr.draft ?? false,
additions: pr.additions,
deletions: pr.deletions,
changedFiles: pr.changed_files,
complexity,
files: files.map((file) => ({
path: file.filename,
additions: file.additions,
deletions: file.deletions,
...(complexity === "large" ? {} : { patch: trimPatch(file.patch) }),
})),
};
},
});Verify complexity classification
Run inspect_pr against a small pull request:
text
agent-sdk tool call inspect_pr --dir . \
--input '{"prUrl":"https://github.com/facebook/react/pull/35623"}'Confirm the result contains "complexity": "trivial".
Run it against a large pull request:
text
agent-sdk tool call inspect_pr --dir . \
--input '{"prUrl":"https://github.com/facebook/react/pull/25229"}'Confirm the result contains "complexity": "large" and no patch fields.
Add the review tool
Create bot/tools/submit_review.ts to post each verdict:
ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
export default defineTool({
description:
"Post the review decision to GitHub: approve the pull request, or comment asking for a human review.",
inputSchema: z.object({
prUrl: z
.string()
.describe("Pull request URL: https://github.com/owner/repo/pull/123"),
verdict: z.enum(["approve", "request_human_review"]),
summary: z
.string()
.describe("One or two sentences explaining the verdict."),
}),
async execute({ prUrl, verdict, summary }, ctx) {
const url = new URL(prUrl);
const [owner, repo, pulls, number] = url.pathname.split("/").filter(Boolean);
const pull_number = Number.parseInt(number ?? "", 10);
if (pulls !== "pull" || owner === undefined || Number.isNaN(pull_number)) {
throw new Error(`Not a pull request URL: ${prUrl}`);
}
const review =
verdict === "approve"
? { event: "APPROVE" as const, body: `PR reviewer: ${summary}` }
: {
event: "COMMENT" as const,
body: `PR reviewer: this change needs a human review. ${summary}`,
};
const octokit = await ctx.host.github.getOctokit();
await octokit.rest.pulls.createReview({
owner,
repo,
pull_number,
event: review.event,
body: review.body,
});
return { posted: true, ...review };
},
});Use a GitHub credential with write access and a pull request you didn't author.
Review a pull request
Review an open pull request:
text
agent-sdk run --dir . \
--message "Review https://github.com/acme/checkout/pull/42"Confirm inspect_pr runs before submit_review, then verify the review on GitHub.
Trigger reviews from GitHub
Create bot/channels/github.ts:
ts
import {
defaultGitHubAuth,
githubChannel,
} from "@grok/agent-sdk/channels/github";
const REVIEW_ACTIONS = new Set(["opened", "reopened", "ready_for_review"]);
export default githubChannel({
botName: "agent-sdk-pr-reviewer",
cursorAccount: { repos: ["acme/checkout"] },
onPullRequest: (ctx, pr) => {
if (!REVIEW_ACTIONS.has(pr.action) || pr.draft) {
return null;
}
return {
auth: defaultGitHubAuth(ctx),
title: `Review ${ctx.repository.fullName}#${pr.number}`,
context: [
`Review ${pr.url}. Inspect it first, then submit your verdict with submit_review.`,
],
};
},
});Channel-turn context is a readonly string[], so wrap each context block in an array. The context label under progress.commitStatus is a separate string option.
Keep agent-sdk dev running. In a second terminal, replay an open pull request. Pull access is enough; repo admin isn't required:
text
agent-sdk github replay https://github.com/acme/checkout/pull/42 \
--dir . --action openedOpen the playground and confirm Review acme/checkout#42 contains the trigger, both tool calls, and the verdict.
Run options
- CLI message
text
agent-sdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"- Local replay
text
agent-sdk github replay https://github.com/acme/checkout/pull/42 \
--dir . --action openedDeploy the reviewer
Follow Deployment and run agent-sdk deploy. After the command reports the runtime is running, open Deployed Agents to see the reviewer and manage it.
Where to go next
- Evals: freeze these two PRs as regression checks so prompt changes can't flip a verdict
- Tools: more on typed tools, dry-run effects, and direct calls
- GitHub: fixtures, forwarding, and pulling events from Cursor
- Building agents with agents: have a coding agent extend the project for you