AI SDK
Beginner4 min read
Overview
createGithubTools() returns the GitHub tools as an AI SDK ToolSet, so they go wherever tools goes: generateText, streamText, or your own agent. createGithubAgent() wraps the same tools in a ToolLoopAgent with the preset's system prompt and the approval policy already set up.
Pick this entry point when you already call the AI SDK, or want the smallest setup. Vercel Workflow and Chat SDK build on it, so what you learn here carries over.
Install
pnpm add @github-tools/sdk ai zod
npm install @github-tools/sdk ai zod
yarn add @github-tools/sdk ai zod
bun add @github-tools/sdk ai zod
Set GITHUB_TOKEN as in Installation. For a script, a CI job or a webhook, where one call produces one answer, pass the tools to generateText:
import { createGithubTools } from '@github-tools/sdk'
import { generateText, stepCountIs } from 'ai'
const { text } = await generateText({
model: 'anthropic/claude-opus-5.5',
tools: createGithubTools({ preset: 'repo-explorer' }),
stopWhen: stepCountIs(10),
prompt: 'Summarize the last five merged pull requests on vercel/ai.',
})
The model needs several steps to call tools and then answer. generateText stops after the first step unless you pass stopWhen.
Stream to a chat UI
The same tools work in a route that streams to a chat UI:
import { createGithubTools } from '@github-tools/sdk'
import { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai'
export async function POST(request: Request) {
const { messages }: { messages: UIMessage[] } = await request.json()
const result = streamText({
model: 'anthropic/claude-opus-5.5',
tools: createGithubTools({ preset: 'code-review' }),
stopWhen: stepCountIs(20),
messages: await convertToModelMessages(messages),
})
return result.toUIMessageStreamResponse()
}
createGithubAgent
When the same configuration serves several routes or jobs, define an agent once and call .generate() or .stream():
import { createGithubAgent } from '@github-tools/sdk'
export const reviewer = createGithubAgent({
model: 'anthropic/claude-opus-5.5',
preset: 'code-review',
context: { owner: 'vercel', repo: 'ai' },
additionalInstructions: 'Cite file paths and line numbers.',
})
const { text } = await reviewer.generate({ prompt: 'Review pull request #42.' })
Each preset comes with a system prompt. additionalInstructions appends to it, and instructions replaces it. Every other ToolLoopAgent setting, such as stopWhen, providerOptions or prepareCall, passes through. API lists every option.
Only the agent takes preset: 'auto', because picking tools for each request needs its prepareCall hook. Presets explains the routing.
Approval in a chat UI
Every write tool asks before it runs. In a chat, the request reaches the client as a tool part in the approval-requested state, the person clicks a button, and the answer goes back to the server. The server streams the agent:
import { createGithubAgent } from '@github-tools/sdk'
import { createAgentUIStreamResponse } from 'ai'
const agent = createGithubAgent({
model: 'anthropic/claude-opus-5.5',
preset: 'issue-triage',
experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
})
export async function POST(request: Request) {
const { messages } = await request.json()
return createAgentUIStreamResponse({ agent, uiMessages: messages })
}
The client renders the request and sends the answer back:
'use client'
import { useChat } from '@ai-sdk/react'
import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
export function Chat() {
const { messages, addToolApprovalResponse } = useChat({
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
})
const answer = (id: string, approved: boolean) => addToolApprovalResponse({ id, approved })
return messages.flatMap(message => message.parts.map((part) => {
if (part.type !== 'tool-addLabels' || part.state !== 'approval-requested') return null
return (
<div key={part.toolCallId}>
<p>Add {part.input.labels.join(', ')} to #{part.input.issueNumber}?</p>
<button onClick={() => answer(part.approval.id, true)}>Approve</button>
<button onClick={() => answer(part.approval.id, false)}>Deny</button>
</div>
)
}))
}
sendAutomaticallyWhen sends the conversation back once every pending request has an answer. The approved tool then runs on the server, and a denied one returns a denial the model can read.
The client sends the whole history back, approvals included, so sign them: the approval secret rejects an approved: true the server didn't issue. Approval covers the policy itself.
Show why the agent asks
With requireApproval: 'auto' on ai 7, a request carries the decision model's reason in part.approval.requestReason. Parts with part.approval.isAutomatic were decided without a person, so render them without buttons.
FAQ
ai 6.0.97 and later, and ai 7. Everything on this page runs on both, except signing, which needs ai 7.0.36 or later, and requireApproval: 'auto' and preset: 'auto', which need ai 7.0.105 or later.github_tools.EVALUATION_UNAVAILABLE.ai 7. createGithubAgent sets the AI SDK 7 call-level toolApproval, which carries the reasons. On ai 6, approval runs through each tool's needsApproval, without reasons.needsApproval deprecated in favor of toolApproval on generateText and streamText, but still honors it. createGithubTools always sets the tool-level needsApproval.