# 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-sdk` package:

```text
npm install -g @grok/agent-sdk
```

- A Cursor credential for model turns. Sign in once:

```text
agent-sdk login
```

## Create 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 dev
```

Keep `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 opened
```

Open the playground and confirm `Review acme/checkout#42` contains the
trigger, both tool calls, and the verdict.

## Run options

1. **CLI message**
  ```text
   agent-sdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"
  ```
2. **Local replay**
  ```text
   agent-sdk github replay https://github.com/acme/checkout/pull/42 \
     --dir . --action opened
  ```

## Deploy the reviewer

Follow [Deployment](/docs/deployment.md) and run `agent-sdk deploy`. After the command reports the runtime is running, open [Deployed Agents](https://cursor.com/dashboard/deployed-agents) to see the reviewer and manage it.

## Where to go next

- [Evals](/docs/evals.md): freeze these two PRs as regression checks so
prompt changes can't flip a verdict
- [Tools](/docs/reference/tools.md): more on typed tools, dry-run effects,
and direct calls
- [GitHub](/docs/guides/github.md): fixtures, forwarding, and pulling
events from Cursor
- [Building agents with agents](/docs/building-with-agents.md): have a
coding agent extend the project for you
