Build a GitHub agent with the eve extension
eve is Vercel's filesystem-first agent framework: an agent is a folder with instructions, a model config, and tools. @github-tools/eve-extension packages all 84 GitHub tools as a mountable eve extension: a single pnpm add and a one-line mount under agent/extensions/, no CLI setup, and no direct SDK import in agent/tools/.
createGithubTools and per-tool factories from @github-tools/sdk/eve) are deprecated in its favor. They keep working for existing agent/tools/ setups, but new agents should mount the extension instead. Shared runtime helpers used by this extension live on @github-tools/sdk/eve-runtime (not deprecated).Add GitHub tools to an eve agent via the extension
The whole agent
Three files. That's the entire thing:
You are a GitHub assistant. Use the GitHub tools to inspect repos, PRs, and issues.
Ask before merging or closing anything destructive.
import { defineAgent } from 'eve'
export default defineAgent({
model: 'anthropic/claude-sonnet-5',
})
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
preset: 'maintainer',
})
Run it:
npx eve dev
You now have a GitHub agent that can read repos, review PRs, triage issues, and manage CI, with every write operation gated behind durable approval by default. The full example lives in examples/eve/ (or pnpm dev:eve from the monorepo root).
For complete production implementations, see the Software Factory and Incident Response Agent. Both use the eve extension with Vercel Connect, while scoping GitHub access for different operational needs.
Install
pnpm add @github-tools/eve-extension
npm install @github-tools/eve-extension
yarn add @github-tools/eve-extension
bun add @github-tools/eve-extension
eve is a required peer dependency (>=0.44, which itself requires ai v7):
pnpm add eve
npm install eve
yarn add eve
bun add eve
You still need GITHUB_TOKEN (or a Vercel Connect connector, below). See Installation.
Mount it
Drop a file under agent/extensions/. The filename becomes the tool namespace:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector', // or token: process.env.GITHUB_TOKEN
preset: 'code-review',
requireApproval: {
addPullRequestComment: ({ toolInput }) => toolInput?.owner !== 'vercel-labs',
},
})
Tools are exposed to the model as <namespace>__<toolName>, where <namespace> comes from the mount file's name: agent/extensions/github.ts yields github__listPullRequests, github__createIssue, and so on.
code-review is used above (rather than maintainer) because it pairs cleanly with a Connect connector. maintainer and repo-explorer include gist tools, and GitHub only grants gist access to user access tokens, never the installation tokens Connect mints, so gist calls 403 over Connect (see Tokens & Auth). The requireApproval predicate above is a real gate, not a no-op: write tools already require approval by always() by default, so { mergePullRequest: true } would change nothing. A predicate is what actually narrows or loosens the default.
Pick exact tools
preset scopes to one of the ten predefined presets. To hand-pick tools instead, standalone or layered on top of a preset, use include and exclude:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
include: ['getRepository', 'listPullRequests', 'mergePullRequest'],
})
include adds to preset. The effective set is the union of both, so you can pull in a tool a preset is missing without switching to a bigger preset. When connector is set and connect.scopes is omitted, Connect scopes follow that same resolved tool set — a standalone include does not mint the full admin union:
export default githubExtension({
preset: 'code-review', // read-only PR review tools
include: ['createIssue'], // + one write tool code-review doesn't include
})
exclude removes tool names from whatever preset + include resolved to. Useful for dropping a couple of tools you don't want exposed from a larger preset:
export default githubExtension({
preset: 'maintainer', // all 84 tools
exclude: ['createRepository', 'deleteGist'], // minus these two
})
Config schema
| Field | Type | Notes |
|---|---|---|
token | string | (() => Promise<string>) | PAT string, or an async provider for rotating tokens (e.g. a GitHub App installation token) — the same GithubTokenInput the SDK accepts; falls back to GITHUB_TOKEN when omitted and connector is not set |
connector | string | (() => string | Promise<string>) | Vercel Connect connector name, or a resolver to pick one dynamically (e.g. per environment/tenant); takes priority over token |
connect | record? | Passed through to getToken when connector is set; connect.subject defaults to { type: 'app' } and also accepts a per-caller resolver, see Per-user tokens |
preset | preset name or array | code-review, issue-triage, ci-ops, repo-explorer, security-audit, release-manager, discussion-moderator, notification-inbox, pr-author, maintainer, see Presets |
include | string[]? | Tool names to add on top of preset (union), or the full set standalone, see Pick exact tools |
exclude | string[]? | Tool names to remove from the resolved preset + include set |
context | { owner?, repo?, pullNumber?, issueNumber?, ref? }? | Default owner/repo/number/ref for tool inputs — matching fields become optional and fill from context when omitted, see Working context |
requireApproval | boolean | record | Global or per-tool; per-tool values may be 'once', 'always', 'never', or predicate functions |
overrides | record | Per-tool description / approval / toModelOutput / outputSchema |
author / committer / coAuthors | commit identity | Attribution for commit-creating tools, see Commit Attribution |
The provider runs on every tool call, so short-lived tokens stay fresh:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
token: () => mintInstallationToken(),
preset: 'issue-triage',
})
Durable multi-turn sessions
The extension registers each tool with an authored inline execute, toModelOutput, and approval as direct defineTool properties that only close over a serializable tool name, then rebuilds session options from the extension config on every call via @github-tools/sdk/eve-runtime. Tools resolve on step.started so registration stays fresh across durable steps. That pattern survives multi-turn eve Workflow replay (see #51, #99). A spread or call-expression callback (resolveEveApproval(...), a ternary-wrapped toModelOutput) has no durable descriptor: on eve 0.44+ the resolver discards the entire GitHub toolset. Prefer this mount over the deprecated createGithubTools / connectGithubTools paths for Slack / multi-turn durable agents — those register tools from inside node_modules and are skipped on replay. Author overrides.toModelOutput inline in the agent; a function imported from a library will not get a durable descriptor.
Object-shaped execute results include rateLimit (remaining, limit, reset, resource). toModelOutput strips it so the model never sees the remaining count; toolResultFrom and channels still do. See Rate-limit metadata.
If execute fails (token mint, GitHub 403/429, …), the tool returns { error } instead of throwing so the model always receives a tool_result. A thrown error in eve's tool-loop can leave a tool_use unpaired and kill the turn.
Durable approval, done right
Approval pauses the session durably until a human responds, and policies are expressive:
| Value | Maps to | Behavior |
|---|---|---|
true / 'always' | always() | Require approval on every call |
false / 'never' | omit approval | Skip approval (eve default) |
'once' | once() | Approve once per session, then auto-allow |
| predicate | custom Approval | Input-dependent gate (toolInput, session context) |
Default (no requireApproval): all write tools → always(). Unlisted write tools keep the always() fail-safe default. Read tools never require approval. Details: Control write safety.
Vercel Connect
Skip GITHUB_TOKEN entirely and mint the token from a Connect connector. Pass connector directly in the mount config, no separate connectGithubTools import needed:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector',
preset: 'code-review',
})
Unlike the deprecated direct import, no build.externalDependencies workaround is needed in agent/agent.ts. The extension is pre-built via eve extension build and loaded through eve's extension mechanism rather than inlined from a workspace-linked source import.
@vercel/connect is an optional peer dependency of the extension, install it only when using connector. connector also accepts a () => string | Promise<string> resolver for picking a connector per environment or tenant, see dynamic connector selection. See Vercel Connect for the connector setup checklist and multi-tenant scoping.
Per-user tokens
By default Connect mints the project's GitHub App installation token (subject: { type: 'app' }) — one identity shared by every caller. That is right for single-tenant agents, but in multi-user apps where each user connects their own GitHub account, it silently gives every signed-in user the project-level access. Set connect.subject to a per-caller resolver to mint each caller's own connection token instead. The resolver receives the eve tool execution context on every tool call:
import githubExtension from '@github-tools/eve-extension'
export default githubExtension({
connector: 'github/my-connector',
preset: 'issue-triage',
connect: {
subject: (ctx) => {
const caller = ctx.session.auth.current
if (!caller) throw new Error('GitHub tools require an authenticated caller')
return { type: 'user', id: caller.principalId }
},
},
})
ctx.session.auth.current is the authenticated caller of the active turn — eve restores it durably across workflow replay, so the resolver stays correct in multi-turn sessions. A caller without an active GitHub connection gets a UserAuthorizationRequiredError from Connect instead of silently falling back to the app installation. A static subject value (e.g. { type: 'user', id } fixed at mount time) also works when the agent serves a single known user.
Idempotency
eve replays completed steps but re-runs steps interrupted mid-execution:
| Tool | Idempotency |
|---|---|
createOrUpdateFile | Natural when content + sha unchanged |
closeIssue | Natural when already closed |
createBranch | Natural when branch exists at same SHA |
addIssueComment, createIssue, mergePullRequest, … | Not idempotent |
Gate non-idempotent writes behind always() or once() where replay safety matters.
Migrating from the direct import
If you have an existing agent using @github-tools/sdk/eve directly in agent/tools/, move to the extension in three steps:
pnpm add @github-tools/eve-extensionand remove the now-unused directeve/ai/zodinstall if nothing else in the agent needs them.- Delete
agent/tools/github.tsand createagent/extensions/github.tsexportinggithubExtension({ ...same options... })instead ofcreateGithubTools({ ...same options... }). Options (preset,include,exclude,context,requireApproval,overrides,author/committer/coAuthors,token) are unchanged. - If you used
connectGithubToolsfrom@github-tools/sdk/connect/eve, drop it. Passconnectordirectly togithubExtensioninstead. - If you cherry-picked single-tool factories (e.g.
listPullRequests()exported alone from its own file), replace eachagent/tools/*.tsfile with aninclude: [...]list in the singleagent/extensions/github.tsmount, see Pick exact tools.
One behavior change to be aware of: tool names gain the <namespace>__ prefix described above, so update any code that references tool names by their bare string (requireApproval and include keys stay bare, only the exposed model-facing tool name gets the prefix).
eve extension vs direct import vs AI SDK vs Workflow SDK
| eve extension | eve (direct import) | AI SDK | Workflow SDK | |
|---|---|---|---|---|
| Import | @github-tools/eve-extension | @github-tools/sdk/eve (createGithubTools) | @github-tools/sdk | @github-tools/sdk/workflow |
| Mount point | agent/extensions/ | agent/tools/ | anywhere | workflow function |
| Status | Recommended | Deprecated | Active | Active |
| Tool registration | githubExtension() mount | defineDynamic in agent/tools/ | createGithubTools() object | createGithubTools() in workflow |
| Tool naming | <namespace>__<toolName> | bare tool name | bare tool name | bare tool name |
| Approval | always / once / predicates | always / once / predicates | boolean needsApproval | boolean needsApproval (durable pause) |
| Durability | eve session (filesystem-first) | eve session (filesystem-first) | in-process | "use workflow" steps |
External references
- How to build a GitHub agent with eve and GitHub tools
- eve documentation
- eve extensions
- Dynamic capabilities (bundled with the
evepackage) - Human-in-the-loop